Video视频抽帧和WebCodecs API视频抽帧介绍

目录

mp4Box抽帧

ffmpeg抽帧

video元素抽帧

[WebCodecs 核心API](#WebCodecs 核心API)


视频文件是一个容器,里面有很多不同的轨道信息。如:图像、声音、字幕等。而视频图像信息又是由一系列图片序列帧的集合。如10秒时长的视频,假设每秒30帧。那大概有300条图像数据。

怎么得到图像数据,通常有几种方法:

  1. 用video元素指向视频文件,通过将它绘制到canvas上面获取图像数据,如果想要更高级的操作,可以调用video.captureStream()获取媒体流对象。它可以通过getTracks()获取上面所说的视频的轨道信息.有视频和音频数据。利用MediaStreamTrackProcessor对象。可以读取Track解码后的Frame(VideoFrame和AudioFrame)数据.但是这有一个问题就是它不能一次性得到整个视频帧的数据,需要通过播放或seek到某个时间去播放获取对应时间的帧

  2. 直接解封装视频文件的ArrayBuffer数据得到完整帧,这样可以自己再通过timestamp属性去获取自己想要某个时间段的帧,速度是非常快的,不过解封装通常非常的复杂。你需要在这领域对视频各种格式有非常深的了解。一般在我们可以使用现有的库去处理。如用ffmpeg或mp4box.前端使用ffmpeg-wasm版本的话,性能并不好。但它功能很全面,可以应对几乎所有格式,并且它还提供格式转换视频滤波裁剪很多功能。mp4box只能处理mp4格式,用它只解封装,它性能非常快,而且利用它来进行抽帧。我们可以不依赖video元素,这样我们可以将buffer放在worker pool线程池去进行并行一次处理多个视频素材

像下面通过mp4box可以一次性得到整个videoTrack的samples数据,再转换为EncoderVideoChunk,通过VideoDecoder解码,能够非常快抽帧。比使用video元素快很多倍

非完整代码:

mp4Box抽帧

javascript 复制代码
let currentTime=startTime; 
const videoDecoder = new VideoDecoder({
                            output: (videoFrame) => {
                                const timestamp = videoFrame.timestamp / 1e6
                                
                                if (timestamp >= currentTime&&timestamp<=endTime) {
            
                                    ctx.drawImage(videoFrame, 0, 0, canvas.width, canvas.height)
                                    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
                                    frames.push({ imageData: imageData })
                                    currentTime += perFrameTime
                                }
                                videoFrame.close()

                            },
                            error: (e) => {
                                console.log('解编码错误:', e)
                            }
                        })
                        videoDecoder.configure({
                            codec: videoTrack.codec,
                            codedWidth: videoTrack.video.width,
                            codedHeight: videoTrack.video.height,
                            description: videoDesc,
                            // optimizeForLatency: false
                        })
mp4boxfile.onSamples = async (id, user, samples) => {
                     
                            for (let i = 0, len = samples.length; i < len; i++) {
                                const s = samples[i]
                                const timestamp = s.cts / s.timescale
                                const duration = s.duration / s.timescale
                                const type = (s.is_sync ? 'key' : 'delta')
                                const videoChunkData = s.data

                                const videoChunk = new EncodedVideoChunk({
                                    type: type,
                                    timestamp: timestamp * 1e6,
                                    duration: duration * 1e6,
                                    data: videoChunkData,
                              
                                })
                                videoDecoder.decode(videoChunk)
            }
            // 逻辑判断,当前所有处理完
            if(complete){
             await videoDecoder.flush() // 启动解码
            }        
}
mp4boxfile.appendBuffer(videoBuffer)
mp4boxfile.flush();

ffmpeg抽帧

前端版ffmpeg-wasm抽帧比较特殊,需要执行命令转换你想要的格式,因为它是一类似终端执行命令去解析文件写入到它的内存虚拟文件系统中。(你可以用FileSystemAPI 获取物理磁盘权限,真正写入磁盘中,这样可以做到减少内存空间)。所以你要先写入对应的文件。再读取对应文件的arraybuffer数据。如下面,我可以得到图像的yuv420的格式的图像数据

javascript 复制代码
async fileToImageData(path) {
        const buffer = await this.ffmpeg.readFile(path)
        return buffer

    }
    async fileToImageDataList(basename, imageList) {
        return Promise.all(imageList.filter(d => !d.isDir).map(d => this.fileToImageData(basename + '/' + d.name)))
    }
    // 抽帧
    async extractVideoFrames(inputPath, startTime, endTime) {
        this.clear() 
        let fileName = getFileName(inputPath)
        let fileFrameDir = `${fileName}/images`
        await this.createDir(fileFrameDir)
        let images = await this.readDir(fileFrameDir)
        if (!images.length) {
            this.input(inputPath)
            this.everyFrame('1')// 每秒一帧
            this.size('100*100').aspect('1:1').pixelFormat('yuv420p')
            if (startTime) {
                this.toSS(startTime)
            }
            if (endTime) {
                this.to(endTime)
            }
            this.outFile(fileFrameDir + '/frame%03d.bmp')
            await this.run()
            images = await this.readDir(fileFrameDir)
        }
        return await this.fileToImageDataList(fileFrameDir, images)
    }

video元素抽帧

javascript 复制代码
video.ontimeupdate=async ()=>{
                            if(video.currentTime>endTime||currentTime>endTime||video.ended){
                                console.log('完成')
                                resolve(frames)
                                return
                            }
                            if(video.currentTime>=currentTime){
                                currentTime+=perFrameTime;
                                ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
                                const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height, imageDataSettings)
                                frames.push({
                                    imageData: imageData
                                })
                                video.currentTime=currentTime
                               
                            }
                        }
                        video.play()

WebCodecs 核心API

几个核心对象

VideoFrame可以直接绘制到Canvas上面,进行与其它图层合成,也可以再通过canvas获取ImageDdata转换为ArrayBuffer。VideoFrame.copyTo(buffer) 也可以将ArrayBuffer数据拷贝给目标.但一般videoFrame的图像数据是yuv格式。

  • 图像编码器: VideoEncoder(opens new window)
    可以自定义new VideoFrame(canvas),videoEncoder.encode(videoFrame).将图像数据编码为EncodedVideoChunk 数据,最终封装导出一个处理后的视频文件.(就像你在剪映,把源视频编辑后,导出新的视频)
  • 压缩图像数据: EncodedVideoChunk(opens new window)
    视频的图像压缩数据,通常要使用VideoDecoder对象解压缩成VideoFrame
  • 图像解码器: VideoDecoder
    和VideoEncoder的作用,正好相后。它是解编码的

看上面的流程:假设视频源是一个直播摄像头,它采集了数据、我们需要编码,封装压缩(这样会减少带宽传输和传输速度).数据来到了后台管理,然后解封装,解码,中间可以为视频添加场景或特效。再通类似WEBRTC实时传输协议给客户端去播放.

相关推荐
辻戋19 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保19 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun20 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp20 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.21 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
电鱼智能的电小鱼1 天前
基于电鱼 AI 工控机的智慧工地视频智能分析方案——边缘端AI检测,实现无人值守下的实时安全预警
网络·人工智能·嵌入式硬件·算法·安全·音视频
TeleostNaCl1 天前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫1 天前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友1 天前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
音视频牛哥1 天前
从协议规范和使用场景探讨为什么SmartMediaKit没有支持DASH
人工智能·音视频·大牛直播sdk·dash·dash还是rtmp·dash还是rtsp·dash还是hls