使用WebCodec在网页中进行视频编解码

1,704 阅读2分钟

在网页中播放视频,可以通过几种方式实现:

  • 通过video标播放视频
  • 通过webassembly对视频数据解码,然后渲染到canvas上面
  • WebCodec 这次主要介绍一下WebCodec,需要Chrome 94以上版本可以使用。

流程

graph LR
获取摄像头 --> 显示采集画面 --> 编码 --> 显示编码后的画面 --> 解码 --> 显示解码后的画面

获取摄像头

浏览器自带API支持获取摄像头,具体可以参考文档

``` typescript
navigator.mediaDevices.getUserMedia({video: true}).then((mediastream) => {
    // 获取摄像头成功
}).catch((err) => {
    // 获取摄像头失败
});
``` 

VideoEncoder

通过VideoEncoder可以在网页中使用浏览器的硬编能力,相比使用wasm进行软编,性能还是有很大的提升的。

  videoEncoder = new VideoEncoder({
    output: (chunk) => {
      // 在handleVideoEncoded中处理编码后的数据
      handleVideoEncoded(chunk);
    },
    error: (error) => {
      console.error('video encoder error:', error);
    }
  });
  videoEncoder.configure({ 
    codec: 'vp8', 
    width: 1280, 
    height: 720 
  });

  let videoProcessor = new MediaStreamTrackProcessor(stream.getVideoTracks()[0]);
  // videoGenerator就是一个videoTrack,可以添加到MediaStream中用<video>来显示
  let videoGenerator = new MediaStreamTrackGenerator('video');
  let videoTransformer = new TransformStream({
    transform: (frame, controller) => {
      // 每60帧会插入一个关键帧
      const insert_keyframe = (sendFrames % 60) == 0;
      videoEncoder.encode(frame, { keyFrame: insert_keyframe });
      controller.enqueue(frame);
      sendFrames++;
    }
  });
  videoProcessor
    .readable
    .pipeThrough(videoTransformer)
    .pipeTo(videoGenerator.writable);
     

VideoDeocder

    videoDecoder = new VideoDecoder({
       output: (frame) => {
         if (videoWriter) {
           videoWriter.write(frame);
         }
       },
       error: (error) => {
         console.error('video decoder error:' + error);
       }
     });
     videoDecoder.configure({ 
       codec: 'vp8', 
       width: 1280, 
       height: 720 
     });
     
     // videoGenerator就是解码后的VideoTrack,可以添加到MediaStream中用<video>来显示
     let videoGenerator = new MediaStreamTrackGenerator('video');
     videoWriter = videoGenerator.writable.getWriter();
       

上效果

效果如图: image.png

图中有三个画面,分别是显示采集摄像头画面、编码后的画面、解码后的画面。通过秒表可以看到,采集画面和编码后的画面基本没有时差(此处应该是手机秒表的间隔时间大于了编码耗时,所以截图好多次,都是采集画面和编码画面是一模一样的),解码画面和猜忌画面的延迟大概在20ms左右。 图中视频使用的是1280*720 30帧,视频编码使用的vp8(从api上看,应该是h264、h265、vp都能支持的,但是我只跑通了vp8。。。)

结论

  • 优点
    • 可以使用浏览器的硬编、硬解的能力,性能上相比软编软解有明显优势
    • 可以配置浏览器的WebTransport的能力,在音视频的场景下摆脱对webrtc的依赖,在浏览器端使用自研的协议,不再只能使用webrtc
  • 缺点
    • 浏览器兼容性,目前只有Chrome支持,并且需要的浏览器版本较高

代码

最后上代码,本地服务用localhost或者127.0.0.1都可以,如果用域名需要https才可以

<!doctype html>
<html>

<head>
  <meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>
  <title>WebCodecs Demo</title>
</head>

<body>
  <style>
    .item {
      width: 320px;
      display: inline-block;
      text-align: center;
    }

    .video {
      width: 1280;
      height: 720;
    }
  </style>
  <h1>WebCodecs Demo</h1>
  <br />
  <button id="startBtn">开始</button>
  <br />
  <div>
    <div class="item">
      <span>采集画面:</span>
      <video id="localVideo" autoplay class="video"></video>
    </div>
    <div class="item">
      <span>编码画面:</span>
      <video id="encodeVideo" autoplay class="video"></video>
    </div>
    <div class="item">
      <span>解码画面::</span>
      <video id="playVideo" autoplay class="video"></video>
    </div>
  </div>

  <script type='text/javascript'>
    let localVideo = document.getElementById("localVideo");
    let encodeVideo = document.getElementById("encodeVideo");
    let playVideo = document.getElementById("playVideo");
    let startBtn = document.getElementById("startBtn");
    let playBtn = document.getElementById("playBtn");
    let sendFrames = 0;
    const codec = 'vp8';
    let waitKeyFrame = true;
    let videoEncoder = null;
    let videoDecoder = null;
    let videoWriter = null; // WritableStreamDefaultWriter

    const videoConstraints = {
      width: 640,
      height: 480
    }

    startBtn.addEventListener('click', () => {
      initVideoEncoder();
      initVideoDecoder();
      sendFrames = 0;
      navigator.mediaDevices.getUserMedia({ video: videoConstraints }).then((stream) => {
        localVideo.srcObject = stream;
        let videoProcessor = new MediaStreamTrackProcessor(stream.getVideoTracks()[0]);
        let videoGenerator = new MediaStreamTrackGenerator('video');
        let videoTransformer = new TransformStream({
          transform: (frame, controller) => {
            const insert_keyframe = (sendFrames % 60) == 0;
            videoEncoder.encode(frame, { keyFrame: insert_keyframe });
            controller.enqueue(frame);
            sendFrames++;
          }
        });
        videoProcessor.readable.pipeThrough(videoTransformer).pipeTo(videoGenerator.writable);

        // 此处需要显示videoGenerator,否则一段时间就会报错
        let processedStream = new MediaStream();
        processedStream.addTrack(videoGenerator);
        encodeVideo.srcObject = processedStream;
      }).catch((err) => {

      })
    })

    function initVideoEncoder() {
      videoEncoder = new VideoEncoder({
        output: (chunk) => {
          handleVideoEncoded(chunk);
        },
        error: (error) => {
          console.error('video encoder error:', error);
        }
      });
      videoEncoder.configure({ codec: codec, width: videoConstraints.width, height: videoConstraints.height });
    }

    function handleVideoEncoded(chunk) {
      let data = new Uint8Array(chunk.byteLength);
      chunk.copyTo(data.buffer);
      chunk.data = data;

      const encoded = new EncodedVideoChunk(chunk);
      videoDecoder.decode(encoded);
    }
    function initVideoDecoder() {
      videoDecoder = new VideoDecoder({
        output: (frame) => {
          if (videoWriter) {
            videoWriter.write(frame);
          }
        },
        error: (error) => {
          console.error('video decoder error:' + error);
        }
      });
      videoDecoder.configure({ codec: codec, width: videoConstraints.width, height: videoConstraints.height });

      let videoGenerator = new MediaStreamTrackGenerator('video');
      videoWriter = videoGenerator.writable.getWriter();
      console.log("writer:", videoWriter);

      let processedStream = new MediaStream();
      processedStream.addTrack(videoGenerator);
      playVideo.srcObject = processedStream;
    }
  </script>
</body>

</html>

其他

如果你也是专注前端多媒体或者对前端多媒体感兴趣,可以关注前端多媒体公众号

qrcode_for_gh_1f34d1ba9020_258.jpg