使用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
相关推荐
cwj&xyp2 小时前
Python(二)str、list、tuple、dict、set
前端·python·算法
dlnu20152506222 小时前
ssr实现方案
前端·javascript·ssr
古木20192 小时前
前端面试宝典
前端·面试·职场和发展
轻口味3 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王4 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发4 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀4 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪5 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef6 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端