首页 > web前端 > js教程 > 正文

使用 WebCodecs VideoDecoder 实现精确逐帧回退

聖光之護
发布: 2025-07-30 17:42:01
原创
534人浏览过

使用 webcodecs videodecoder 实现精确逐帧回退

本文档旨在解决在使用 WebCodecs VideoDecoder 进行视频解码时,实现精确逐帧回退的问题。通过比较帧的时间戳与目标帧的时间戳,可以避免渲染中间帧,从而提高用户体验。本文将提供详细的解决方案和示例代码,帮助开发者实现精确的视频帧控制。

在使用 WebCodecs VideoDecoder 创建自定义视频播放器时,实现逐帧控制是一项常见的需求。然而,由于 VideoDecoder 的工作方式,在进行后退操作时,需要解码自上一个关键帧到目标帧之间的所有帧。这会导致在目标帧显示之前,中间帧被渲染到画布上,从而产生视觉上的不流畅感。 本文将介绍如何避免渲染这些中间帧,只显示目标帧,从而实现更精确的逐帧回退。

解决方案

核心思想是在 displayFrame 函数中,比较当前帧的时间戳与目标帧的时间戳。只有当时间戳匹配时,才将帧绘制到画布上。这样可以确保只渲染目标帧,而忽略中间帧。

实现步骤

  1. 修改 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() 确保释放帧的资源。

    AssemblyAI
    AssemblyAI

    转录和理解语音的AI模型

    AssemblyAI 65
    查看详情 AssemblyAI
  2. 确保 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--;
    }
    登录后复制

完整代码示例

下面是包含上述修改的完整代码示例:

<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Custom Video Player</title>
</head>

<body>
  <canvas id="videoCanvas" width="640" height="360"></canvas>
  <br>
  <input type="file" id="fileInput" accept="video/mp4">
  <button id="play">Play</button>
  <button id="pause">Pause</button>
  <button id="nextFrame">Next frame</button>
  <button id="prevFrame">Previous frame</button>

  <script src="mp4box.all.min.js"></script>
  <script>
    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 <= 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--;
    }

    async function initVideoDecoder() {
      videoDecoder = new VideoDecoder({
        output: displayFrame,
        error: e => 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 < frames.length && playing; i++) {
          await videoDecoder.decode(frames[i]);
          currentFrame = i + 1;
          await new Promise(r => 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();

  </script>
</body>

</html>
登录后复制

注意事项

  • 时间戳的准确性: 确保视频帧的时间戳是准确的,并且与目标帧的时间戳进行比较。如果时间戳不准确,可能会导致无法正确渲染目标帧。
  • 性能考虑: 在 displayFrame 函数中进行时间戳比较可能会对性能产生一定的影响,特别是在处理高帧率视频时。需要根据实际情况进行优化。
  • 错误处理: 在 displayFrame 函数中,需要确保 frames[currentFrame - 1] 存在,以避免访问越界错误。可以添加额外的条件判断来处理这种情况。
  • 初始化 currentFrame: 确保 currentFrame 在初始状态下是正确的,例如,在视频加载完成后将其设置为 0 或 1。

总结

通过比较帧的时间戳与目标帧的时间戳,可以有效地避免在使用 WebCodecs VideoDecoder 进行视频解码时渲染中间帧的问题。这种方法可以提高用户体验,并实现更精确的逐帧控制。在实际应用中,需要注意时间戳的准确性、性能考虑和错误处理,以确保代码的稳定性和可靠性。

以上就是使用 WebCodecs VideoDecoder 实现精确逐帧回退的详细内容,更多请关注php中文网其它相关文章!

相关标签:
最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号