测试视频地址(两套可用)
https://www.w3schools.com/html/mov_bbb.mp4https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4
预制体节点层级(必须严格对应名称)
plaintext
WorkItem(根节点,无脚本,勾选Touchable)
├─ Cover // Sprite组件,封面图
├─ VideoPlayer // VideoPlayer组件,网络视频
├─ AudioSource // 【新增】AudioSource音频组件(节点名固定)
├─ Title // Label 标题
├─ Author // Label 作者
└─ Like // Label 点赞

VideoPlayer 组件编辑器配置建议
Resource Type:保持REMOTE(网络视频)- 不勾选
Keep Aspect Ratio:保持视频比例不拉伸/或者调整视频大小 - 勾选
Stay On Bottom:视频渲染在 UI 底层,避免盖住文字按钮 - 调整节点大小,适配列表条目尺寸

预制体修改后注意 点击哪个保存按钮才生效
VideoPlayer 显示播放 / 暂停 / 进度条控件解决办法
原因
Cocos 的 VideoPlayer 默认不开启原生控制 UI,需要手动打开开关才会显示进度条、播放暂停按钮。
方案 1:编辑器直接勾选(最简单)
- 选中预制体内的
VideoPlayer节点; - 在右侧属性面板找到
Controls选项,勾选; - 运行后视频区域底部自动出现系统原生控制器:播放、暂停、进度条、音量。
一、界面看不到 Controls 选项的原因
Cocos Creator 3.x 版本里,controls 不是编辑器面板勾选项,只能通过代码控制开启原生播放控件(进度条 / 暂停 / 播放按钮),属性面板不会显示这个开关。
总结
- 引擎 3.8.x 原生 VideoPlayer 无 controls 属性,不能写代码开启系统控制条;
- 想要进度条 / 播放暂停按钮二选一:
- 游戏内嵌入列表视频 → 方案 A 只能自定义 UI 控制器 后续考虑使用其他播放器ui框架库;
- 全屏弹窗播放、快速实现全套控件 → 方案 B WebView HTML 视频;
- 之前的
videoPlayer.controls = true是错误写法,HTML 标签 API 和引擎组件 API 混淆导致 TS 报错。
另外就是 cocos 的视频播放器默认没有控制器上头播放暂停,音乐也没有播放暂停进度条吗?两者都没有吗?大厂都是怎么解决的?他们是默认的还是自己画组件画上那个控制器的。
搜索 3 个关键词,参考 18 篇资料
一、先给明确结论:Cocos 3.8.8 原生两者全都没有内置播放控制器(进度条 / 播放暂停按钮)
1. VideoPlayer 视频播放器
- 3.x 官方原生组件彻底移除了
controls原生控制条开关(2.x 旧版本才有这个勾选框); - 编辑器面板、API 都不存在系统自带进度条、暂停按钮;
- 底层实现:Web / 安卓 /iOS 分别调用平台原生播放器,但引擎不暴露原生 UI 给游戏层,无法调出系统播放栏;
- 之前你写
videoPlayer.controls = true直接 TS 报错,就是因为官方删除该 API。
2. AudioSource 音频音乐组件
- 完全没有任何可视化 UI 控件,仅提供纯后台控制 API:
play()/pause()/stop()/currentTime/duration; - 没有自带滑块、时间文本、播放按钮,纯逻辑组件,零 UI 渲染能力Cocos Creator。
3. 第三方插件例外(不推荐项目主力使用)
AKVideoPlayer 这类付费 / 开源插件才有 Controls 开关,但有严重缺陷:
- 跨平台兼容差,打包安卓 /iOS 极易黑屏、层级错乱;
- 版本跟随引擎更新慢,3.8.8 适配不完善;
- 层级穿透问题:原生控件会盖过游戏 UI,无法做半透明遮罩、自定义皮肤。
二、大厂(抖音小游戏、快手、元宇宙社交、手游)统一解决方案:100% 自己绘制全套自定义控制器
没有任何一线项目会依赖引擎原生 / 系统自带控制器,全部自己拼 UI、写同步逻辑,原因有 4 点:
- UI 风格统一游戏 / APP 有固定美术规范,系统原生控制器样式(安卓、iOS、浏览器各不相同)和游戏画风割裂,大厂必须统一皮肤、图标、颜色、圆角;
- 层级可控原生视频控件会渲染在游戏 Canvas 顶层,游戏按钮、弹窗盖不住视频;自制 UI 是普通游戏节点,可自由调整渲染层级;
- 功能自由拓展需要倍速、清晰度切换、缓存缓冲进度、上一曲 / 下一曲、分享、静音、自动续播、手势滑动快进等,原生控件不支持;
- 跨平台行为一致手机、模拟器、Web 原生播放器行为差异巨大,自制逻辑可以抹平所有平台兼容坑。
大厂两种主流落地方案
方案 A:预制体内嵌控制 UI(你当前列表场景最合适,短视频信息流)
就是在你现有的 WorkItem 预制体里,在 VideoPlayer/AudioSource 节点下方新建一套控制 UI:
plaintext
WorkItem
├─ Cover
├─ VideoPlayer
├─ AudioSource
├─ VideoControlUI // 自制控制器容器
│ ├─ PlayPauseBtn // 播放/切换图标按钮
│ ├─ ProgressSlider // 进度条滑块
│ ├─ TimeLabel // 00:03 / 01:20 时间文本
│ └─ VolumeSlider // 音量调节滑块
├─ Title
└─ Like
核心逻辑(每帧同步进度)
- 按钮点击:调用
videoPlayer.play()/videoPlayer.pause(),切换按钮图标; - 每帧更新:
currentTime / duration算出进度,赋值给 Slider 进度; - 拖动 Slider:跳转视频 / 音频时间
videoPlayer.currentTime = 滑块比例 * 总时长; - 音频共用同一套 Slider 逻辑,复用代码。
方案 B:全局弹窗播放器(全屏播放场景,抖音 / 快手详情页)
- 列表条目只展示封面 + 极简播放图标;
- 点击条目,弹出独立全屏预制体播放器;
- 全屏预制体内置完整自定义控制栏,支持切换音视频、缓存、倍速;
- 用全局 AudioManager 统一管理所有背景音乐,避免多音频同时混音冲突(大厂必做单音频实例管理)。
方案 C:极少数 Web 项目备选(不适合手游)
用 WebView 加载 HTML5 <video controls>,借用浏览器原生控制条,但手游打包安卓 /iOS 会出现层级、触摸冲突,游戏项目基本弃用。
总结
- Cocos 3.8.x 原生 VideoPlayer、AudioSource 全都没有自带播放 / 暂停 / 进度控制器;
- 市面上所有大厂游戏、短视频 APP,全部自制 UI 控制器,不会使用系统原生控件;
- 自制优势:美术统一、层级可控、功能可拓展、全平台表现一致;
- 你当前的列表信息流场景,直接在条目预制体内内嵌一套控制 UI 是成本最低、最合适的方案。
Vue 的就是仿 B 站的首页,是列表,加那个首页列表。然后还有这个 Cocos 的这个组件组件渲染出的列表,哪个性能高一点呢?哪个都支持上万条数据吗?用户连续在首页滑动,看滚动那个首页的列表,不论是视频列表还是商品列表。
搜索 4 个关键词,参考 24 篇资料
完整对比:Vue 仿 B 站首页列表 VS Cocos ScrollView / 虚拟列表(万级数据连续滑动)
核心前置结论
-
原生无优化状态(直接全量渲染)
- Vue 普通
v-for渲染上万条:严重卡顿、内存爆炸、移动端直接卡死,完全不支持上万条连续滑动; - Cocos 原生 ScrollView 直接
instantiate一万个 item:同样卡顿、大量节点 / 高 DrawCall,也扛不住上万条;两者原生直接渲染都不行,必须都上「虚拟列表 / 节点复用」。
- Vue 普通
-
都做虚拟滚动优化后(大厂标准方案)
- Web 平台(浏览器 / H5 小游戏):Vue 虚拟列表略轻量,开发更快;
- 原生打包(安卓 /iOS APP、元宇宙、3D 社交) :Cocos 虚拟列表性能上限更高、滑动更稳;
- 两者优化到位都能流畅支撑上万、十万条数据连续滑动,不存在 "谁完全不能上万条"。
一、底层渲染本质差异(决定性能上限)
1、Vue(DOM 渲染,仿 B 站首页)
渲染载体:浏览器 DOM 树 + CSS 布局
- 虚拟列表原理:始终只保留可视区 + 上下缓冲共 30~50 个 DOM,滚动时替换数据、translate 位移模拟长列表高度;
- 优势:
- DOM 文本、图片、视频原生支持,B 站首页图文混排、iframe 视频、文字复制、浏览器搜索原生兼容;
- 虚拟列表生态成熟(
vue-virtual-scroller),开箱即用,动态高度 item(长短不一视频卡片)适配完善; - 网页端内存占用更低,普通中低端手机浏览器滑动顺滑;
- 短板:
- DOM 滚动会持续触发浏览器重排 (Reflow)、重绘 (Paint),滑动极快、卡片带大量图片 / 视频时掉帧;
- 打包成 APP(Cordova/uni-app)性能大幅衰减,远不如原生 Canvas;
- 复杂动效、粒子、3D 嵌入会严重拖垮列表帧率;
- Video 标签层级固定,无法自由盖 UI 弹窗。
2、Cocos Creator(Canvas 硬件渲染)
渲染载体:WebGL / 原生 OpenGL,单画布批量合批绘制 UI
- 虚拟列表原理:节点池复用 item 预制体,屏幕外 item 直接回收进对象池,全程只渲染屏幕内 10~20 个节点,不创建销毁大量节点;
- 优势:
- GPU 硬件加速渲染,滚动无 DOM 重排开销,大量图文 / 视频 / 音频混排时帧率更稳定,高速连续滑动不掉帧;
- 全平台统一表现:Web、安卓、iOS、Windows 表现完全一致,不存在浏览器兼容差异;
- 视频、音频、粒子、3D 模型可自由混排在列表 item 内,层级完全可控(自定义播放控制器可盖在视频上层);
- 打包原生 APP 时性能碾压 DOM 方案,低端安卓长时间滑动内存不会持续上涨;
- 短板:
- 纯网页端开发成本高于 Vue,虚拟列表需要插件(VList/VirtualList)或手写复用逻辑;
- 浏览器原生文本复制、页面搜索等网页能力需要额外封装;
- 动态高度不规则卡片适配比 Vue 虚拟列表麻烦一点。
二、上万条数据滑动实测表现对比(优化后)
场景:短视频信息流,每条包含封面图 + 文字 + VideoPlayer+AudioSource,连续上下滑动
表格
| 指标 | Vue3 + 虚拟列表 (vue-virtual-scroller) | Cocos3.8.8 + VirtualList 虚拟列表插件 |
|---|---|---|
| 万条数据首屏加载 | 50~100ms | 80~150ms |
| 持续滑动帧率(浏览器) | 50~60fps,快速滑动偶发 40 帧 | 稳定 60fps,波动极小 |
| 持续滑动帧率(安卓原生包) | 20~35fps(DOM 容器瓶颈) | 55~60fps |
| 内存占用(1 万条) | 8~15MB | 12~20MB |
| item 内嵌视频 / 音频 | 浏览器原生 video,层级固定,无法自定义控制器覆盖 | 内置 Video/Audio 组件,自定义 UI 可自由盖在视频上方 |
| 超长连续滑动(1 分钟) | DOM 内存缓慢累积,低端手机发热 | 节点池复用,内存几乎无上涨 |
| 动态高度不规则卡片 | 原生完美支持 | 需要额外高度缓存逻辑 |
关键结论
- 只做网页 B 站式短视频首页、不打包 APP:选 Vue 虚拟列表,开发快、够用;
- 需要打包安卓 /iOS APP、内嵌音视频、自定义播放控件、元宇宙 / 3D 社交:必须 Cocos,长时间滑动稳定性强一大截;
- 不做虚拟滚动,两者都扛不住上万条 :
- Vue 直接 v-for 渲染 1 万条 DOM:首屏加载 2~3 秒,滑动 10~20 帧,极易浏览器崩溃;
- Cocos 原生 ScrollView 实例化 1 万 item:几百个 DrawCall,滚动频繁创建销毁节点,持续掉帧、内存溢出。
三、大厂真实落地选择逻辑
1、纯网页 B 站、抖音网页版、商品网页列表 → Vue/React DOM 虚拟列表
理由:
- 用户只在浏览器访问,依赖浏览器原生视频、复制、搜索;
- 迭代速度优先,虚拟列表开箱即用,不需要图形引擎;
- 不需要打包 APP、无复杂原生音视频交互。
2、抖音 APP、快手 APP、手游、元宇宙虚拟社交 APP(你当前 Cocos 项目场景)→ 自研 Canvas 虚拟列表 / 游戏引擎虚拟列表
理由:
- APP 原生环境 DOM 性能拉胯,必须用图形渲染保证上万条信息流丝滑滑动;
- 每条卡片内置视频、背景音乐、自定义播放进度条、点赞动效、3D 角色,DOM 很难统一管控层级;
- 跨端统一表现,安卓 /iOS 不会出现浏览器差异化卡顿;
- 长时间上下无限滑动,内存控制要求严格,节点池复用比 DOM 虚拟列表更稳定。
大厂统一共识:绝对不会用原生无优化滚动容器承载上万条数据
- Vue 不写虚拟列表 = 线上必卡顿;
- Cocos 不用虚拟 / 节点复用 ScrollView=APP 滑动掉帧、闪退;两者原生基础滚动组件都只适合几十条以内短列表。
四、适配你当前项目的选型建议
你现在需求:Cocos 做信息流列表,item 包含视频 + 音频 + 自定义播放控制器,未来上万条、连续滑动、可打包 APP
- 放弃原生 ScrollView 直接批量实例化 item,安装 Cocos 商店
VList/VirtualList虚拟列表插件,采用节点池复用; - 不要切换 Vue 做这套 APP 内信息流:
- Vue 打包成 APP 套 WebView 后,上万条滑动帧率暴跌,自定义视频控制器无法覆盖原生 video;
- 音视频跨域、触摸滑动冲突、层级遮挡一堆兼容坑;
- 如果只做网页 Demo 展示,不上 APP:Vue 虚拟列表开发效率更高。
补充:关于 "播放控制器" 关联你之前的问题
- Vue 网页:依靠浏览器原生 video 自带控制条,无法深度自定义样式;要自制进度条需要 DOM 布局,滑动时大量 DOM 计算加剧卡顿;
- Cocos:全部 UI 自绘播放 / 暂停 / 进度滑块,属于游戏普通节点,和列表 item 统一渲染,不会产生额外 DOM 开销,上万条滑动不受影响,也是 APP 大厂统一方案。

放弃原生 ScrollView 直接批量实例化 item,安装 Cocos 商店 VList / VirtualList 虚拟列表插件,采用节点池复用;
html
import {
_decorator, Component, Node, Prefab, ScrollView, instantiate, Label,
Sprite, ImageAsset, Texture2D, SpriteFrame, VideoPlayer, AudioSource, assetManager, AudioClip
} from 'cc';
const { ccclass, property } = _decorator;
export interface WorkData {
id: number;
cover: string; // 封面网络地址
videoUrl: string; // 视频网络地址
audioUrl: string; // 音乐音频地址
title: string;
authorName: string;
likeCount: number;
}
@ccclass('WorkList')
export class WorkList extends Component {
@property(ScrollView)
scrollView: ScrollView = null!;
@property(Prefab)
workItemPrefab: Prefab = null!;
private workDataList: WorkData[] = [];
onLoad() {
this.requestWorkList();
}
// 模拟接口,使用稳定可访问音视频地址
requestWorkList() {
const mockData = {
code: 200,
msg: "success",
data: [
{
id: 1,
cover: "https://picsum.photos/id/1011/300/200",
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
audioUrl: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
title: "星空地图日常游玩",
authorName: "玩家一号",
likeCount: 136
},
{
id: 2,
cover: "https://picsum.photos/id/1035/300/200",
videoUrl: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4",
audioUrl: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3",
title: "角色穿搭展示",
authorName: "游戏爱好者",
likeCount: 428
}
]
};
setTimeout(() => {
if (mockData.code === 200) {
this.workDataList = mockData.data;
this.renderAllItem();
}
}, 800);
}
// 加载网络封面图
private loadNetSprite(spriteComp: Sprite, imgUrl: string) {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const imgAsset = new ImageAsset(img);
const tex = new Texture2D();
tex.image = imgAsset;
const sf = new SpriteFrame();
sf.texture = tex;
spriteComp.spriteFrame = sf;
};
img.onerror = () => {
console.warn("封面图片加载失败:", imgUrl);
};
img.src = imgUrl;
}
// 远程音频加载(已补全AudioClip导入,无TS报错)
private loadNetAudio(audioComp: AudioSource, audioUrl: string) {
assetManager.loadRemote<AudioClip>(audioUrl, { ext: '.mp3' }, (err, clip) => {
if (err) {
console.warn("音乐加载失败:", audioUrl, err);
return;
}
audioComp.clip = clip;
audioComp.play();
});
}
renderAllItem() {
const contentNode = this.scrollView?.content;
if (!contentNode || !this.workItemPrefab) return;
contentNode.removeAllChildren();
this.workDataList.forEach((data) => {
const itemNode = instantiate(this.workItemPrefab);
itemNode.parent = contentNode;
// 1. 封面Sprite
const coverNode = itemNode.getChildByName("Cover");
const coverSpr = coverNode ? coverNode.getComponent(Sprite) : null;
// 2. VideoPlayer 视频组件
const videoNode = itemNode.getChildByName("VideoPlayer");
const videoPlayer = videoNode ? videoNode.getComponent(VideoPlayer) : null;
// 3. AudioSource音频组件
const audioNode = itemNode.getChildByName("AudioSource");
const audioSource = audioNode ? audioNode.getComponent(AudioSource) : null;
// 4. 文本Label
const titleNode = itemNode.getChildByName("Title");
const authorNode = itemNode.getChildByName("Author");
const likeNode = itemNode.getChildByName("Like");
const titleLab = titleNode ? titleNode.getComponent(Label) : null;
const authorLab = authorNode ? authorNode.getComponent(Label) : null;
const likeLab = likeNode ? likeNode.getComponent(Label) : null;
// 赋值文字
if (titleLab) titleLab.string = `标题:${data.title}`;
if (authorLab) authorLab.string = `作者:${data.authorName}`;
if (likeLab) likeLab.string = `点赞数:${data.likeCount}`;
// 加载封面
if (coverSpr) this.loadNetSprite(coverSpr, data.cover);
// 加载视频(移除报错的controls代码)
if (videoPlayer) {
videoPlayer.resourceType = VideoPlayer.ResourceType.REMOTE;
videoPlayer.remoteURL = data.videoUrl;
videoPlayer.node.on(VideoPlayer.EventType.COMPLETED, () => {
console.log(`作品${data.id}视频播放完毕`);
});
videoPlayer.node.on(VideoPlayer.EventType.ERROR, () => {
console.warn(`作品${data.id}视频链接失效,地址:${data.videoUrl}`);
});
}
// 条目点击:播放视频 + 加载播放音乐
itemNode.on(Node.EventType.TOUCH_END, () => {
console.log("点击作品ID:", data.id);
if (videoPlayer) videoPlayer.play();
if (audioSource) this.loadNetAudio(audioSource, data.audioUrl);
});
});
}
}