custom-vap
v1.1.0
Published
A lightweight, WebGL-powered video animation player (VAP) for Vue 3 & modern web. Supports real-time RGB/Alpha channel compositing, VAPX blend-layer image/text replacement, precise frame rendering (rVFC), and IndexedDB data persistence. ~30KB gzipped, zer
Maintainers
Readme
🎬 Custom VAP
A lightweight, WebGL-powered video animation player (VAP) for Vue 3 & modern web.
支持视频 Alpha 通道实时合成 · VAPX 融合层动态图片/文本替换 · 精确帧渲染 · TypeScript 全量类型
⭐ 给个 Star 支持一下
如果这个项目对你有帮助,请点击右上角的 ⭐ Star 按钮,你的支持是我持续维护的动力!
✨ Features
| 特性 | 说明 |
| --- | --- |
| 🎞️ 视频 Alpha 合成 | 基于 WebGL 将 MP4 视频的 RGB 通道与 Alpha 通道实时合成,实现透明动效 |
| 🧩 VAPX 融合层 | 支持在视频上叠加图片 / 文本插槽(slot),实现动态内容替换 |
| 🎯 精确帧渲染 | 支持 requestVideoFrameCallback(rVFC)精准同步视频帧渲染,兼容环境自动回退 RAF |
| 🖼️ 运行时图片替换 | 不重编 VAP 即可替换融合层图片图层(支持 URL 动态加载) |
| 📝 动态文本注入 | 在动画播放中实时替换文本图层 |
| 👆 插槽点击事件 | 支持指定插槽 / 全局插槽的点击回调 |
| 💾 数据持久化 | 基于 IndexedDB 缓存 VAP 解析结果,减少重复网络请求 |
| 👀 可见性监听 | 多实例共享 IntersectionObserver,支持移出视口时暂停 / 跳过渲染 |
| 🎬 帧范围播放 | 支持播放任意帧区间并自定义循环次数 |
| 🌐 多产物输出 | ESM / CJS / UMD 全场景覆盖(npm、script 标签、CDN) |
| 🎯 TypeScript 一等公民 | 完整 .d.ts 类型定义,IDE 智能提示开箱即用 |
📥 Installation
# npm
npm install custom-vap
# yarn
yarn add custom-vap
# pnpm
pnpm add custom-vap浏览器 CDN(无构建工具)
<!-- unpkg -->
<script src="https://unpkg.com/custom-vap@latest/dist/index.umd.js"></script>
<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/custom-vap@latest/dist/index.umd.js"></script>
<script>
const vap = new CustomVap(document.getElementById('stage'), { autoplay: true });
vap.load('/path/to/video.mp4');
</script>🧪 演示 Demo
仓库内置 Vite 示例工程,可在线验证核心能力(含 VAPX 动态资源插槽测试)。
# 首次需安装 demo 依赖
npm install --prefix demo/vite-rollup-esm
# 启动开发服务(热更新,alias 直连包源码 src/,改动即时生效)
npm run dev --prefix demo/vite-rollup-esm浏览器打开 http://localhost:5173/,顶栏可切换页面:
| 页面 | 说明 |
| --- | --- |
| /single 单例测试 | 加载 / 播放 / 暂停 / 停止 / 销毁 / 帧范围 / 结束帧模式 / 海报等;加载 VAPX 视频后底部出现「动态资源」面板,可对每个插槽填入图片 URL 或文本,点击「应用动态资源」即调 setImage / setText / setDynamicResources |
| /stress 多实例压测 | 同时加载多个实例,验证多实例共享渲染调度与可见性优化 |
默认演示素材为远程 VAP 视频(
vap0.mp4);本地文件可点击「选择本地文件」或拖拽到舞台直接播放。
🚀 Quick Start
Vue 3 组件用法
<template>
<div ref="container" class="vap-stage"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { CustomVap } from 'custom-vap';
const container = ref<HTMLDivElement>();
let player: CustomVap | null = null;
onMounted(async () => {
if (!container.value) return;
player = new CustomVap(container.value, {
autoplay: true, // 加载后自动播放
loop: 0, // 无限循环
});
// 方式一:on(key, cb),支持同事件多监听
player.on('start', () => console.log('播放开始'));
player.on('process', ({ frame }) => console.log('当前帧:', frame));
player.on('end', () => console.log('播放结束'));
// 方式二:onLoad / onReady / onStart / onPause / onResume / onLoop /
// onStop / onEnd / onError / onVisible / onDestroy 单监听(process 与
// webglcontext* 不提供),重复调用自动替换旧回调,返回取消函数便于清理
const offEnd = player.onEnd(({ frame }) => console.log('播放结束于第', frame, '帧'));
await player.load('/path/to/video.mp4');
});
onBeforeUnmount(() => {
player?.destroy();
});
</script>
<style scoped>
.vap-stage {
width: 300px;
height: 300px;
}
</style>原生 JavaScript 用法
import { CustomVap } from 'custom-vap';
const player = new CustomVap(document.getElementById('stage'), {
loop: 3,
});
await player.load('/path/to/video.mp4');
player.play();传入 Canvas 元素
import { CustomVap } from 'custom-vap';
// 直接传入已有 canvas 元素
const player = new CustomVap(canvasEl, { loop: 0 });
await player.load('/path/to/video.mp4');
player.play();📚 API 文档
new CustomVap(el, options?)
创建播放器实例。传入 div 时内部自动创建 canvas,传入 canvas 时直接使用该画布。
| 参数 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| el | HTMLDivElement \| HTMLCanvasElement | ✅ | 挂载容器节点或 Canvas 元素 |
| options | VapOptions | ❌ | 初始化配置 |
VapOptions 配置项
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| loop | number | 1 | 循环次数(0 或负数 = 无限循环) |
| autoplay | boolean | false | 加载后自动播放 |
| muted | boolean | false | 是否静音 |
| volume | number | 1.0 | 音量(0~1) |
| fps | number | - | 目标帧率,默认使用视频原始帧率 |
| endFrameMode | 'Forward' \| 'Backward' \| 'Clearward' | 'Backward' | 播放结束行为:Backward 停在尾帧、Forward 回到首帧、Clearward 清空画布 |
| dataPersistence | boolean | false | 是否开启 IndexedDB 数据持久化缓存 |
| enableBlob | boolean | true | 是否将视频转为 Blob URL 播放 |
| isPreciseFrame | boolean | true | 是否使用 rVFC 精确帧渲染(不支持时自动回退独立 RAF) |
| isClickable | boolean | false | 是否启用 VAPX 融合层插槽点击事件 |
| invisibleBehavior | 'pause' \| 'skip' | 'skip' | 移出视口时的行为:pause 自动暂停、skip 跳过渲染保持播放 |
CustomVap 实例方法
| 方法 | 说明 |
| --- | --- |
| load(url) | 加载并解析 VAP 视频,返回 Promise<this> |
| play(loop?) | 播放动效,loop 可覆盖配置项,返回 Promise<this> |
| playRange(startFrame, endFrame, loop?) | 播放指定帧范围,返回 Promise<this> |
| pause() | 暂停播放 |
| resume() | 恢复播放 |
| stop() | 停止播放,按 endFrameMode 决定结束帧表现 |
| destroy() | 彻底销毁实例,释放视频 / WebGL / 事件等资源 |
| resize() | 手动重新计算画布物理尺寸(跟随容器与 DPR) |
| setImage(tag, url) | 通过 URL 设置融合层图片资源,返回 Promise<this> |
| setText(tag, text) | 设置融合层文本资源 |
| setDynamicResources(headData) | 批量设置动态资源,支持 [tag] 通配匹配,返回 Promise<this> |
| updateOptions(newOptions) | 动态合并更新配置 |
| on(event, callback) | 监听事件(多监听并存),返回 this |
| off(event, callback) | 移除事件监听,返回 this |
| onLoad(callback) | 便捷单监听 load(重复调用自动替换),返回取消函数 |
| onReady(callback) | 便捷单监听 ready(重复调用自动替换),返回取消函数 |
| onStart(callback) | 便捷单监听 start(重复调用自动替换),返回取消函数 |
| onLoop(callback) | 便捷单监听 loop(重复调用自动替换),返回取消函数 |
| onPause(callback) | 便捷单监听 pause(重复调用自动替换),返回取消函数 |
| onResume(callback) | 便捷单监听 resume(重复调用自动替换),返回取消函数 |
| onStop(callback) | 便捷单监听 stop(重复调用自动替换),返回取消函数 |
| onEnd(callback) | 便捷单监听 end(重复调用自动替换),返回取消函数 |
| onError(callback) | 便捷单监听 error(重复调用自动替换),返回取消函数 |
| onVisible(callback) | 便捷单监听 visible(重复调用自动替换),返回取消函数 |
| onDestroy(callback) | 便捷单监听 destroy(重复调用自动替换),返回取消函数 |
| onClick(srcTag, callback) | 注册指定插槽的点击事件 |
| onAllClick(callback) | 注册所有插槽通用的点击事件 |
CustomVap 事件回调
| 事件 | 载荷 | 触发时机 |
| --- | --- | --- |
| load | { url, config } | 视频加载解析完成 |
| ready | { currentTime } | 视频已就绪可播放(<video> canplaythrough) |
| start | { currentTime } | 播放开始 |
| process | { frame, currentTime } | 每渲染一帧触发 |
| loop | { currentTime, loopCount } | 每轮循环回到起点重新播放(loopCount 为当前开始的第几遍,自 2 起) |
| pause | { currentTime } | 播放暂停 |
| resume | { currentTime } | 播放恢复 |
| stop | { currentTime } | 播放停止 |
| end | { currentTime, frame } | 播放结束 |
| error | { error, message } | 加载 / 播放出错 |
| webglcontextlost | { message } | WebGL 上下文丢失 |
| webglcontextrestored | { message } | WebGL 上下文恢复 |
| visible | { isVisible, playState } | 容器可见性变化 |
| destroy | { currentTime, playState } | 实例销毁前触发(资源尚未释放) |
🎯 高级用法
1. 帧范围播放
// 只播放第 10~50 帧,循环 3 次
await player.playRange(10, 50, 3);2. 动态替换图片图层
// 替换融合层指定插槽的图片
await player.setImage('avatar', 'https://example.com/new-avatar.png');3. 动态注入文本
// 简单文本替换:第一个参数是插槽 tag,第二个是文本内容
player.setText('username', 'VIP 用户');4. 批量设置动态资源
// 支持 [tag] 通配:资源里的 [username] 会匹配 headData 中的 username
await player.setDynamicResources({
username: 'DreamLife',
avatar: 'https://example.com/avatar.png',
});5. 融合层插槽点击事件
const player = new CustomVap(container, { isClickable: true });
// 指定插槽点击
player.onClick('avatar', () => console.log('点击了 avatar 插槽'));
// 全局插槽点击(可拿到 srcId / srcTag / 点击区域坐标)
player.onAllClick(({ srcId, srcTag, frame }) => {
console.log('点击插槽:', srcId, srcTag, frame);
});6. 可见性优化
const player = new CustomVap(container, {
invisibleBehavior: 'pause', // 移出视口时自动暂停,回到视口自动恢复
// invisibleBehavior: 'skip', // 移出视口时跳过渲染但保持帧同步(默认)
});7. 结束帧模式
const player = new CustomVap(container, {
endFrameMode: 'Forward', // 结束回到第一帧
// endFrameMode: 'Backward', // 结束停在最后一帧(默认)
// endFrameMode: 'Clearward', // 结束清空画布
});8. 动态更新配置
player.updateOptions({ muted: true, loop: 0 });📊 特性一览
| 场景 | 说明 | | --- | --- | | 精确帧 | 支持 rVFC 的浏览器自动使用精准帧驱动,否则回退每个实例独立的 RAF | | 数据持久化 | 开启后解析结果自动写入 IndexedDB,二次加载秒开 | | 上下文丢失 | 自动监听 WebGL 上下文丢失 / 恢复,并重建着色器与纹理 | | 多实例共享 | 可见性观察器为多实例共享,节省开销;渲染循环各实例独立互不抢占 | | 自动播放拦截 | 浏览器拦截自动播放时,自动降级为静音播放 |
🌐 浏览器兼容性
| 特性 | 最低支持 | | --- | --- | | WebGL 渲染 | 所有支持 WebGL 1.0+ 的现代浏览器 | | rVFC 精确帧 | Chrome / Edge 87+,不支持时自动回退 RAF | | IntersectionObserver 可见性监听 | Chrome / Edge 51+,Firefox 55+,Safari 12.1+ | | IndexedDB 持久化 | 所有现代浏览器(不支持时自动跳过) |
🔧 常见问题
- 请确认容器具有非零宽高,且容器尺寸在
load时已就绪 - 播放器会在
load/start等生命周期自动执行resize(),若容器尺寸变化可手动调用player.resize()
播放器内置自动播放降级策略:当浏览器阻止有声自动播放时,会自动切换为静音模式重试。也可在初始化时直接设置 muted: true。
const player = new CustomVap(container, { autoplay: true, muted: true });// 播放第 1~30 帧,循环 2 次
await player.playRange(1, 30, 2);无需手动处理。播放器已监听 webglcontextlost / webglcontextrestored 事件,恢复后自动重建着色器程序、缓冲与纹理,并触发对应事件通知。
const player = new CustomVap(container, {
invisibleBehavior: 'pause', // 移出视口自动暂停,回到视口自动恢复
});🤝 贡献
欢迎 PR 和 Issue!请确保:
- 代码风格一致(TypeScript strict)
- 提交前
npm run build:rollup能成功 - 重大变更请先开 Issue 讨论
📄 License
MIT © 2026 DreamLife(Zhou)
🙏 致谢
- VAP - 腾讯视频云动画播放方案(VAP/VAPX)
⭐ 喜欢这个项目?在 GitHub 上给我们一个 Star 吧!你的支持就是最大的动力!
