
本文档旨在解决在使用 WebCodecs VideoDecoder 进行视频解码时,实现精确逐帧回退的问题。通过比较帧的时间戳与目标帧的时间戳,可以避免渲染中间帧,从而提高用户体验。本文将提供详细的解决方案和示例代码,帮助开发者实现精确的视频帧控制。
在使用 WebCodecs VideoDecoder 创建自定义视频播放器时,实现逐帧控制是一项常见的需求。然而,由于 VideoDecoder 的工作方式,在进行后退操作时,需要解码自上一个关键帧到目标帧之间的所有帧。这会导致在目标帧显示之前,中间帧被渲染到画布上,从而产生视觉上的不流畅感。 本文将介绍如何避免渲染这些中间帧,只显示目标帧,从而实现更精确的逐帧回退。
解决方案
核心思想是在 displayFrame 函数中,比较当前帧的时间戳与目标帧的时间戳。只有当时间戳匹配时,才将帧绘制到画布上。这样可以确保只渲染目标帧,而忽略中间帧。
实现步骤
修改 displayFrame 函数
修改 displayFrame 函数,使其只在当前帧的时间戳与目标帧的时间戳匹配时才绘制帧。
function displayFrame(frame) { if(frame.timestamp == frames[currentFrame - 1].timestamp){ ctx.drawImage(frame, 0, 0); } frame.close();}
在这个修改后的 displayFrame 函数中,frame.timestamp 是当前解码帧的时间戳,frames[currentFrame – 1].timestamp 是目标帧的时间戳。只有当这两个值相等时,才会调用 ctx.drawImage(frame, 0, 0) 将帧绘制到画布上。frame.close() 确保释放帧的资源。
确保 currentFrame 的正确维护
currentFrame 变量需要正确地维护,以便在 displayFrame 函数中能够正确地访问目标帧的时间戳。在 prevFrame 函数中,需要在调用 displayFramesInRange 之后递减 currentFrame 的值。
async function prevFrame() { if (playing || currentFrame <= 1) return; // Find the previous keyframe. const keyFrameIndex = findPreviousKeyFrame(currentFrame - 1); // If no keyframe found, we can't go back. if (keyFrameIndex === -1) return; // Display frames from the previous keyframe up to the desired frame. await displayFramesInRange(keyFrameIndex, currentFrame - 1); currentFrame--;}
完整代码示例
下面是包含上述修改的完整代码示例:
Custom Video Player
const fileInput = document.getElementById('fileInput'); const playButton = document.getElementById('play'); const pauseButton = document.getElementById('pause'); const nextFrameButton = document.getElementById('nextFrame'); const prevFrameButton = document.getElementById('prevFrame'); const canvas = document.getElementById('videoCanvas'); const ctx = canvas.getContext('2d'); let mp4boxFile; let videoDecoder; let playing = false; let frameDuration = 1000 / 50; // 50 fps let currentFrame = 0; let frames = []; let shouldRenderFrame = true; function findPreviousKeyFrame(frameIndex) { for (let i = frameIndex - 1; i >= 0; i--) { if (frames[i].type === 'key') { return i; } } return -1; } async function displayFramesInRange(start, end) { shouldRenderFrame = false; for (let i = start; i < end; i++) { if (i == end - 1) { shouldRenderFrame = true; console.log("end"); } await videoDecoder.decode(frames[i]); } } function shouldRenderNextFrame() { return shouldRenderFrame; } async function prevFrame() { if (playing || currentFrame console.error(e), }); } function displayFrame(frame) { if(frame.timestamp == frames[currentFrame - 1].timestamp){ ctx.drawImage(frame, 0, 0); } frame.close(); } function playVideo() { if (playing) return; console.log('Playing video'); playing = true; (async () => { for (let i = currentFrame; i setTimeout(r, frameDuration)); } playing = false; })(); } function getDescription(trak) { for (const entry of trak.mdia.minf.stbl.stsd.entries) { if (entry.avcC || entry.hvcC) { const stream = new DataStream(undefined, 0, DataStream.BIG_ENDIAN); if (entry.avcC) { entry.avcC.write(stream); } else { entry.hvcC.write(stream); } return new Uint8Array(stream.buffer, 8); // Remove the box header. } } throw "avcC or hvcC not found"; } function pauseVideo() { playing = false; } function nextFrame() { if (playing || currentFrame >= frames.length) return; videoDecoder.decode(frames[currentFrame]); currentFrame++; } fileInput.addEventListener('change', () => { if (!fileInput.files[0]) return; const fileReader = new FileReader(); fileReader.onload = e => { mp4boxFile = MP4Box.createFile(); mp4boxFile.onReady = info => { const videoTrack = info.tracks.find(track => track.type === 'video'); const trak = mp4boxFile.getTrackById(videoTrack.id); videoDecoder.configure({ codec: videoTrack.codec, codedHeight: videoTrack.video.height, codedWidth: videoTrack.video.width, description: this.getDescription(trak) }); mp4boxFile.setExtractionOptions(videoTrack.id); mp4boxFile.start() mp4boxFile.onSamples = (id, user, samples) => { frames.push(...samples.map(sample => new EncodedVideoChunk({ type: sample.is_sync ? 'key' : 'delta', timestamp: sample.dts, data: sample.data.buffer, }))); }; mp4boxFile.flush(); }; e.target.result.fileStart = 0; mp4boxFile.appendBuffer(e.target.result); }; fileReader.readAsArrayBuffer(fileInput.files[0]); }); playButton.addEventListener('click', playVideo); pauseButton.addEventListener('click', pauseVideo); nextFrameButton.addEventListener('click', nextFrame); prevFrameButton.addEventListener('click', prevFrame); initVideoDecoder();
注意事项
时间戳的准确性: 确保视频帧的时间戳是准确的,并且与目标帧的时间戳进行比较。如果时间戳不准确,可能会导致无法正确渲染目标帧。性能考虑: 在 displayFrame 函数中进行时间戳比较可能会对性能产生一定的影响,特别是在处理高帧率视频时。需要根据实际情况进行优化。错误处理: 在 displayFrame 函数中,需要确保 frames[currentFrame – 1] 存在,以避免访问越界错误。可以添加额外的条件判断来处理这种情况。初始化 currentFrame: 确保 currentFrame 在初始状态下是正确的,例如,在视频加载完成后将其设置为 0 或 1。
总结
通过比较帧的时间戳与目标帧的时间戳,可以有效地避免在使用 WebCodecs VideoDecoder 进行视频解码时渲染中间帧的问题。这种方法可以提高用户体验,并实现更精确的逐帧控制。在实际应用中,需要注意时间戳的准确性、性能考虑和错误处理,以确保代码的稳定性和可靠性。
以上就是使用 WebCodecs VideoDecoder 实现精确逐帧回退的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1512749.html
微信扫一扫
支付宝扫一扫