快速知道 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>
相关推荐
roamingcode1 小时前
Claude Code NPM 包发布命令
前端·npm·node.js·claude·自定义指令·claude code
码哥DFS1 小时前
NPM模块化总结
前端·javascript
灵感__idea1 小时前
JavaScript高级程序设计(第5版):代码整洁之道
前端·javascript·程序员
唐璜Taro2 小时前
electron进程间通信-IPC通信注册机制
前端·javascript·electron
陪我一起学编程3 小时前
创建Vue项目的不同方式及项目规范化配置
前端·javascript·vue.js·git·elementui·axios·企业规范
LinXunFeng3 小时前
Flutter - 详情页初始锚点与优化
前端·flutter·开源
GISer_Jing3 小时前
Vue Teleport 原理解析与React Portal、 Fragment 组件
前端·vue.js·react.js
Summer不秃4 小时前
uniapp 手写签名组件开发全攻略
前端·javascript·vue.js·微信小程序·小程序·html
coderklaus4 小时前
Base64编码详解
前端·javascript
NobodyDJ4 小时前
Vue3 响应式大对比:ref vs reactive,到底该怎么选?
前端·vue.js·面试