概要
本篇主要完成视频解封装 -> 视频数据解码 -> canvas播放流程。
整个流程大体如下:
流程及相关代码介绍
mp4解封装
解封装主要就是将视频容器中对应的视频轨道,音频轨道分离开来,之后在用对应的处理器处理。
这边使用的是专门处理mp4视频的工具: mp4Box.js
代码如下:
js
const videoUrl = "video/h264_1.mp4";
function queryVideo() {
// fetch到的是mp4刻度流,包含视频,音频等多个track
fetch(videoUrl).then((res: Response) => res.arrayBuffer()).then((buffer: BoxArrayBuffer) => {
buffer.fileStart = 0;
mp4boxfile.appendBuffer(buffer);
mp4boxfile.flush();
})
}
function initMP4Box() {
mp4boxfile = MP4Box.createFile();
mp4boxfile.onMoovStart = function() {
console.log("Moov box start");
};
mp4boxfile.onReady = (info) => {
videoTrack = info.videoTracks?.[0];
if(!videoTrack) {
console.warn("获取视频轨道失败");
return
}
mp4boxfile.setExtractionOptions(videoTrack.id, "video", {
// 每次onSamples函数接收到的samples个数
nbSamples: 100
});
mp4boxfile.start();
}
mp4boxfile.onSamples = (trackId, ref, samples) => {
if (videoTrack.id === trackId) {
sampleQueueNum += samples.length;
if(sampleQueueNum >= maxSampleNum) {
mp4boxfile.stop();
}
countSample += samples.length;
for (const sample of samples) {
// 是否为关键帧
const type = sample.is_sync ? 'key' : 'delta';
const chunk = new (window as any).EncodedVideoChunk({
type,
timestamp: sample.cts,
duration: sample.duration,
data: sample.data
});
videoDecoder.decode(chunk);
}
if (countSample === videoTrack.nb_samples) {
videoDecoder.flush();
}
}
}
}
完整的api可以浏览mp4Box
的官网,这里简单介绍下:
appendBuffer
:将视频buffer传入Mp4Box
对象,可以多次导入(适合视频很大的时候分段加载解析)onReady
回调函数: 视频头中的数据被解析完之后触发。这时候可以得到视频的codec,width,height,track等视频的关键描述信息setExtractionOptions
: 开始解析对应的轨道数据onSamples
回调函数: 解析等到数据后,进入该回调。这时候就可以拿到sample数据了。这里每个sample当一帧去处理的。
上述步骤便完成了fetch到的视频流
到 EncodedVideoChunk
的处理
视频数据解码
使用webCodec
解码视频流,主要就是通过webCodec
将EncodedVideoChunk
解码为VideoFrame
。
-
EncodedVideoChunk
代表编码后的视频数据流。developer.mozilla.org/en-US/docs/... -
VideoFrame
之前我们已经研究过了(VideoFrame学习篇),就是浏览器面向webCodec
的api,表示一帧数据。
关键代码如下:
js
function initVideoDecode(track: any = videoTrack) {
videoDecoder = new (window as any).VideoDecoder({
output: (videoFrame) => {
createImageBitmap(videoFrame).then((img) => {
videoFrames.push({
img,
duration: videoFrame.duration,
timestamp: videoFrame.timestamp
});
videoFrame.close();
if(!isPlay) { isPlay = true; draw(); }
});
console.log(sampleQueueNum)
sampleQueueNum --;
if(sampleQueueNum < maxSampleNum) {
mp4boxfile.start();
}
},
error: (err) => {
console.error('videoDecoder错误:', err);
}
});
videoDecoder.configure({
codec: track.codec,
codeWidth: track.track_width,
codeHeight: track.track_height,
description: getExtradata()
})
}
// 这个是额外的处理方法,不需要关心里面的细节
function getExtradata() {
const { DataStream } = MP4Box;
const validEntryList = mp4boxfile.moov.traks.filter(item => {
const entry = item.mdia.minf.stbl.stsd.entries[0];
return entry.avcC ?? entry.hvcC ?? entry.vpcC;
})
// 生成VideoDecoder.configure需要的description信息
const entry = validEntryList[0].mdia.minf.stbl.stsd.entries[0];
const box = entry.avcC ?? entry.hvcC ?? entry.vpcC;
if (box != null) {
const stream = new DataStream(
undefined,
0,
DataStream.BIG_ENDIAN
)
box.write(stream)
// slice()方法的作用是移除moov box的header信息
return new Uint8Array(stream.buffer.slice(8))
}
};
其实就是维护一个解码队列,使得VideoDecoder
解码和onSamples
分析视频获取sample同时进行。
这个方法完成后,我们就会得到一个VideoFrame
数组。
canvas播放
关键代码如下:
js
let index = 0;
function draw() {
const { img, timestamp, duration } = videoFrames[index];
const canvasDom = document.getElementById("canvas") as HTMLCanvasElement;
const context = canvasDom.getContext("2d");
// 清除画布
context.clearRect(0, 0, canvasDom.width, canvasDom.height);
context.drawImage(img, 0, 0, canvasDom.width, canvasDom.height);
// 开始下一帧绘制
index++;
if (index === videoFrames.length) {
// 重新开始
index = 0;
}
setTimeout(draw, duration);
}
这里其实就是很简单的canvas动画绘制的逻辑。用我们上面得到的VideoFrame
数组,做一个循环绘制就行了。
需要注意的是VideoFrame.duration
参数,api上说表示这一帧展示的毫秒时间,但是发现不同的视频,duration代表的时间长短不一致,需要用timeout
辅助处理。
最终效果
最终便可完成视频在画布上的渲染:
思考
这个效果还不如直接用浏览器的video标签呢。这样大张旗鼓有什么好处嘛?
- 倍速播放 : 非常简单,只需要控制
setTimeout
的间隔时间就行。(原生video中也可以用playbackRate) - 截图 :都拿到视频帧了,直接
createObjectURL
美滋滋。 (原生video中,可以用MediaStreamTrackProcessor
,比较麻烦) - 拖拽进度条: 虽然实现起来很麻烦,但是自由度和播放效果可能要比原生video好很多。
- 视频合成: 虽然原生video做不了,但是可以用透明gif图层等其他方式吧
- 解码能力 :
webCodec
正常情况应该是和原生video标签
解码能力类似的. - 直播: 这个应该是比较关键的点了,原生video是没法播放flv,hls之类的直播流的。