使用webCodec完成视频播放器(1)

概要

本篇主要完成视频解封装 -> 视频数据解码 -> 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解码视频流,主要就是通过webCodecEncodedVideoChunk解码为VideoFrame

关键代码如下:

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标签呢。这样大张旗鼓有什么好处嘛?

  1. 倍速播放 : 非常简单,只需要控制setTimeout的间隔时间就行。(原生video中也可以用playbackRate)
  2. 截图 :都拿到视频帧了,直接createObjectURL美滋滋。 (原生video中,可以用MediaStreamTrackProcessor,比较麻烦)
  3. 拖拽进度条: 虽然实现起来很麻烦,但是自由度和播放效果可能要比原生video好很多。
  4. 视频合成: 虽然原生video做不了,但是可以用透明gif图层等其他方式吧
  5. 解码能力webCodec正常情况应该是和原生video标签解码能力类似的.
  6. 直播: 这个应该是比较关键的点了,原生video是没法播放flv,hls之类的直播流的。

参考

  1. www.zhangxinxu.com/wordpress/2...
  2. zhuanlan.zhihu.com/p/654997285
  3. zhuanlan.zhihu.com/p/637337464
相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax