使用FFmpeg和WebAssembly在浏览器中对视频文件进行转码

2020-11-25 03:47:37

FFmpeg的WebAssembly构建允许您直接在浏览器中运行此功能强大的视频处理工具。在此博客文章中,我将探索FFmpeg.wasm并创建一个简单的客户端代码转换器,将数据流传输到视频元素中,并加入一些RxJS以作很好的衡量。

FFmpeg最常通过其命令行界面使用。例如,您可以按如下方式将AVI文件转码为MP4中的等效视频文件:

让我们看看您如何在浏览器中执行相同的任务……

FFmpeg.wasm是FFmpeg的WebAssembly端口,您可以通过npm安装该端口,并在Node或浏览器中使用它,就像其他任何JavaScript模块一样:

安装FFmpeg.wasm后,您可以完全在浏览器中执行等效的代码转换,如下所示:

//获取AVI文件const sourceBuffer = await fetch(“ input.avi”)。然后(r => r。arrayBuffer()); //创建FFmpeg实例并加载它const ffmpeg = createFFmpeg({log:true});等待ffmpeg。加载(); //将AVI写入FFmpeg文件系统ffmpeg。 FS(“ writeFile”,“ input.avi”,新的Uint8Array(sourceBuffer,0,sourceBuffer。byteLength)); //运行FFmpeg命令行工具,将AVI转换为MP4等待ffmpeg。运行(“ -i”,“ input.avi”,“ output.mp4”); //从FFmpeg文件系统const output = ffmpeg读回MP4文件。 FS(“ readFile”,“ output.mp4”); // ...现在对文件const video = document进行处理。 getElementById(“ video”);视频 。 src = URL。 createObjectURL(新的Blob([output.buffer],{type:“ video / mp4”}));

这里有很多有趣的事情,所以让我们深入研究细节。

使用获取API加载AVI文件后,以下步骤将初始化FFmpeg本身:

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

WebAssembly是在浏览器中运行的,经过性能优化的新低级字节码。它被专门设计为多种语言的编译目标,并且是允许现有的非浏览器应用程序定位到Web的便捷工具。

在这种情况下,FFmpeg是一个已有20年历史的项目,拥有超过1,000名贡献者和近10万次提交。在进行WebAssembly之前,几乎无法想象要创建此库的JavaScript端口,所涉及的工作可能很繁琐!此外,JavaScript的性能特征可能会限制这种方法的有效性。

从长远来看,我们可能会看到WebAssembly的使用更为广泛,但就目前而言,WebAssembly作为将成熟的大量C / C ++代码库引入网络的一种机制最为成功。 Google Earth,AutoCAD和TensorFlow

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

好吧,这有点奇怪不是吗?要了解这里发生的情况,我们需要更深入地研究FFmpeg.wasm的编译方式。

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

Emscripten提供了映射到内存中存储的文件系统API。使用FFmpeg.wasm,可以直接通过ffmpeg.FS函数公开基础的Emscripten文件系统API-您可以使用此界面导航文件夹,创建文件和其他各种文件系统操作。

如果您在Chrome开发工具中跨过以上一行,则会注意到它创建了许多Web Worker,每个Web Worker都加载ffmpeg.wasm:

这利用了Emscripten的Pthread支持。启用日志记录后,您可以在控制台中查看进度;

输出#0,mp4,到'output.mp4':元数据:编码器:Lavf58.45.100流#0:0:视频:h264(libx264)(avc1 / 0x31637661),yuv420p,256x240,q = -1--1, 35 fps,17920 tbn,35 tbc元数据:编码器:Lavc58.91.100 libx264辅助数据:cpb:最大比特率/最小/平均:0/0/0缓冲区大小:0 vbv_delay:N / Aframe = 47 fps = 0.0 q = 0.0大小= 0kB时间= 00:00:00.00比特率= N / A速度= 0xframe = 76 fps = 68 q = 30.0大小= 0kB时间= 00:00:00.65比特率= 0.6kbits / s速度= 0.589xframe = 102 fps = 62 q = 30.0大小= 0kB时间= 00:00:01.40比特率= 0.3kbits / s速度= 0.846x

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

const output = ffmpeg。 FS(“ readFile”,“ output.mp4”); const video = document。 getElementById(“ video”);视频 。 src = URL。 createObjectURL(新的Blob([output.buffer],{type:“ video / mp4”}));

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

对大文件进行代码转换可能需要一些时间。有趣的是,让我们看一下如何将文件转码为片段,并将其逐步添加到视频缓冲区中。

您可以使用Media Source Extension API(包括MediaSource和SourceBuffer对象)来构建流媒体播放。创建和加载缓冲区可能非常棘手,因为这两个对象都提供了生命周期事件,您必须处理这些事件才能在正确的时间添加新的缓冲区。为了管理这些事件的协调,我选择使用RxJS。

const bufferStream = filename => new Observable(异步订阅者=> {const ffmpeg = FFmpeg。createFFmpeg({corePath:“ thirdparty / ffmpeg-core.js”,log:false}); const fileExists =文件=> ffmpeg。FS( “(readdir”,“ /”)。包括(文件); const readFile =文件=> ffmpeg。FS(“ readFile”,file);等待ffmpeg。load(); const sourceBuffer =等待fetch(文件名)。然后(r => r。arrayBuffer()); ffmpeg。FS(“ writeFile”,“ input.mp4”,新的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“)。然后(()=> {//发送其余文件,而(fileExists(`$ {index} .mp4`)){ 。 next(readFile(`$ {index} .mp4`));索引++; }订户。完成(); }); setInterval(()=> {//定期检查是否已写入文件if(fileExists(`$ {index + 1} .mp4`)){Subscriber。next(readFile(`$ {index} .mp4`)) ; index ++;}},200); });

上面的代码使用与以前相同的FFmpeg.wasm设置,将要转码的文件写入内存文件系统。 ffmpeg.run具有与上一个示例不同的配置,以便创建具有适当代码转换器设置的分段输出。运行时,FFmpeg将具有增量索引(0.mp4,1.mp4等)的文件写入mem文件系统。

为了流式传输输出,间隔时间轮询文件系统以获取转码后的输出,并通过Subscriber.next将数据作为事件发出。最后,当ffmpeg.run完成时,将发射其余文件,并完成流(关闭)。

为了将数据流传输到视频元素,您需要创建一个MediaSource对象,并等待sourceopen事件触发。以下代码使用RxJS CombineLatest来确保在触发此事件之前不处理FFmpeg输出:

const mediaSource = new MediaSource();视频播放器 。 src = URL。 createObjectURL(mediaSource);视频播放器 。播放(); const mediaSourceOpen = fromEvent(mediaSource,“ sourceopen”); const bufferStreamReady = CombineLatest(mediaSourceOpen,bufferStream(“ 4club-JTV-i63.avi”))。管道(map(([[,a])=> a));

收到第一个视频片段/缓冲区时,我们需要在正确的时间将SourceBuffer添加到MediaSource并将原始缓冲区附加到SourceBuffer。此后,还有一个仔细的协调点,新缓冲区不能添加到SourceBuffer中,直到它发出updateend事件以指示先前的缓冲区已被处理。

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

const sourceBufferUpdateEnd = bufferStreamReady。 pipe(take(1),map(buffer => {//使用正确的mime类型创建缓冲区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) ; // //创建一个新的事件流fromEvent(sourceBuf,“ updateend”)。pipe(map(()=> sourceBuf));}),flatMap(value => value));

剩下的就是在缓冲区到达时以及SourceBuffer准备好时追加缓冲区。这可以使用RxJS zip函数来实现:

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

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

我是Scott Logic的技术总监,还是多领域的技术作家,博客作者和演讲者。

我的博客包含有关广泛主题的文章,包括WebAssembly,HTML5 / JavaScript和使用D3和d3fc的数据可视化。您还将找到大量有关以前的技术兴趣的帖子,包括iOS,Swift,WPF和Silverlight。

我是FINOS的董事会成员,FINOS鼓励金融领域的开源合作。我在GitHub上也非常活跃,为许多不同的项目做出了贡献。