Webcodecs音视频编解码与封装技术探索

探讨了在 Web 端处理音视频的重要性,介绍了 WebCodecs API 在音视频编解码和封装方面的作用和优势,以及演示了利用 MP4Box.js 进行视频解析的示例。本文由 @zhouzijian 分享,公号:大转转 FE。

背景

在 web 端处理音视频是一个复杂而又重要的课题,市场上主流的视频编辑通常采用服务端进行渲染导出,因为专用的服务器对音视频的编解码能力更强,所以服务端渲染导出的速度很不错。

少数编辑器在浏览器本地对视频进行处理,一方面对服务器成本非常友好,另一方面可以不需要注册等流程,在小型视频的渲染上用户体验更好。但是浏览器本地渲染对用户设备有一定要求,对浏览器的兼容性等等也有要求。

而经典的在浏览器本地处理视频的方案是通过 ffmpeg.wasm,近些年 Webcodecs API 的出现与普及逐渐改变了这一现象。

ffmpeg.wasm 的底层 webassembly 对 ffmpeg 多线程处理视频的兼容很差,GPU 调用效果也不尽人如意,导致渲染视频的速度非常不理想,并且还要额外下载编解码器,整体使用体验存在很多不适。

而 WebCodecs API 可以利用浏览器自带的 FFmpeg,而且可以充分利用 GPU,所以其执行效率是远高于 webassembly 的。

Webcodecs音视频编解码与封装技术探索

功能对比

特性 / 功能ffmpeg.wasmWebCodecs
目标在浏览器中实现音视频处理提供底层音视频操作 API
技术基础基于 WebAssembly 技术基于现代 Web 浏览器 API
跨平台兼容性跨平台兼容性较好跨平台兼容性一般
应用场景实时视频预览、截图、教育与教程、社交媒体、数据分析实时通信、视频编辑、自适应流媒体、机器学习
操作层级提供简单的 JavaScript API,方便集成到 Web 应用更深入地控制媒体流,实现高效、低延迟的实时通信或视频编辑
前端集成难度提供了简单的 JavaScript API,集成相对容易需要一定的音视频处理知识,集成难度可能稍高

WebCodecs 介绍

如果要问 WebCodecs 是什么,可以简单的概括为 JavaScript 赋予了通过浏览器底层对视频流的单个帧和音频数据块的底层访问能力的一项 web 技术。

简单地说,就是设置一个解码器,将视频编码字节块处理为视频帧 / 音频数据,或者反之,设置一个编码器,将视频帧 / 音频数据处理回编码字节块。

上文所说的 WebCodecs API 的解码器有:

名称介绍
AudioDecoder解码 EncodedAudioChunk 对象
VideoDecoder解码 EncodedVideoChunk 对象

上表中 EncodedAudioChunk 和 EncodedVideoChunk 就是上文提到的编码字节块。

上文所说的 WebCodecs API 的编码器有:

名称介绍
AudioEncoder编码 AudioData 对象
VideoEncoder编码 VideoFrame 对象

上表中 AudioData 和 VideoFrame 就是上文提到的视频帧 / 音频数据。

请注意,WebCodecs API 并不提供对某一视频类型具体的编解码器,解码视频时,你需要自行将这个视频转为 EncodedVideoChunk 和 EncodedAudioChunk,再交由 WebCodecs API 进行处理。渲染合成视频同理。

常见的方案有 Mp4Box.js。

WebCodecs 支持情况

WebCodecs 在 Chrome 94 上得到支持,下面是一个可供参考的浏览器支持表。

浏览器支持情况发布时间
Chrome94+2021-09-21
Edge94+2021-09-24
Firefox不支持
Opera80+2021-10-05
Safari16.4+2023-03-27
360 浏览器14+2022-11
QQ 浏览器12+大约 2023-09-05 之后
2345 浏览器12+未查到
Chrome Android94+2021-09-21
Firefox for Android不支持
Opera Android66+2021-12-15
Safari on iOS16.4+2023-03-27

可以看到,不少浏览器的在 23 年才提供支持。

可用如下代码进行判定浏览器是否支持:

 if('VideoEncoder' in window){
console.log("webcodecs is supported.")
}

视频播放原理

众所周知,视频由画面和音频构成。而画面由一帧一帧的图像组成,音频由一段一段的声波构成。按照某个频率不断地同步切换帧和声波,就可以实现视频的播放。

但是,视频并不会完整的将每一帧以图片的形式进行保存,而是通过一些复杂的结构,将视频的画面进行压缩,并将时长等元数据整合到一起,形成一个完整地视频文件。

下面介绍下视频文件的结构。

视频结构

HTML5 提供了 HTMLMediaElement,可以直接使用 HTML 标签播放视频音频,而对于 m3u8 或 Flash 时代留存的大量 Flv 视频,也有例如 FLV.js 等相应的库,使其可以被 HTMLMediaElement 播放。

这些高度封装的库也使得我们对视频文件的结构比较陌生,这里以最常见的 MP4 格式简单介绍一下。

视频的编码

视频编码是将原始视频数据转换为压缩格式的过程,以减小文件大小并提高传输效率。

编码的目的是为了压缩,不同的编码格式则对应不同的压缩算法。

MP4 文件常用的编码格式有 H.264(即 AVC)、H.265(HEVC)、VP8、VP9 等。

H.265 在市场上有很高的占有量,但因为其高昂的授权费用,免费的 AV1 编码正逐步被市场接纳。

视频的封装

视频编码后,将其和文件的元数据封装到容器格式中,以创建完整的视频文件。

压缩后的原始数据,需要有元数据的配合才能被解析播放;

常见的元数据包括:时间信息,编码格式,分辨率,作者,标题等等。

动态补偿与帧间压缩

对视频进行二次压缩,无需掌握具体算法。

动态补偿指的是,连续的两帧之间有相同的部分,只是位置发生了变化,所以第二帧可以只储存偏移量

帧间压缩是对两帧之间进行 diff,第二帧只储存 diff 运算出的不同的那一部分

帧的类型

根据上面的过程,帧之间相互可能并不独立,于是产生了三种帧类型

I 帧:也就是关键帧,保留完整的画面信息,没有被二次压缩,可以被独立还原为图像

P 帧:依赖前一帧的解码结果才能还原为图像

B 帧:依赖前一帧与后一帧的解码结果才能还原为图像,但占用空间一般最少

Demo

前面介绍了非常多的 Webcodecs 和视频相关的概念,我们来做一个小的 demo,利用 MP4Box.js 作为编解码器,尝试解析一个视频。

先放一个 Demo 地址:https://codesandbox.io/p/devbox/nifty-dawn-gghryr?embed=1&file=/index.js:111,1

解析部分

我们先创建一个 Mp4box 实例:

 const mp4box = MP4Box.createFile();

Webcodecs 基于 Stream 的思想,所以我们需要用 Stream 去提供数据。比较简单的方法是用 fetch 去请求:

 fetch(mp4url)
.then((res) => res.arrayBuffer())
.then((buffer) => {
state.innerHTML = "开始解码视频";
buffer.fileStart = 0;
mp4box.appendBuffer(buffer);
mp4box.flush();
});

请注意,mp4box.appendBuffer 接受 ArrayBuffer 类型的数据。

加载小型视频时,可以直接用上面的代码。但若是视频较大,上面的代码效率就不太够看。可以用 reader.read ().then (({ done, value}) 替代,但是要注意,这样获取的 data 是 Unit8Array 类型,需要手动转为 ArrayBuffer,并且要修改 buffer.fileStart 为这一段 data 的起点。

然后,我们对 mp4box 进行监听,当文件开始解码会首先触发 onMoovStart(Demo 中未用到),这里的 Moov 可能不好理解,他指的是 “Movie Box”,也被称为 “moov atom”,包含了视频文件的关键信息,如视频和音频的媒体数据、时长、轨道信息等。

当 moov 解析完成,会触发 onReady,onReady 会将视频的详细信息也就是 moov 传给回调函数的第一个参数。详细的数据结构可以参考 Mp4Box.js 官方文档:地址

我们姑且叫这个信息为 info,这里面我们在意的参数是轨道 info.videoTracks。他是一个数组,包含了这个轨道的采样率、编码方法等等信息,一般长度是 2,第 0 个是视频轨道,第 1 个是音频轨道。(不过例如专业电影等更复杂的视频可能会有更多轨道,这里不做考虑)

我们将轨道拉出来,扔到下面的萃取环节中。

萃取部分

这是一个非常形象的名称,在官方文档里叫做 Extraction,它用来提取轨道并进行采样。

我们在 onReady 过程中,设置了 Extraction 的参数:

 mp4box.setExtractionOptions(videoTrack.id, "video", {
nbSamples: 100,
});

第二个参数指的是 user,指的是此轨道的分段调用方,将会被传到后面介绍的 onSamples 中,可以是任意字符串,表示唯一标识

第三个参数中 nbSamples 表示每次回调调用的样本数。如果收到的数据不足以提取样本数量,则保留迄今为止收到的样本。如果未提供,则默认值为 1000。越大获取的帧数越多。

当一组样本准备就绪时,将根据 setExtractionOptions 中传递的选项,启动 onSamples 的回调函数。

 mp4box.onSamples = function (trackId, ref, samples) {
//......
}

onSamples 会给回掉传入三个参数:trackId, ref, samples 分别代表轨道 id,user,上一步采样的样品数组。

通过遍历这个数组,将样品编码成 EncodedVideoChunk 数据:

 for (const sample of samples) {
const type = sample.is_sync ? "key" : "delta";
const chunk = new EncodedVideoChunk({
type,
timestamp: sample.cts,
duration: sample.duration,
data: sample.data,
});
videoDecoder.decode(chunk);
}

其中 sample.is_sync 为 true,则为关键字。然后将 EncodedVideoChunk 送入 videoDecoder.decode 进行解码,从而获取帧数据。

videoDecoder 是在 onReady 中创建的:

 videoDecoder = new VideoDecoder({
output: (videoFrame) => {
createImageBitmap(videoFrame).then((img) => {
videoFrames.push({
img,
duration: videoFrame.duration,
timestamp: videoFrame.timestamp,
});
state.innerHTML = "已获取帧数:" + videoFrames.length;
videoFrame.close();
});
},
error: (err) => {
console.error("videoDecoder错误:", err);
},
});

其本质还是使用 Webcodecs API 的 VideoDecoder,在接受 onSamples 送来的数据后,解码为 videoFrame 数据。此时的操作可根据业务来,Demo 中将他送到 createImageBitmap 转为位图,然后推入 videoFrames 中。

在 Demo 的控制台中打印 videoFrames,即可直接看到帧的数组。

Webcodecs音视频编解码与封装技术探索

我们可以在页面上再创建一个 canvas,然后 ctx.drawImage (videoFrames [0].img,0,0) 即可将任意一帧绘制到画面上。(Demo 里没有加 canvas,大家可以在控制台自己加)

Webcodecs音视频编解码与封装技术探索

音频

音频的操作与视频类似,onReady 中的 info 也有 audioTracks 属性,从里面取出来并配置 Extraction:

 if (audioTrack) {
mp4box.setExtractionOptions(audioTrack.id, 'audio', {
nbSamples: 100000
})
}

配置音频解码器 AudioDecoder:

 audioDecoder = new AudioDecoder({
output: (audioFrame) => {
console.log('audioFrame:', audioFrame);
},
error: (err) => {
console.error('audioDecoder错误:', err);
}
})
const config = {
codec: audioTrack.codec,
sampleRate: audioTrack.audio.sample_rate,
numberOfChannels: audioTrack.audio.channel_count,
}

audioDecoder.configure(config);

其他操作不再赘述,可以在 Demo 的 AudioTest.html 中查看。

可以发现,最后获取到的数据与上文,视频帧的结构非常类似,是 AudioData 数据结构,可以将他转换为 Float32Array,就可以进行对音频的各种操作了。

视频的单位是帧,音频的单位可以说是波,任意一个波可以用若干个三角函数 sin、cos 之和表示。

根据高中物理,波可以相加。我们可以将两个 Float32Array 每一项相加,实现两个声波的混流:

 function mixAudioBuffers(buffer1, buffer2) {
if (buffer1.length !== buffer2.length) {
return
}
const mixedBuffer = new Float32Array(buffer1.length);

for (let i = 0; i < buffer1.length; i++) {
const mixedSample = buffer1[i] + buffer2[i];
mixedBuffer[i] = Math.min(1, Math.max(-1, mixedSample));
}
return mixedBuffer;
}

上述代码进行了归一化,避免求和的值超过 1,而这里的波的振幅范围是 [-1,1]。更常见的做法是提前对波进行缩放。

此外,我们可以通过改变波的振幅来修改音量,只需要把 Float32Array 的每一项 * 2 即可放大两倍音量:

 function increaseVolume(audioBuffer, volumeFactor) {
const adjustedBuffer = new Float32Array(audioBuffer.length);
for (let i = 0; i < audioBuffer.length; i++) {
const adjustedSample = audioBuffer[i] * volumeFactor;
adjustedBuffer[i] = Math.min(1, Math.max(-1, adjustedSample));
}
return adjustedBuffer;
}

可以参考:AudioData 文档,摸索更多有意思的操作。

可能的应用场景

  • 在上传视频的场景截取视频封面
  • 轻量级视频剪辑
  • 封装不易被爬虫的视频播放器

部分参考资料

  • https://www.bilibili.com/read/cv30358687/
  • https://www.zhangxinxu.com/study/202311/js-mp4-parse-effect-pixi-demo.php
  • https://developer.mozilla.org/en-US/docs/Web/API/AudioData
  • https://zhuanlan.zhihu.com/p/648657440

关于本文
作者:@zhouzijian
原文:https://mp.weixin.qq.com/s/RiUz-l3Hzn0Xrq4vrEWgNQ

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论