在浏览器端处理音视频时,开发者经常会调用媒体元素的play函数来开始播放,但play本身只是一个触发播放的指令,并不提供播放何时结束的反馈。要准确知道一段音频或视频是否已经播完,必须依靠媒体元素自身的状态和事件机制。

play函数与结束检测的关系
play函数是HTMLMediaElement接口提供的方法,调用后浏览器会尝试播放媒体资源,它返回的是一个Promise对象,用来反映播放启动是否成功,例如被浏览器自动播放策略拦截时会拒绝。但它从设计上就不包含“播完了”这一信息,因为启动和结束处在媒体生命周期的不同阶段。
真正描述“播放结束”的是媒体元素的ended属性以及ended事件。当当前播放位置到达资源末尾且未设置循环时,浏览器会将ended设为true并派发ended事件。因此检测播放结束的核心思路是:用play启动,用ended收尾,两者分工明确。
使用ended事件监听播放结束
最基础也最推荐的方式,是直接在audio或video元素上注册ended事件的监听器。这种方式由浏览器在准确的时间点触发,不需要开发者自己计算时间,也不会带来额外的轮询开销。
下面是一段原生JavaScript的示例,展示如何在一个视频播完时修改页面提示并调用后续函数:
// 获取视频元素
const videoEl = document.querySelector('video');
// 点击按钮开始播放
document.querySelector('#startBtn').addEventListener('click', () => {
videoEl.play().catch(err => {
console.log('播放启动失败:' + err.message);
});
});
// 监听结束事件
videoEl.addEventListener('ended', () => {
console.log('视频已播放结束');
document.querySelector('#tip').textContent = '播放完成,请选择下一步操作';
handleVideoFinished();
});
function handleVideoFinished() {
// 执行播放结束后的业务逻辑
}
这种写法的优点是逻辑清晰、性能友好。需要注意的是,如果元素设置了loop属性为true,浏览器会循环播放而不会触发ended事件,此时应移除loop或在循环中自行判断次数。
在列表连播场景中,可以在ended事件里加载下一个资源并再次调用play,从而实现自动接续,而不必用定时器去猜是否播完。
结合Promise与play的检测封装
有些业务希望把“播放并开始等待结束”封装成一个可等待的过程。由于play返回的是启动Promise,而ended是独立事件,我们可以用Promise手动桥接两者,让调用方用async或then风格写代码。
以下代码演示如何封装一个waitUntilEnded函数,它在调用play后,返回一个在新媒体结束才resolve的Promise:
function playAndWait(mediaEl) {
return new Promise((resolve, reject) => {
// 先绑定结束监听,避免错过事件
mediaEl.addEventListener('ended', function onEnded() {
mediaEl.removeEventListener('ended', onEnded);
resolve('ended');
});
const p = mediaEl.play();
if (p && typeof p.then === 'function') {
p.catch(err => reject(err));
}
});
}
// 使用方式
async function run() {
const audio = document.querySelector('audio');
try {
await playAndWait(audio);
console.log('音频播放结束');
} catch (e) {
console.log('播放过程出错:' + e.message);
}
}
这种封装把启动失败和播放结束都纳入了Promise体系,适合在复杂异步流程里使用。但要小心重复绑定监听,所以在示例里用了具名函数并在触发后移除。
如果媒体元素被重新赋值src,旧的ended监听可能失效,封装时应考虑在换源前清理监听器,或者使用AbortController来管理事件绑定生命周期。
常见误区与避坑建议
一个典型误区是用setInterval不断对比currentTime和duration来判断结束。这种做法不仅浪费主线程资源,还常因duration计算偏差或网络抖动导致误判,例如currentTime永远差零点几秒没追上duration。
另一个误区是以为play函数的Promise会在播放结束时resolve。实际上它只在播放“开始”阶段 settle,和结束毫无关系。下面是一段错误示范:
const video = document.querySelector('video');
// 错误:认为then里是播完
video.play().then(() => {
console.log('这里只是开始播放,不是结束');
});
正确做法永远是监听ended事件或观察ended属性。若一定要用属性轮询,也应在ended为true时停止,且仅作为降级方案。此外,在微信等内置浏览器中,部分版本对ended事件支持有延迟,可同时监听timeupdate做兜底,但优先信任ended。
| 检测方式 | 准确性 | 性能 | 适用场景 |
|---|---|---|---|
| ended事件 | 高 | 优 | 绝大多数正常播放结束 |
| currentTime对比 | 低 | 差 | 特殊兼容或调试 |
| ended属性轮询 | 中 | 中 | 不支持事件时的降级 |
综合来看,play函数只管“开场”,结束检测请交给ended事件。理清这条边界,音视频播放控制会稳健很多。