目录
mp4Box抽帧
ffmpeg抽帧
video元素抽帧
WebCodecs 核心API
视频文件是一个容器,里面有很多不同的轨道信息。如:图像、声音、字幕等。而视频图像信息又是由一系列图片序列帧的集合。如10秒时长的视频,假设每秒30帧。那大概有300条图像数据。
怎么得到图像数据,通常有几种方法:
- 用video元素指向视频文件,通过将它绘制到canvas上面获取图像数据,如果想要更高级的操作,可以调用video.captureStream()获取媒体流对象。它可以通过getTracks()获取上面所说的视频的轨道信息.有视频和音频数据。利用MediaStreamTrackProcessor对象。可以读取Track解码后的Frame(VideoFrame和AudioFrame)数据.但是这有一个问题就是它不能一次性得到整个视频帧的数据,需要通过播放或seek到某个时间去播放获取对应时间的帧
- 直接解封装视频文件的ArrayBuffer数据得到完整帧,这样可以自己再通过timestamp属性去获取自己想要某个时间段的帧,速度是非常快的,不过解封装通常非常的复杂。你需要在这领域对视频各种格式有非常深的了解。一般在我们可以使用现有的库去处理。如用ffmpeg或mp4box.前端使用ffmpeg-wasm版本的话,性能并不好。但它功能很全面,可以应对几乎所有格式,并且它还提供格式转换视频滤波裁剪很多功能。mp4box只能处理mp4格式,用它只解封装,它性能非常快,而且利用它来进行抽帧。我们可以不依赖video元素,这样我们可以将buffer放在worker pool线程池去进行并行一次处理多个视频素材
像下面通过mp4box可以一次性得到整个videoTrack的samples数据,再转换为EncoderVideoChunk,通过VideoDecoder解码,能够非常快抽帧。比使用video元素快很多倍
非完整代码:
mp4Box抽帧
let currentTime=startTime;
const videoDecoder = new VideoDecoder({output: (videoFrame) => {const timestamp = videoFrame.timestamp / 1e6if (timestamp >= currentTime&×tamp<=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.timescaleconst duration = s.duration / s.timescaleconst type = (s.is_sync ? 'key' : 'delta')const videoChunkData = s.dataconst 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的格式的图像数据
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元素抽帧
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(opens new window)
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实时传输协议给客户端去播放.