最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 用WebAssembly在浏览器中对视频进行转码

    正文概述 掘金(前端先锋)   2021-01-29   875

    我们可以凭借 FFmpeg 的 WebAssembly 版直接在浏览器中运行这个能强大的视频处理工具。在本文中,我们来探索一下 FFmpeg.wasm,并写一个简单的代码转换器,把数据流传输到视频元素中并播放出来。

    用WebAssembly在浏览器中对视频进行转码

    FFmpeg.wasm

    一般我们通过其命令行使用 FFmpeg。例如下面的命令可以吧 AVI 文件转码为 MP4 格式:

    $ ffmpeg -i input.avi output.mp4
    

    同样的工作也可以在浏览器中做到。FFmpeg.wasm 是 FFmpeg 的 WebAssembly 端口,像其他 JavaScript 模块一样可以通过 npm 安装,并在 Node 或浏览器中使用:

    $ npm install @ffmpeg/ffmpeg @ffmpeg/core
    

    装好 FFmpeg.wasm 后,可以在浏览器中执行等效的转码操作:

    // fetch AVI 文件
    const sourceBuffer = await fetch("input.avi").then(r => r.arrayBuffer());
    
    // 创建 FFmpeg 实例并载入
    const ffmpeg = createFFmpeg({ log: true });
    await ffmpeg.load();
    
    // 把 AVI 写入 FFmpeg 文件系统
    ffmpeg.FS(
      "writeFile",
      "input.avi",
      new Uint8Array(sourceBuffer, 0, sourceBuffer.byteLength)
    );
    
    // 执行 FFmpeg 命令行工具, 把 AVI 转码为 MP4
    await ffmpeg.run("-i", "input.avi", "output.mp4");
    
    // 把 MP4 文件从 FFmpeg 文件系统中取出
    const output = ffmpeg.FS("readFile", "output.mp4");
    
    // 对视频文件进行后续操作
    const video = document.getElementById("video");
    video.src = URL.createObjectURL(
      new Blob([output.buffer], { type: "video/mp4" })
    );
    

    这里有很多有趣的东西,接下来深入研究细节。

    在 fetch API 加载 AVI 文件之后,用下面的步骤初始化 FFmpeg:

    const ffmpeg = createFFmpeg({ log: true });
    await ffmpeg.load();
    

    FFmpeg.wasm 由一个很薄的 JavaScript API 层和一个较大的(20M)WebAssembly 二进制文件组成。上面的代码加载并初始化了可供使用的 WebAssembly 文件。

    WebAssembly 是在浏览器中运行的、经过性能优化的底层字节码。它被专门设计为能够用多种语言进行开发和编译。

    FFmpeg 的历史已经超过20年了,有一千多人贡献过代码。在 WebAssembly 出现之前,要给它创建 JavaScript 能够调用的接口,所涉及的工作可能会非常繁琐。

    未来 WebAssembly 的使用会更加广泛,现在它作为把大量成熟的 C/C++ 代码库引入 Web 的一种机制,已经非常成功了, Google Earth,AutoCAD 和 TensorFlow 等都是非常典型的案例。

    在初始化之后,下一步是把 AVI 文件写入文件系统:

    ffmpeg.FS(
      "writeFile",
      "input.avi",
      new Uint8Array(sourceBuffer, 0, sourceBuffer.byteLength)
    );
    

    这段代码有些奇怪,想要知道这是什么情况,需要更深入地研究 FFmpeg.wasm 的编译方式。

    Emscripten 是遵循 WebAssembly 规范开发的把 C/C++ 代码编译为 WebAssembly 的工具链,正是它把 FFmpeg.wasm 编译为 WebAssembly 的。但是 Emscripten 不只是一个 C++ 编译器,为了简化现有代码库的迁移,它通过基于 Web 的等效项提供对许多 C/C++ API 的支持。例如通过把函数调用映射到 WebGL 来支持 OpenGL。它还支持 SDL、POSIX 和 pthread。

    Emscripten 通过提供 file-system API 来映射到内存中的存储。使用 FFmpeg.wasm 可以直接通过 ffmpeg.FS 函数公开底层的 Emscripten 文件系统API,你可以用这个借口浏览目录、创建文件和其他各种针对文件系统的操作。

    下一步是真正有意思的地方:

    await ffmpeg.run("-i", "input.avi", "output.mp4");
    

    如果你在 Chrome 的开发工具中进行观察,会注意到它创建了许多 Web Worker,每个 Web Worker 都加载了 ffmpeg.wasm:

    用WebAssembly在浏览器中对视频进行转码

    在这里用到了 Emscripten 的 Pthread 支持(https://emscripten.org/docs/porting/pthreads.html)。启用日志记录后,你可以在控制台中查看进度;

    Output #0, mp4, to 'output.mp4':
       Metadata:
         encoder         : Lavf58.45.100
         Stream #0:0: Video: h264 (libx264) (avc1 / 0x31637661), yuv420p, 256x240, q=-1--1, 35 fps, 17920 tbn, 35 tbc
         Metadata:
           encoder         : Lavc58.91.100 libx264
         Side data:
           cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
    frame=   47 fps=0.0 q=0.0 size=       0kB time=00:00:00.00 bitrate=N/A speed=   0x
    frame=   76 fps= 68 q=30.0 size=       0kB time=00:00:00.65 bitrate=   0.6kbits/s speed=0.589x
    frame=  102 fps= 62 q=30.0 size=       0kB time=00:00:01.40 bitrate=   0.3kbits/s speed=0.846x
    

    最后一步是读取输出文件并将其提供给 video 元素:

    const output = ffmpeg.FS("readFile", "output.mp4");
    const video = document.getElementById("video");
    video.src = URL.createObjectURL(
      new Blob([output.buffer], { type: "video/mp4" })
    );
    

    有趣的是,带有虚拟文件系统的命令行工具 FFmpeg.wasm 有点像 docker!

    创建流式转码器

    对大视频文件进行编码转换可能耗时较长。我们可以先把文件转码为切片,并将其逐步添加到视频缓冲区中。

    你可以用 Media Source Extension APIs 来构建流媒体播放,其中包括 MediaSourceSourceBuffer 对象。创建和加载缓冲区的操作可能非常棘手,因为这两个对象都提供了生命周期事件,必须通过处理这些事件才能在正确的时间添加新的缓冲区。为了管理这些事件的协调,我用到了 RxJS。

    下面的函数基于 FFmpeg.wasm 转码后的输出创建一个 RxJS Observable:

    const bufferStream = filename =>
      new Observable(async subscriber => {
        const ffmpeg = FFmpeg.createFFmpeg({
          corePath: "thirdparty/ffmpeg-core.js",
          log: false
        });
    
        const fileExists = file => ffmpeg.FS("readdir", "/").includes(file);
        const readFile = file => ffmpeg.FS("readFile", file);
    
        await ffmpeg.load();
        const sourceBuffer = await fetch(filename).then(r => r.arrayBuffer());
        ffmpeg.FS(
          "writeFile", "input.mp4",
          new Uint8Array(sourceBuffer, 0, sourceBuffer.byteLength)
        );
    
        let index = 0;
    
        ffmpeg
          .run(
            "-i", "input.mp4",
            // 给流进行编码
            "-segment_format_options", "movflags=frag_keyframe+empty_moov+default_base_moof",
            // 编码为 5 秒钟的片段
            "-segment_time", "5",
            // 通过索引写入文件系统
            "-f", "segment", "%d.mp4"
          )
          .then(() => {
            // 发送剩余的文件内容
            while (fileExists(`${index}.mp4`)) {
              subscriber.next(readFile(`${index}.mp4`));
              index++;
            }
            subscriber.complete();
          });
    
        setInterval(() => {
          // 定期检查是否已写入文件
          if (fileExists(`${index + 1}.mp4`)) {
            subscriber.next(readFile(`${index}.mp4`));
            index++;
          }
        }, 200);
      });
    

    上面的代码用了和以前相同的 FFmpeg.wasm 设置,将要转码的文件写入内存文件系统。为了创建分段输出,ffmpeg.run 的配置与上一个例子有所不同,需要设置合适的转码器。在运行时 FFmpeg 把带有增量索引(0.mp41.mp4, …)的文件写入内存文件系统。

    为了实现流式传输输出,需要通过轮询文件系统以获取转码后的输出,并通过 subscriber.next 把数据作为事件发出。最后当 ffmpeg.run 完成时,余下的文件内容被送出并关闭流。

    需要创建一个MediaSource对象来把数据流传输到视频元素中,并等待 sourceopen 事件触发。下面的代码用到了 RxJS 的 combineLatest 来确保在触发这个事件之前不处理 FFmpeg 输出:

    const mediaSource = new MediaSource();
    videoPlayer.src = URL.createObjectURL(mediaSource);
    videoPlayer.play();
    
    const mediaSourceOpen = fromEvent(mediaSource, "sourceopen");
    
    const bufferStreamReady = combineLatest(
      mediaSourceOpen,
      bufferStream("4club-JTV-i63.avi")
    ).pipe(map(([, a]) => a));
    

    当接收到第一个视频切片或缓冲时,需要在正确的时间向 SourceBuffer 添加 MediaSource,并将原始缓冲区附加到 SourceBuffer。在此之后,还有一个需要注意的地方,新缓冲不能马上添加到 SourceBuffer 中,需要等到它发出 updateend 事件表明先前的缓冲区已被处理后才行。

    下面的代码用 take 处理第一个缓冲区,并用 mux.js 库读取 mime 类型。然后从 updateend 事件返回一个新的可观察流:

    const sourceBufferUpdateEnd = bufferStreamReady.pipe(
      take(1),
      map(buffer => {
        // 基于当前的 mime type 创建一个buffer
        const mime = `video/mp4; codecs="${muxjs.mp4.probe
          .tracks(buffer)
          .map(t => t.codec)
          .join(",")}"`;
        const sourceBuf = mediaSource.addSourceBuffer(mime);
    
        // 追加道缓冲区
        mediaSource.duration = 5;
        sourceBuf.timestampOffset = 0;
        sourceBuf.appendBuffer(buffer);
    
        // 创建一个新的事件流 
        return fromEvent(sourceBuf, "updateend").pipe(map(() => sourceBuf));
      }),
      flatMap(value => value)
    );
    

    剩下的就是在缓冲区到达及 SourceBuffer 准备就绪时追加缓冲区。可以通过 RxJS 的 zip 函数实现:

    zip(sourceBufferUpdateEnd, bufferStreamReady.pipe(skip(1)))
      .pipe(
        map(([sourceBuf, buffer], index) => {
          mediaSource.duration = 10 + index * 5;
          sourceBuf.timestampOffset = 5 + index * 5;
          sourceBuf.appendBuffer(buffer.buffer);
        })
      )
      .subscribe();
    

    就这样对事件进行了一些协调,最终只需很少的代码就能对视频进行转码了,并将结果逐渐添加到视频元素中。

    最后一个例子的代码在GitHub上。

    欢迎关注我的公众号:前端先锋


    起源地下载网 » 用WebAssembly在浏览器中对视频进行转码

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元