<p>应避免监听 video 的 ended 事件,而改用 timeupdate 主动判断接近结尾(如 currentTime ≥ duration - 0.1)、复用 DOM 元素、预加载+双视频视觉过渡、适配 iOS 手势限制,五者协同实现真正无缝切换。</p>

video 元素的 ended 事件不可靠,别直接监听它做切换
浏览器对 ended 的触发时机不一致:有的在最后一帧渲染完就发,有的卡在音频末尾才发,还有的在缓冲失败时误触发。尤其当视频编码有 B 帧或音频轨略长于视频轨时,ended 往往滞后几十毫秒,导致黑屏或跳帧。
真正稳定的做法是监听 timeupdate,主动判断是否接近结尾:
- 设置阈值(如
0.1秒),当video.currentTime >= video.duration - 0.1时触发切换 - 必须配合
video.playbackRate = 1,避免变速播放干扰时间判断 - 检查
video.readyState === 4再执行切换,防止因加载未完成而报错DOMException: The element has no supported sources
多个 <video> 元素共用一个播放器 DOM,比新建更稳
不要每次切换都 document.createElement('video') 或反复 innerHTML = '' —— 这会触发完整销毁重建,丢失硬件解码上下文,造成首帧延迟甚至音画不同步。
推荐复用单个 <video> 元素,只换 src 和重置状态:
立即学习“前端免费学习笔记(深入)”;
- 切换前调用
video.pause()、video.currentTime = 0 - 用
video.load()清除旧缓冲,但别等canplaythrough再播——它可能永远不触发(尤其 HLS 或低带宽下) - 直接
video.src = nextUrl后立刻video.play().catch(e => console.warn('play failed:', e)),现代浏览器支持“静音自动播放”策略下基本能成功
无缝关键:预加载下一视频 + 隐藏画面过渡
人眼察觉卡顿的阈值约 50ms,纯 JS 切换做不到毫秒级衔接。必须用视觉欺骗:
- 准备两个
<video>元素(videoA和videoB),始终有一个在后台静音加载并 seek 到 0 - 当前播放的视频进入最后 0.3 秒时,启动
videoB的load()并videoB.play()(此时videoA还没结束) - 用 CSS 控制 opacity 或 visibility,在
videoA的timeupdate判断到currentTime >= duration - 0.05时,立即videoA.style.display = 'none',videoB.style.display = 'block' - 两个 video 尺寸、poster、宽高比必须完全一致,否则 DOM 重排会闪动
移动端 iOS Safari 的静音限制必须绕过
iOS Safari 要求首次播放必须由用户手势触发,且之后所有 play() 调用必须在同一个用户交互链路内,否则报错 NotAllowedError: play() can only be initiated by user gesture。
解决方法只有两个:
- 页面加载后,第一次播放必须绑定在按钮 click 或 touchstart 上(不能是 setTimeout 或 load 事件)
- 后续切换全部走同一个
play()链路:比如用button.addEventListener('click', () => { video.play(); })初始化,之后所有切换都用video.play()而不是video.src = ...; video.play()—— 后者会被视为新交互 - 如果要用自动切换,必须在首次手势后立刻调用
video.play()并保持 Promise 不 reject,iOS 才会授予“可自动播放”权限
真实场景里,“无缝”从来不是靠单点技术实现的,而是预加载、时间判断、DOM 复用、视觉过渡和平台限制处理这五件事咬合在一起的结果。漏掉任意一环,用户都会在某个设备上看到黑屏或跳帧。



















