代码之家  ›  专栏  ›  技术社区  ›  Amit

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

  •  0
  • Amit  · 技术社区  · 3 年前

    我想从上传到我的网站的多个图像创建一个视频文件。

    到目前为止,我所做的是拍摄这些图像,在画布上逐个绘制,并使用 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:元素没有支持的源。

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

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

    最小复制

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

    0 回复  |  直到 2 年前
        1
  •  13
  •   msaw328    2 年前

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

    1. 创建多媒体文件时,使用编码器算法对视频和音频数据进行编码,然后使用多路复用器将流组合到一个文件(容器)中。为了播放文件,解复用器将流拆开,并将它们输入解码器,以获得视频和音频数据。
    2. 编解码器是指编码器/解码器,是一个独立于容器格式的概念。许多容器格式可以容纳许多不同类型的格式(AVI和QuickTime/MOV非常通用)。其他格式仅限于一种或两种媒体类型。

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

    将字节缓冲区连接到一个大的数据块中是不起作用的:

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

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

    遗憾的是,到目前为止,复用器似乎还不是WebCodecs API的一部分。 Example 在中 official repository API的使用 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 是关于向浏览器添加muxing支持的讨论的链接,以及 here 是官方回购跟踪这一功能中的一个问题。到目前为止,您的问题似乎还没有一个内置的解决方案。

    要解决此问题,您可以使用第三方库,例如 mux.js 或类似( here 是他们的“基本用法”示例的链接,可能会对您有所帮助)。可替换地, this project 声称使用创建WebM容器 视频编码器 编码数据。摘录自的描述 their demo 似乎正是你想要实现的(除了使用网络摄像头 VideoFrame 源,而不是画布):

    当您单击“开始”按钮时,浏览器会要求您授予拍摄相机和麦克风的权限。然后,每个人的数据被传递给两个独立的工作人员,他们使用WebCodecs浏览器API将视频编码为VP9,将音频编码为Opus。

    来自每个工作者的编码视频和音频被传递给第三工作者,第三工作者将其多路复用为WebM格式。

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

    编辑: 我发现 another library 这可能会对你有所帮助。根据他们的自述:

    支持的内容:

    • MP4视频多路复用(获取已编码的H264帧并将其封装在MP4容器中)
    • 基于WebCodecs的MP4/H264编码和复用

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

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

    编辑2:

    David Kanal's answer below 提供了可用于复用WebM的库的另一个示例。

        2
  •  5
  •   Naej    1 年前

    更新(2023-04-13):

    为MP4制作多路复用器: https://github.com/Vanilagy/mp4-muxer

    更新(2022-11-10):

    由于我为这个主题找到的库不足以满足我的需求,我创建了自己的库: https://github.com/Vanilagy/webm-muxer

    这是一个功能齐全的 WebM复用器 (视频+音频)在纯TypeScript中,不需要庞大的wasm文件。README中详细解释了用法。这个库为我的 browser-based game .


    我想我会在这个话题上花两分钱,因为我最近正为OP提到的完全相同的事情而挣扎。

    我设法找到了一个渲染和导出WebM文件的解决方案,尽管没有音频。

    我在这里找到了W3C的一个官方示例: https://w3c.github.io/webcodecs/samples/capture-to-file/capture-to-file.html . 它会捕获网络摄像头的视频流,并将其保存为磁盘上的.webm文件。深入到代码中,负责获取编码视频块并将其写入(复用)到可播放的WebM的代码是 webm-writer2.js

    由于该文件包含在网站中,编写WebM文件所需的操作如下:

    // Acquire `fileHandle` somewhere, I use
    // https://developer.mozilla.org/en-US/docs/Web/API/Window/showSaveFilePicker
    
    let fileWritableStream = await fileHandle.createWritable();
    
    // This WebMWriter thing comes from the third-party library
    let webmWriter = new WebMWriter({
        fileWriter: fileWritableStream,
        codec: 'VP9',
        width: width,
        height: height
    });
    
    let encoder = new VideoEncoder({
        output: chunk => webmWriter.addFrame(chunk),
        error: e => console.error(e)
    });
    // Configure to your liking
    encoder.configure({
        codec: "vp09.00.10.08",
        width: width,
        height: height,
        bitrate: bitrate,
        latencyMode: 'realtime'
    });
    

    然后,只需像往常一样使用 encoder.encode(videoFrame) .

    希望这能帮助到别人。

        3
  •  2
  •   Nino Filiu    2 年前

    喜欢 msaw328 says ,在获取文件之前,您必须向原始编码块blob添加一些特定于格式的字节。但浏览器已经知道如何做到这一点了!问题变成了,我如何告诉浏览器这样做?

    嗯,与 captureStream ,您可以从画布中发生的事情中获取流,并使用 MediaRecorder 要录制此流,我将在中解释如何执行此操作 this answer 。这就是你已经做的,它有两个问题:

    • 如果在画布上绘制东西所需的时间不到1/60,我们就会让用户无所作为
    • 如果在画布上绘制东西的时间超过1/60,输出视频的速度就会全部减慢

    因此,我们可以采用的另一种设置是不使用 VideoEncoder 直接,而是使用 MediaStreamTrackGenerator 从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 应该只能实时工作,也许是铬虫。

    推荐文章