大厂技术 坚持周更 精选好文
最近一直在开发 web视频剪辑工具(cutter),这个工具可以方便老师们编辑拍摄好的视频。
这是一个挺有意思的项目,预计分多章和大家分享介绍。
本期主要介绍下其大体流程,方便大家对其原理有一个简单认知
Cutter 剪辑工具效果演示:
web编辑器
导出效果
阅读完本文,预计可以收获的知识点:
技术链路
全链路可以简单分为
页面依赖的底层:vesdk
页面核心交互:web剪辑工具
后端:视频架构-视频合成
问题抛出
为了更好的理解全链路,这里我们抛出两个问题,带着问题来看整体链路,增强我们的理解:
Q1:视频是怎么在网页端实现编辑预览的效果?
Q2:怎么保证预览效果和合成效果一致性?
Q1:视频是怎么在网页端实现编辑预览的效果
目前web视频编辑主要有两个方向
二者的对比,可以参考如下:
图片来源 《VESDK技术演进之Web音视频编辑技术》
流程图
vesdk采用的是第二种方式(ffmpeg+wasm),大体流程转换图可参考如下:
调度逻辑:
解码、绘制时尽力出帧frame,将frame放入缓存池中,上屏时结合raf 自行根据fps计算下一帧渲染时间点
剪辑中 音频、文字 是怎么绘制的:
音频:在主线程中,基于Web Audio的OpenAL API来构建
文字、特效:基于webgl shader等直接绘制
为了更好的理解上面的流程图,介绍下里面的一些关键名词
YUV
YUV是一种颜色编码格式,能够通过公式计算还原为RGB,相比于RGB编码体积占用更小。
R = Y + 1.140*V
G = Y - 0.394U - 0.581V
B = Y + 2.032*U
“Y”表示明亮度(Luminance或Luma),也就是灰阶值
“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色
例如:通过YUV预览工具,我们可以看到各个分量单独显示时的成像
FFmpeg
在上述过程中,FFmpeg就是这样一款领先的多媒体框架,几乎实现了当下所有常见的数据封装格式、多媒体传输协议、音视频编解码器。
FFmpeg提供了两种调用姿势,可以面向不同场景需求:
// 例如:
ffmpeg -i tempalte.mp4 -pix_fmt yuv420p tempalte.yuv
可以看到解封装后,原文件体积是远大于1.1MB的
// 可以参考该文章Mp4读取yuv数据
https://www.jianshu.com/p/f4516e6df9f1
// 几个关键api
av_read_frame 读取视频流的h264帧数据
avcodec_send_packet 将h264帧数据发送给解码器
avcodec_receive_frame 从解码器中读出解码后的yuv数据
// demo
void decode() {
char *path = "/template.mp4";
...
while(true) {
av_read_frame(avformat_context, packet);//读取文件中的h264帧
if (packet->stream_index == videoStream) {
int ret = avcodec_send_packet(avcodec_context, packet);//将h264帧发送到解码器
if (ret < 0) {
break;
}
while (true) {
int ret = avcodec_receive_frame(avcodec_context, frame);//从解码器获取帧
sws_scale(sws_context,
(uint8_t const * const *) frame->data,
frame->linesize, 0, avcodec_context->height, pFrameYUV->data,
pFrameYUV->linesize);//将帧数据转为yuv420p
fwrite(pFrameYUV->data[0], sizeof( uint8_t ), avcodec_context->width * avcodec_context->height, pFile);//将y数据写入文件中
fwrite(pFrameYUV->data[1], sizeof( uint8_t ), avcodec_context->width * avcodec_context->height / 4, pFile);//将u数据写入文件中
fwrite(pFrameYUV->data[2], sizeof( uint8_t ), avcodec_context->width * avcodec_context->height / 4, pFile);//将v数据写入文件中
}
}
}
...
}
WASM
WebAssembly是一种安全、可移植、效率高、文件小的格式,其提供的命令行工具wasm可以将高级语言(如 C++)编写的代码转换为浏览器可理解的机器码,所以实现了在浏览器中直接运行。
例如c代码 经过如下步骤,可以被浏览器直接执行
加法 demo小例子:
step1、可利用在线工具(https://mbebenita.github.io/WasmExplorer/)编写一个加法例子,然后下载得到 编译好的 test.wasm 文件
step2、加载test.wasm
step3、浏览器中直接运行
OffscreenCanvas
OffscreenCanvas[5]非常有意思,这是一个离前端开发人员比较近的概念,它是一个可以脱离屏幕渲染的canvas对象,在主线程环境和web worker环境均有效。
OffscreenCanvas 一般搭配worker使用,目前主要用于两种不同的使用场景:
image.png
|
流程 |
优点 |
劣势 |
模式一:同步显示offscrrenCanvas中的帧 |
step1、在 Worker 线程创建一个 OffscreenCanvas 做后台渲染step2、再把渲染好的缓冲区 Transfer 回主线程显示 |
主线程可以直接控制渲染内容 |
canvas渲染受主线程影响 |
模式二:异步显示offscrrenCanvas中的帧 |
step1、将主线程中 Canvas 转换为 OffscreenCanvas,并发送给worker线程step2、worker线程获取到OffscreenCanvas后,进行绘制计算操作,最后把绘制结果直接 Commit 到浏览器的 Display Compositor (相当于在 Worker 线程直接更新 Canvas 元素的内容,不走常规的渲染流程)(参考表格下面的图) |
canvas渲染不受主线程影响- 避免绘制过程中的大量的计算阻塞主线程- 避免主线程的耗时任务阻塞渲染 |
主线程无法控制绘制内容 |
模式一:
// 主线程 进行渲染
const ctx = renderCanvas.getContext( '2d' );
const worker = new Worker( 'worker.js' );
worker.onmessage = function ( msg ) {
if (msg.data.method === 'transfer' ) {
ctx.drawImage(msg.data.buffer, 0 , 0 );
}
};
// worker线程
onmessage = async (event) => {
const offscreenCanvas = new OffscreenCanvas( 480 , 270 );
const ctx = offscreenCanvas.getContext( "2d" );
// ctx绘制工作
...
const imageBitmap = await self.createImageBitmap( new Blob([data.buffer]));
ctx.drawImage(imageBitmap, 0 , 0 );
let imageBitmap = offscreenCanvas.transferToImageBitmap();
// bitmap发送给主线程
postMessage({ method : "transfer" , buffer : imageBitmap}, [imageBitmap])
}
备注:postMessage 常规传递是通过拷贝的方式;对此postMessage提供了第二个参数,可以传入实现了Transferable[6]接口的数据(例如 ImageBitmap),这些数据的控制权会被转移到子线程,转移后主线程无法使用这些数据(会抛错)
备注:postMessage 常规传递是通过拷贝的方式;对此postMessage提供了第二个参数,可以传入实现了Transferable[7]接口的数据(例如 ImageBitmap),这些数据的控制权会被转移到子线程,转移后主线程无法使用这些数据(会抛错)
模式二:
// 主线程
const worker = new Worker('worker.js');
const offscreenCanvas = canvas.transferControlToOffscreen();
worker.postMessage({
canvas: offscreenCanvas,
}, [offscreenCanvas])
// worker
onmessage = async (event) => {
const canvas = event.data.canvas;
const ctx = canvas.getContext("2d");
// ctx绘制工作 ...
const imageBitmap = await self.createImageBitmap(new Blob([data.buffer]));
// 开始渲染
ctx.drawImage(imageBitmap, 0, 0);
}
一个对照试验:
对照 |
主线程解码+主线程渲染(参考动图 1) |
|
实验组 |
demo1:主线程解码 + 主线程渲染- 解码被卡住 ❌- 渲染被卡住 ❌(参考动图 2) |
demo2:work 线程 解码 + 主线程canvas渲染- 解码不被卡住 ✅- 渲染被卡住 ❌(参考动图 3) |
实验组 |
demo3:worker 线程 解码 + offscreenCanvas(同步模式)- 解码不被卡住 ✅- 渲染被卡住 ❌(参考动图 4) |
demo4:worker 线程 解码 + offscreenCanvas( 异步模式 )- 解码不被卡住 ✅- 渲染不被卡住 ✅(参考动图 5) |
动图 1
动图 2
动图 3
动图 4
动图 5
通过实验,我们可以发现:
解码任务放在worker线程,不会被主线程打断;渲染任务放在offscreenCanvas,不会被主线程打断
Q2:怎么保证预览效果和合成效果一致性?
这个问题比较容易理解,受限于浏览器自身的性能和限制,前端合成问题较多,稳定性和性能不足,所以是采用服务端合成的方式。
为了保证服务端导出和前端编辑预览一致,约定一个草稿协议,云端合成时基于草稿做类似前端合成操作即可
可以看到 ffmpeg + wasm + worker + offscreenCanvas搭配起来后,还是能做出一款性能不错的有意思的音视频小工具
我们进入一个小实战环节
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)