使用 Canvas 替代 <video> 标签加载并渲染视频

在部分浏览器环境或业务场景下,直接使用 <video> 标签加载视频会出现首帧延迟的情况。

以下方法通过 WebGPU + Canvas 2D 将视频帧绘制到自定义 Canvas 上,让 <video> 只做解码,WebGPU 接管渲染,通过最小化对象创建 + 精准帧回调,实现高性能、可扩展、跨端一致的视频播放管线。

HTML 部分

<video id="instructional_video_id_2" :src="instru_video_src" autoplay loop muted playsinlinestyle="display: none;"></video>
<canvas id="instructional_video_id_1" width="640" height="360"style="width: 32.3125rem; height: 18.25rem;"></canvas>

JS 代码

import {WebGLVideoRenderer} from './video-canvas.js';const appInstance = createApp({data() {return {videoElement: null,isVideoLoading: false,lastVideoUrl: null,isRendering: false,renderer: null,}},mounted() {this.initRender()},methods: {initRender() {const canvas = document.getElementById('instructional_video_id_1');this.renderer = new WebGLVideoRenderer(canvas);this.videoElement = document.getElementById('instructional_video_id_2');if (!this.isVideoLoading) {this.isVideoLoading = truethis.videoElement.addEventListener('play', () => {// 视频播放时开始绘制到 canvasthis.drawVideoFrame();});this.videoElement.addEventListener('pause', () => {this.stopRendering();});this.videoElement.addEventListener('ended', () => {this.stopRendering();// 视频播放结束时重新播放// this.videoElement.currentTime = 0;// this.videoElement.play();});this.videoElement.addEventListener('error', () => {console.error('视频加载失败');});}},// 初始化视频initVideo(src) {if (this.lastVideoUrl === src) {return}this.lastVideoUrl = srcif (src === null) {return}// 设置视频源this.setVideoSource(src);},// 渲染单帧renderFrame() {// 直接调用 WebGL 渲染器this.renderer.render(this.videoElement);},// 绘制视频帧到 canvasdrawVideoFrame() {if (this.isRendering) return;this.isRendering = true;const useRVFC = 'requestVideoFrameCallback' in this.videoElement;if (useRVFC) {const rvfcLoop = () => {if (!this.isRendering) return;this.renderFrame();this.videoElement.requestVideoFrameCallback(rvfcLoop);};this.videoElement.requestVideoFrameCallback(rvfcLoop);} else {const renderLoop = () => {if (!this.isRendering) return;if (this.videoElement && !this.videoElement.paused && !this.videoElement.ended) {this.renderFrame()}requestAnimationFrame(renderLoop);};requestAnimationFrame(renderLoop);}},// 停止渲染stopRendering() {this.isRendering = false;},// 设置视频源setVideoSource(src) {this.videoElement.src = src;this.videoElement.load();// this.videoElement.play();},}

video-canvas.js代码

// video-canvas.js
export class WebGLVideoRenderer {constructor(canvas) {this.canvas = canvas;this.device = null;this.pipeline = null;this.sampler = null;this.bindGroupLayout = null;this.context = null;// 新增:可复用的对象this.currentExternalTexture = null;this.currentBindGroup = null;this.renderPassDescriptor = null;this.init();}async init() {if (!navigator.gpu) throw new Error('WebGPU not supported');const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });this.device = await adapter.requestDevice();this.context = this.canvas.getContext('webgpu');const format = navigator.gpu.getPreferredCanvasFormat();this.context.configure({device: this.device,format,alphaMode: 'opaque'});// 着色器不变const code = `@vertex fn vs(@builtin(vertex_index) i: u32) ->@builtin(position) vec4f {const pos = array(vec2f(-1, -3), vec2f(3, 1), vec2f(-1, 1));return vec4f(pos[i], 0, 1);}@group(0) @binding(0) var s: sampler;@group(0) @binding(1) var t: texture_external;@fragment fn fs(@builtin(position) p: vec4f) ->@location(0) vec4f {let uv = p.xy / vec2f(textureDimensions(t));return textureSampleBaseClampToEdge(t, s, uv);}`;const shader = this.device.createShaderModule({ code });this.bindGroupLayout = this.device.createBindGroupLayout({entries: [{ binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },{ binding: 1, visibility: GPUShaderStage.FRAGMENT, externalTexture: {} }]});this.pipeline = this.device.createRenderPipeline({layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.bindGroupLayout] }),vertex: { module: shader, entryPoint: 'vs' },fragment: { module: shader, entryPoint: 'fs', targets: [{ format }] },primitive: { topology: 'triangle-list' }});this.sampler = this.device.createSampler({magFilter: 'linear', minFilter: 'linear'});// RenderPassDescriptor 的骨架,view 每帧再填this.renderPassDescriptor = {colorAttachments: [{view: undefined,            // 占位,下面会替换loadOp: 'clear',storeOp: 'store'}]};}render(video) {if (!this.device) return;// 1. 画布尺寸变化时再改const { videoWidth, videoHeight } = video;if (this.canvas.width !== videoWidth || this.canvas.height !== videoHeight) {this.canvas.width  = videoWidth;this.canvas.height = videoHeight;}// 2. 只有在必要时才重新生成 BindGroup//    importExternalTexture 每次都会返回新对象,必须每帧调用const externalTexture = this.device.importExternalTexture({ source: video });if (this.currentExternalTexture !== externalTexture) {this.currentExternalTexture = externalTexture;this.currentBindGroup = this.device.createBindGroup({layout: this.bindGroupLayout,entries: [{ binding: 0, resource: this.sampler },{ binding: 1, resource: externalTexture }]});}// 3. 更新 colorAttachment.viewthis.renderPassDescriptor.colorAttachments[0].view =this.context.getCurrentTexture().createView();// 4. 复用 RenderPassDescriptor,不再每帧 newconst encoder = this.device.createCommandEncoder();const pass = encoder.beginRenderPass(this.renderPassDescriptor);pass.setPipeline(this.pipeline);pass.setBindGroup(0, this.currentBindGroup);pass.draw(3);pass.end();this.device.queue.submit([encoder.finish()]);}dispose() {this.device?.destroy();}
}

关键点

  • <video> 元素仅作解码器,不可见 (display: none)。

  • 每帧通过 requestVideoFrameCallback(优先)或 requestAnimationFrame 轮询,把最新纹理塞进 WebGPU。

  • Canvas 尺寸动态跟随 video.videoWidth / videoHeight,防止花屏。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/93375.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/93375.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Flask的智能停车场管理系统开发实践

在现代城市中&#xff0c;停车难已成为一个普遍问题。为了解决这一问题&#xff0c;我开发了一个基于Python Flask框架的智能停车场管理系统。该系统集成了车牌识别、车位状态监控、收费管理等多项功能&#xff0c;为停车场的智能化管理提供了完整的解决方案。系统功能概述该停…

【C#获取高精度时间】

在C#中&#xff0c;有几种方法可以获取高精度时间&#xff08;高分辨率时间戳&#xff09;&#xff0c;适用于性能测量、计时等需要高精度的场景。以下是几种常用方法&#xff1a; 1. 使用 Stopwatch 类&#xff08;推荐&#xff09; Stopwatch 类提供了最高精度的时间测量&…

Spring Boot + React 打造现代化高校成绩管理系统实战记录

作者: 笙囧同学 发布时间: 2025年7月 技术栈: Spring Boot 3.2.3 React 18 TypeScript 华为云GaussDB 项目类型: 全栈Web应用 开发周期: 30天 代码量: 15000 行 &#x1f4d6; 前言 大家好&#xff0c;我是笙囧同学&#xff01;&#x1f64b;‍♂️ 作为一名计算机科学与技…

形参表不匹配(BUG)

在您的代码中&#xff0c;存在两个主要问题导致"形参表中不匹配"的错误&#xff1a;erase() 函数中的成员变量名错误iterator erase(iterator pos) {// ...size--; // ❌ 错误&#xff1a;成员变量名为 _size 而非 sizereturn iterator(next); }修正&#xff1a;ite…

Spring循环依赖以及三个级别缓存

Spring循环依赖以及三个级别缓存 什么是循环依赖&#xff1f; 循环依赖&#xff0c;顾名思义&#xff0c;就是指两个或多个 Spring Bean 之间相互依赖&#xff0c;形成一个闭环。 最常见也是 Spring 能够“解决”的循环依赖是构造器注入 和 setter 注入 混合或单独使用时&…

《零基础入门AI:OpenCV图像预处理进一步学习》

本文全面讲解OpenCV图像预处理的七大核心技术&#xff08;插值方法、边缘填充、图像矫正&#xff08;透视变换&#xff09;、图像掩膜、ROI切割、图像添加水印、图像噪点消除&#xff09;&#xff0c;每个知识点都配有详细解释和实用代码示例&#xff0c;帮助初学者建立系统的图…

MongoDB的内存和核心数对于运行效率的影响

在 MongoDB 线上生产环境中&#xff0c;CPU&#xff08;核心&#xff09; 和 内存 是两大关键硬件资源&#xff0c;它们在不同的操作场景下发挥着核心作用&#xff0c;共同影响着数据库的性能、稳定性和扩展性。理解它们的作用场景至关重要&#xff0c;是容量规划、性能优化和故…

自己的SAPGUI尝试

为满足用户需求&#xff0c;博主做了一个台账管理程序&#xff0c;尝试用自己的程序做GUI&#xff0c;用SAP 系统做数据库。 运行了半年&#xff0c;程序很nice,用户每天都在高效的使用&#xff0c;已经有十几万的数据。 总结一下这次自己的GUI尝试&#xff0c;好处是C# WINFOR…

高效处理 JSON 数据:JsonUtil 工具类全方位解析与实战

在现代软件开发中,JSON(JavaScript Object Notation)已成为数据交换的“通用语言”——从前后端接口通信到微服务数据交互,从配置文件解析到日志格式化,几乎所有场景都离不开JSON的处理。然而,原生JSON框架(如FastJSON、Jackson)的API往往需要大量重复代码,且空指针、…

Python 库手册:xmlrpc.client 与 xmlrpc.server 模块

xmlrpc.client 和 xmlrpc.server 是 Python 标准库中用于构建基于 XML-RPC 协议的远程过程调用&#xff08;RPC&#xff09;通信模块。xmlrpc.client 用于编写客户端程序&#xff0c;向远程服务器发起方法调用。xmlrpc.server 用于编写服务器端&#xff0c;暴露本地方法供远程客…

渲染篇(一):从零实现一个“微型React”:Virtual DOM的真面目

渲染篇(一)&#xff1a;从零实现一个“微型React”&#xff1a;Virtual DOM的真面目 引子&#xff1a;前端性能的“永恒之问” 在前面两章中&#xff0c;我们已经奠定了坚实的架构基础。我们用“任务调度器”建立了声明式和模块化的编程范式&#xff0c;并通过对比MVC等模式论…

SWC 深入全面讲解

一、核心功能与原理 1. 高性能编译 Rust 架构优势&#xff1a;SWC 基于 Rust 编写&#xff0c;利用 Rust 的性能和并发性优势&#xff0c;编译速度比 Babel 快约 20 倍&#xff0c;比 TypeScript 编译器更快。并行编译&#xff1a;支持多线程并行处理&#xff0c;在四核基准测试…

XML Expat Parser:深入解析与高效应用

XML Expat Parser:深入解析与高效应用 引言 XML(可扩展标记语言)作为一种广泛使用的标记语言,在数据交换、存储和表示中扮演着重要角色。XML Expat Parser 是一个高性能、可扩展的XML解析库,广泛应用于各种编程语言中。本文将深入探讨XML Expat Parser 的原理、特性以及…

【Python】自动化GIT提交

在日常开发中&#xff0c;我们经常需要频繁地向 Git 仓库提交代码。虽然 git add、git commit、git push 这几个命令并不复杂&#xff0c;但重复操作容易出错&#xff0c;也浪费时间。本文将介绍如何使用 Python 脚本自动化完成 Git 提交流程&#xff0c;让开发更高效&#xff…

基于Qlearning强化学习的水下无人航行器路径规划与避障系统matlab性能仿真

目录 1.引言 2.算法仿真效果演示 3.数据集格式或算法参数简介 4.算法涉及理论知识概要 5.参考文献 6.完整算法代码文件获得 1.引言 水下无人航行器 (Autonomous Underwater Vehicle, AUV) 的路径规划与避障是海洋探索、资源开发和军事应用中的关键技术。传统的路径规划方…

模块自由拼装!Python重构DSSAT作物模块教程(以杂交水稻为例)

基于过程的作物生长模型&#xff08;Process-based Crop Growth Simulation Model&#xff09;在模拟作物对气候变化的响应与适应、农田管理优化、作物品种和株型筛选、农业碳中和、农田固碳减排等领域扮演着越来越重要的作用。Decision Support Systems for Agrotechnology Tr…

Java项目接口权限校验的灵活实现

引言 在Java Web开发中&#xff0c;接口权限校验是保护系统资源安全的关键机制。本文将介绍一种灵活、可配置的接口权限校验方案&#xff0c;通过注解驱动和拦截器实现&#xff0c;既能保证安全性&#xff0c;又能灵活控制哪些接口需要校验。 设计思路 实现方案的核心设计要点&…

瀚高DB兼容MySQL if函数

文章目录环境症状问题原因解决方案环境 系统平台&#xff1a;Linux x86-64 Red Hat Enterprise Linux 7 版本&#xff1a;4.5 症状 MySQL if函数在瀚高DB当中没有&#xff0c;源应用在用到if函数时&#xff0c;就会报if函数不存在的错误信息。为此&#xff0c;我们需要根据业…

基于深度学习的胸部 X 光图像肺炎分类系统(六)

目录 结果指标解读 一、为什么选择这些指标&#xff1f; 二、各指标的定义和解读 1. 准确率&#xff08;Accuracy&#xff09; 2. 损失&#xff08;Loss&#xff09; 3. 精确率&#xff08;Precision&#xff09; 4. 召回率&#xff08;Recall&#xff09; 三、这些指标…

区块链性能优化策略:从理论到实践

目录 区块链性能优化策略:从理论到实践 1. 引言:区块链性能的挑战 2. 性能评估指标 2.1 核心性能指标 2.2 性能瓶颈分析 3. 分层优化策略 3.1 网络层优化 3.1.1 Gossip协议改进 3.1.2 网络分片 3.2 共识层优化 3.2.1 PBFT优化 3.3 数据层优化 3.3.1 状态树优化 3.3.2 区块数据…