WebCodecs > VideoEncoder:从编码帧创建视频

WebCodecs > VideoEncoder: Create video from encoded frames

我想用上传到我网站的多张图片创建一个视频文件。

直到现在,我所做的就是拍摄这些图像,在 canvas 上 1 对 1 地绘制它们,然后使用 MediaRecorder API 来记录它们。不过空闲的时间很多。

相反,我想使用 VideoEncoder API。

我创建了一个将每个块保存为缓冲区的编码器:

const chunks = [];

let encoder = new VideoEncoder({
  output: (chunk) => {
    const buffer = new ArrayBuffer(chunk.byteLength)
    chunk.copyTo(buffer);
    chunks.push(buffer);
  },
  error: (e) => console.error(e.message)
});

并使用我的设置对其进行配置:

encoder.configure({
  codec: 'vp8',
  width: 256,
  height: 256,
  bitrate: 2_000_000,
  framerate: 25
});

然后,我将每个图像编码为一个帧:

const frame = new VideoFrame(await createImageBitmap(image));
encoder.encode(frame, {keyFrame: true});
frame.close();

最后,我尝试从中创建一个视频:

await encoder.flush();

const blob = new Blob(chunks, {type: 'video/webm; codecs=vp8'});
const url = URL.createObjectURL(blob);

但是,URL blob 无法播放。如果我尝试下载它,VLC 不会显示它。如果我将它设置为 video 元素的来源,我得到:

DOMException: The element has no supported sources.

如何将多个帧编码为可播放的视频?

我如何知道支持哪些编解码器/blob 类型?

最小复制

以下codepen是上面的代码,连接并加入一个函数。 https://codepen.io/AmitMY/pen/OJxgPoG?editors=0010

VideoEncoder 和 WebCodecs API 中的其他 类 为您提供了将图像编码为视频流中的帧的方法,但是编码只是创建图像的第一步可播放的多媒体文件。像这样的文件可能包含多个流 - 例如,当您有一个带声音的视频时,它已经至少有一个视频流和一个音频流,所以总共有两个。您需要额外的容器格式来存储流,这样您就不必在单独的文件中发送流。要从任意数量的流(甚至只有一个)创建容器文件,您需要一个多路复用器(简称 muxer)。可以在 this Stack Overflow 答案中找到很好的主题摘要,但要引用重要部分:

  1. When you create a multimedia file, you use a coder algorithms to encode the video and audio data, then you use a muxer to put the streams together into a file (container). To play the file, a demuxer takes apart the streams and feeds them into decoders to obtain the video and audio data.
  2. Codec means coder/decoder, and is a separate concept from the container format. Many container formats can hold lots of different types of format (AVI and QuickTime/MOV are very general). Other formats are restricted to one or two media types.

你可能会想“我只有一​​个流,我真的需要一个容器吗?”但是多媒体播放器希望接收到的数据(从文件中读取的数据或通过网络流式传输的数据)采用容器格式。即使你只有一个视频流,你仍然需要将它打包到一个容器中以便他们识别它。

将字节缓冲区加入一大块数据是行不通的:

const blob = new Blob(chunks, {type: 'video/webm; codecs=vp8'});

在这里您尝试将所有块粘合在一起并告诉浏览器将其解释为 WebM 视频(video/webm MIME 类型)但它不能这样做,因为它不是 properly formatted.这反过来又是错误的根源。为了使其工作,您必须将相关的元数据附加到您的块(通常格式化为具有特定格式的二进制数据缓冲区,具体格式取决于容器和编解码器的类型)并将其传递给多路复用器。如果您使用专为处理原始视频流(例如来自 WebCodecs API 的视频流)而设计的复用库,那么它将 可能 为您处理元数据.作为一名程序员,您很可能不必手动处理此问题,但是如果您想了解有关整个过程的更多信息,那么我建议您阅读各种容器格式中存在的元数据(例如,VC.Ones 下面的注释回答)。

遗憾的是,到目前为止,muxers 似乎还不是 WebCodecs API 的一部分。 API的Example in the official repository使用muxAndSend()函数作为编码器输出回调:

const videoEncoder = new VideoEncoder({
  output: muxAndSend,
  error: onEncoderError,
});

在上面的代码中我们可以看到这个函数需要程序员提供(原文注释):

// The app provides a way to serialize/containerize encoded media and upload it.
// The browser provides the app byte arrays defined by a codec such as vp8 or opus
// (not in a media container such as mp4 or webm).
function muxAndSend(encodedChunk) { ... };

Here is a link to a discussion about adding muxing support to browsers and here 是跟踪此功能的官方回购中的一个问题。截至目前,似乎没有针对您的问题的内置解决方案。

要解决它,您可以使用第三方库,例如 mux.js or similar (here is a link to their "Basic Usage" example which may help you). Alternatively, this project claims to create WebM containers out of VideoEncoder encoded data. This excerpt from the description of their demo,这似乎正是您想要实现的(除了使用网络摄像头作为 VideoFrame 源,而不是 canvas):

When you click the Start button, you’ll be asked by the browser to give permission to capture your camera and microphone. The data from each is then passed to two separate workers which encode the video into VP9 and audio into Opus using the WebCodecs browser API.

The encoded video and audio from each worker is passed into a third worker which muxes it into WebM format.

我无法为您提供代码示例,因为我自己没有使用过任何提到的库,但我相信在理解了编码器和多路复用器之间的关系之后,您应该能够自己解决问题。

编辑: 我发现 another library 可能对您有帮助。根据他们的自述文件:

What's supported:

  • MP4 video muxing (taking already-encoded H264 frames and wrapping them in a MP4 container)
  • MP4/H264 encoding and muxing via WebCodecs

我在网上找到的许多库和资源似乎都是基于 WASM 的,通常是用 C 或其他编译为本机代码的语言实现的。这可能是因为存在处理各种媒体格式的大型库(首先想到的是ffmpeg),这就是它们的编写方式。JS库通常被编写为绑定到所述本机代码以避免重新发明轮子。此外,我认为性能也可能是一个因素。

免责声明:虽然您在代码示例中使用 video/webm 作为 MIME 类型,但您没有明确说明您希望输出的文件格式是什么,所以我允许自己参考一些生成的库其他格式。

一样,您必须在获取文件之前向原始编码块 blob 添加几个 format-specific 字节。但是浏览器已经知道该怎么做了!问题变成了,我怎样才能告诉浏览器这样做?

好吧,使用 captureStream,您可以从 canvas 中发生的事情中获取一个流,并使用 MediaRecorder 来记录这个流,我将在 。那是你已经做过的,它有两个问题:

  • 如果在 canvas 上绘制内容花费的时间少于 1/60 秒,我们将让用户无需等待
  • 如果在 canvas 上绘制东西需要超过 1/60 秒,输出视频将全部变慢

所以我们可以有的另一个设置是不直接使用 VideoEncoder,而是使用 MediaStreamTrackGenerator to generate a stream from raw VideoFrames,并将流传递给 MediaRecorder。总而言之,它看起来像这样:

(async () => {
  // browser check
  if (typeof MediaStreamTrackGenerator === undefined || typeof MediaStream === undefined || typeof VideoFrame === undefined) {
    console.log('Your browser does not support the web APIs used in this demo');
    return;
  }
  
  // canvas setup
  const canvas = document.createElement("canvas");
  canvas.width = 256;
  canvas.height = 256;
  const ctx = canvas.getContext("2d");

  // recording setup
  const generator = new MediaStreamTrackGenerator({ kind: "video" });
  const writer = generator.writable.getWriter();
  const stream = new MediaStream();
  stream.addTrack(generator);
  const recorder = new MediaRecorder(stream, { mimeType: "video/webm" });
  recorder.start();

  // animate stuff
  console.log('rendering...')
  for (let i = 0; i < 246; i++) {
    ctx.fillStyle = "grey";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = "red";
    ctx.fillRect(i, i, 10, 10);

    const frame = new VideoFrame(canvas, { timestamp: i / 29.97 });
    await writer.write(frame);
    await new Promise(requestAnimationFrame);
  }
  console.log('rendering done');

  // stop recording and 
  recorder.addEventListener("dataavailable", (evt) => {
    const video = document.createElement('video');
    video.src = URL.createObjectURL(evt.data);
    video.muted = true;
    video.autoplay = true;
    document.body.append(video);
  });
  recorder.stop();
})();

我仍然无法理解的一件事是为什么我们需要等待下一帧:如果我们不这样做,生成的 blob 是空的,如果我们等待两倍的时间,生成的视频就会慢两倍.也许 MediaRecorder 应该只能实时工作,也许这是一个 chromium 错误。