快速知道 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>
相关推荐
Wcowin18 分钟前
MkDocs文档日期插件【推荐】
前端·mkdocs
xw51 小时前
免费的个人网站托管-Cloudflare
服务器·前端
网安Ruler1 小时前
Web开发-PHP应用&Cookie脆弱&Session固定&Token唯一&身份验证&数据库通讯
前端·数据库·网络安全·php·渗透·红队
!win !1 小时前
免费的个人网站托管-Cloudflare
服务器·前端·开发工具
饺子不放糖1 小时前
基于BroadcastChannel的前端多标签页同步方案:让用户体验更一致
前端
饺子不放糖1 小时前
前端性能优化实战:从页面加载到交互响应的全链路优化
前端
Jackson__1 小时前
使用 ICE PKG 开发并发布支持多场景引用的 NPM 包
前端
饺子不放糖1 小时前
前端错误监控与异常处理:构建健壮的Web应用
前端
cos2 小时前
FE Bits 前端周周谈 Vol.1|Hello World、TanStack DB 首个 Beta 版发布
前端·javascript·css
饺子不放糖2 小时前
CSS的float布局,让我怀疑人生
前端