快速知道 canvas 来进行微信网页视频无限循环播放的思路

背景

在个人开发中,想在微信 h5 里面做一个大画幅的背景视频,起初直接通过 video 标签引入,然后无限播放。

但是这里有个问题,那就是很不连贯,也就是说在结束之后会有刹那的停顿。

就在想,我要让它能够播放到最后,在从最后往前走的方式来走。

思路

实现视频 "正放→倒放→正放→倒放..." 无限循环播放的核心思路,是通过视频帧捕捉 + Canvas 绘制 + 定时器控制,结合 "正放时记录帧、结束后倒放帧、倒放结束后重新正放" 的逻辑闭环实现的。(老实脸,该代码由 ai 生成,谢谢惠顾)

过程

请打开 ai 软件,然后输入以下提示词:

"我现在想在微信网页端去实现视频"正放→倒放→正放→倒放..."无限循环播放的效果,你帮我基于 Taro + vue3 setup(可替换任意技术栈) 来实现,谢谢"

如果想要深入了解,请输入以下提示词: "帮我解释一下这个实现思路。"

这里并不是我懒,而是我各人期望大家能多用 ai 去解决问题,事实上,自从 ai 起来了以后,我已经很久没有用过搜索引擎了。。。。。

但是 ai 却也不是万能的,对于真实现实世界,它的触达率还是欠缺的(当然或许跟我没有氪金有关),总之我还是遇到了一个问题

遇到的问题

为什么微信h5打开直接就是黑屏了?

在我第五次在普通浏览器打开,发现视频的"正放→倒放→正放→倒放..."无限循环如期发生的时候,我陷入了迷茫。

我列出来如下的可能性去排查:

  1. 在微信网页里面的视频有问题,无法读取?

    并不是,我直接在微信点击了视频链接,可以访问

  2. 机制不对?初始化的时间不对?

    在我延时 2 秒后执行,奇迹发生了,逻辑正常了。

所以关键来了,需要在视频准备好的时候去播放,在于要在 video 的 @loadedmetadata,也就是视频元数据加载完成的时机,去触发才对。

代码示例

⚠️注意:下面的代码是基于 vue3 + taro 实现的,跟纯 h5 存在差异

vue 复制代码
<template>
  <view class="video-wrapper" :data-bg-video-src="bgVideoSrc">
    <button class="play-btn" @tap="handlePlayBtnClick">触发播放</button>
    <video
      :id="videoId"
      :src="bgVideoSrc"
      class="hidden-video"
      :muted="true"
      :controls="false"
      autoplay
      :loop="false"
      webkit-playsinline
      crossorigin="anonymous"
      playsinline
      @loadedmetadata="onVideoReady"
      @ended="onVideoEnded"
      @error="onVideoError"
    />

    <canvas :id="canvasId" class="fullscreen-canvas"></canvas>
  </view>
</template>

<script setup>
import { onMounted, ref, nextTick, onUnmounted } from "vue";
import bgVideoSrc from "@/assets/images/annual-report-2025/index-bg-078.mp4";

// 生成唯一ID
const generateUniqueId = () => {
  return `bg-video-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
};

const videoId = generateUniqueId();
const canvasId = generateUniqueId();

const videoRef = ref(null);
const canvasRef = ref(null);
const ctxRef = ref(null);

// 视频相关状态
const frameList = ref([]);
const fps = 30;
let captureInterval = null;
let reverseTimer = null;
let direction = "forward"; // forward 正放, reverse 倒放

// 初始化视频播放
const initVideoPlayback = async () => {
  try {
    frameList.value = [];
    await nextTick();

        // 获取video和canvas元素
    videoRef.value = document.getElementById(videoId)?.querySelector("video");
    canvasRef.value = document
      .getElementById(canvasId)
      ?.querySelector("canvas");

    if (!videoRef.value || typeof videoRef.value.play !== "function") {
      console.error("video 引用无效或不支持 play:", videoRef.value);
      return;
    }
    if (!canvasRef.value) {
      console.error("canvas 引用无效:", canvasRef.value);
      return;
    }

    ctxRef.value = canvasRef.value.getContext("2d");

    videoRef.value.currentTime = 0;
    direction = "forward";
    videoRef.value.play();
    startCaptureFrames();
  } catch (error) {
    console.error("初始化视频播放失败:", error);
  }
};

// 开始捕捉帧
const startCaptureFrames = () => {
  if (!videoRef.value || !canvasRef.value || !ctxRef.value) return;

  captureInterval = setInterval(() => {
    try {
      const canvas = canvasRef.value;
      const ctx = ctxRef.value;
      const video = videoRef.value;

      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
      const frame = ctx.getImageData(0, 0, canvas.width, canvas.height);
      frameList.value.push(frame);
    } catch (error) {
      console.warn("无法捕捉帧:", error);
    }
  }, 1000 / fps);
};

// 停止捕捉帧
const stopCaptureFrames = () => {
  if (captureInterval) {
    clearInterval(captureInterval);
    captureInterval = null;
  }
};

// 播放倒放帧
const playReverseFrames = () => {
  if (!ctxRef.value || frameList.value.length === 0) return;

  let frameIndex = frameList.value.length - 1;
  const ctx = ctxRef.value;

  reverseTimer = setInterval(() => {
    if (frameIndex < 0) {
      clearInterval(reverseTimer);
      reverseTimer = null;
      // 倒放结束 → 再次正放
      direction = "forward";
      videoRef.value.currentTime = 0;
      frameList.value = [];
      videoRef.value.play();
      startCaptureFrames();
      return;
    }

    try {
      ctx.putImageData(frameList.value[frameIndex], 0, 0);
      frameIndex--;
    } catch (error) {
      console.warn("播放倒放帧失败:", error);
    }
  }, 1000 / fps);
};

// 视频结束时触发
function onVideoEnded() {
  stopCaptureFrames();

  if (direction === "forward") {
    // 正放结束 → 倒放
    direction = "reverse";
    playReverseFrames();
  } else {
    // 倒放结束(理论上不会到这里,因为 playReverseFrames 内部会切回正放)
    direction = "forward";
    videoRef.value.currentTime = 0;
    videoRef.value.play();
    startCaptureFrames();
  }
}

function handlePlayBtnClick() {
  initVideoPlayback();
}

function onVideoReady() {
  console.log('视频加载完成');
  initVideoPlayback();
}

function onVideoError(error) {
  console.error("视频加载错误:", error);
}

// 清理资源
const cleanup = () => {
  stopCaptureFrames();
  if (reverseTimer) {
    clearInterval(reverseTimer);
    reverseTimer = null;
  }
  frameList.value = [];
  videoRef.value = null;
  canvasRef.value = null;
  ctxRef.value = null;
};

onUnmounted(() => {
  cleanup();
});
</script>


<style  lang="less" scoped>
.de-bug{
  .play-btn{
    position: absolute;
    top: 0;
    left: 0;
    z-index: 8;
  }
  .hidden-video{

    z-index: 5;
    opacity: 1;

  }
  .fullscreen-canvas{
    border: 5px solid red;
  }
}
.video-wrapper {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

.hidden-video {
  position: absolute;
  width: 100%;
  height: 100%;
  object-fit: fill;
  z-index: -1;
  opacity: 0;
}

.fullscreen-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  object-fit: fill;
  background: black;
  z-index: 1;
}
</style>
相关推荐
_AaronWong2 分钟前
基于 Vue 3 的屏幕音频捕获实现:从原理到实践
前端·vue.js·音视频开发
孟祥_成都10 分钟前
深入 Nestjs 底层概念(1):依赖注入和面向切面编程 AOP
前端·node.js·nestjs
let_code11 分钟前
CopilotKit-丝滑连接agent和应用-理论篇
前端·agent·ai编程
Apifox35 分钟前
Apifox 11 月更新|AI 生成测试用例能力持续升级、JSON Body 自动补全、支持为响应组件添加描述和 Header
前端·后端·测试
木易士心36 分钟前
深入剖析:按下 F5 后,浏览器前端究竟发生了什么?
前端·javascript
在掘金8011037 分钟前
vue3中使用medium-zoom
前端·vue.js
xump1 小时前
如何在DevTools选中调试一个实时交互才能显示的元素样式
前端·javascript·css
折翅嘀皇虫1 小时前
fastdds.type_propagation 详解
java·服务器·前端
Front_Yue1 小时前
深入探究跨域请求及其解决方案
前端·javascript
wordbaby1 小时前
React Native 进阶实战:基于 Server-Driven UI 的动态表单架构设计
前端·react native·react.js