经过前序章节的介绍,读者能大致了解如何在播放器中解析、创建视频;
本章介绍何在浏览器中合成视频,这是视频编辑中最基础的功能。
你可以跳过原理介绍,直接查看 WebAV 合成视频示例
在视频上叠加素材
常见的素材有:视频、音频、图片、文字
在浏览器中创建视频章节介绍了,视频编码器只接受 VideoFrame 对象,而 canvas 可以构造 VideoFrame;
在视频上叠加素材的实现原理:视频 + 素材 -> canvas -> VideoFrame -> VideoEncoder
- 先绘制视频到 canvas,再绘制其他素材
- 使用 canvas 元素构造 VideoFrame 对象
- 使用编码器编码 VideoFrame
- 处理下一帧
音频则是将各个素材的音频数据(如果有)相加即可,详情可查看上一章在浏览器中处理音频
视频是由一帧帧图像在时间轴上排列而成,原视频也视为一个普通素材;
所以问题可以简化为:决定某一时刻分别需要绘制哪些素材的第几帧,时间轴从 0 开始,重复以上步骤就能得到一个新视频。
# 实现步骤总结
- 将素材抽象为
Clip
接口,不同素材有不同实现,如MP4Clip
、ImgClip
- 创建一个
Conbinator
对象控制时间轴,向各个素材(Clip)发送时间信号,首次为 0 时间不断增加,增加的步长取决于最终需要合成视频 FPS,step = 1000 / FPS
ms - 素材由接收到的时间值,决定当前时刻需要提供的数据:自身的第几帧图像、音频片段(
Float32Array
) Conbinator
收集并合成各个素材的图像(绘制到 canvas)、音频(Float32Array
相加)数据Conbinator
将合成的数据转换成 VideoFrame、AudioData 传递给编码器,编码(压缩)后封装到对应格式的视频容器格式Conbinator
增加时间信号的值,重复步骤 2~5
# 素材抽象设计(Clip)
素材分为动态(视频、音频、动图)与静态(图片、文字)两种,静态素材不受时间影响比较简单,接下来以视频素材举例。
Clip
接口简化实现
export interface IClip {
tick: (time: number) => Promise<{
video?: VideoFrame | ImageBitmap
audio?: Float32Array[]
state: 'done' | 'success'
}>
ready: Promise<{ width: number; height: number; duration: number }>
}
MP4Clip 的实际源码有两百多行,限于篇幅,这里只介绍原理
- 使用 mp4box.js 解封装、WebCodecs 解码视频,得到 VideoFrame、AudioData
- 从 AudioData 提取 PCM 数据(Float32Array)
- MP4Clip 内部使用数组管理图像(VideoFrame)与音频数据(Float32Array)
- 当
Combinator
调用MP4Clip.tick
时,根据事件参数找到对应的图像帧与音频切片并返回
# Combinator 设计
提前介绍一下 OffscreenSprite
,将 Clip
使用 OffscreenSprite
包装起来,记录坐标、宽高、旋转等属性,用于控制素材在 canvas
的位置、实现动画等;在下一篇文章介绍,本章略过。
Combinator
的核心逻辑
class Combinator {
add (sprite: OffscreenSprite) {
}
output() {
let time = 0
while (true) {
let mixedAudio
for (const spr of this.sprites) {
const { video, audio, state } = spr.tick(time)
mixedAudio += audio
ctx.draw(video)
}
new VideoFrame(canvas)
new AudioData(mixedAudio)
time += 1000 / 30 * 1000
}
}
}
拼接视频
前后拼接视频有两种方式
- 重编码拼接,特征是输出视频速度慢、兼容性好
原理跟上一步合成视频是一样的,两个素材的结束、开始时间正好衔接,重新绘制 canvas 再编码 - 快速拼接(非重编码),特征是速度快、可能出现兼容性问题
原理是拆开视频容器,复制其中的编码数据到新的容器中,仅修改其时间偏移
这里讲解快速拼接的核心代码
autoReadStream(stream.pipeThrough(new SampleTransform()), {
onChunk: async ({ chunkType, data }) => {
const { id: curId, type, samples } = data
const trackId = type === 'video' ? vTrackId : aTrackId
samples.forEach(s => {
outfile.addSample(trackId, s.data, {
duration: s.duration,
dts: s.dts + offsetDTS,
cts: s.cts + offsetCTS,
is_sync: s.is_sync
})
})
}
})
WebAV 合成视频示例
DEMO 链接在附录,可在线立即体验
在视频上叠加图片
const resList = ['./public/video/webav1.mp4', './public/img/bunny.png']
const spr1 = new OffscreenSprite(
'spr1',
new MP4Clip((await fetch(resList[0])).body!)
)
const spr2 = new OffscreenSprite(
'spr2',
new ImgClip(
await createImageBitmap(await (await fetch(resList[1])).blob())
)
)
const com = new Combinator({
width: 1280,
height: 720,
bgColor: 'white'
})
await com.add(spr1, { main: true })
await com.add(spr2)
com.output()
快速合并 MP4 文件
const resList = ['./public/video/webav1.mp4', './public/video/webav2.mp4']
const stream = fastConcatMP4(
await Promise.all(resList.map(async url => (await fetch(url)).body!))
)
附录
- WebAV 基于 WebCodecs 构建的音视频处理 SDK
- WebAV 合成素材 DEMO
- WebAV 快速拼接 DEMO
- WebAV Combinator 源码
- WebAV 快速拼接 MP4 源码
本文使用 文章同步助手 同步