基于 HTML Video 元素的 Web 播放器,通常需要在播放卡顿时呈现加载中的交互。它的代码实现可能是这样的:
video.addEventListener('waiting', function() {
console.info('show loading');
}, false);
video.addEventListener('playing', function() {
console.info('hide loading');
}, false);然而,这个方案是不可靠的,在移动设备播放 HLS(M3U8)直播流的场景下会有诸多问题:
- 部分机型或浏览器在缓冲视频时不会触发 waiting,恢复播放后不会触发 playing。
- 部分机型或浏览器在播放尚未恢复时就触发了 playing。
于是,就有了基于 timeupdate 事件的改良方案:
let timer;
function onTimeUpdate() {
if (timer) { clearTimeout(timer); }
console.info('hide loading');
timer = setTimeout(function() {
if (!video.paused) {
console.info('show loading');
}
}, 1000);
}
video.addEventListener('timeupdate', onTimeUpdate, false);只要视频在播放,timeupdate 事件就会不断触发,从而清理上一次回调时创建的定时器,通过定时器设定的函数就不会执行。反之,只要 1 秒内没有触发 timeupdate 事件,通过定时器设定的函数就会执行,从而显示加载中的交互。
这个方案在大部分情况下是适用的。然而,在后来的一次通过代理进行的限速测试中发现:在某些 iOS 版本中,即使直播卡顿,timeupdate 仍在继续触发,从而导致加载中的交互没有显示。苦恼之际,我发现了 requestVideoFrameCallback。
requestVideoFrameCallback
requestVideoFrameCallback 是 HTML Video 元素的方法,它可以注册一个回调函数。该回调函数在一个新的视频帧发送到合成器时执行。
function callback() {
console.info('requestVideoFrameCallback');
}
video.requestVideoFrameCallback(callback);视频播放过程中,会不断产生新的视频帧。不过,通过 requestVideoFrameCallback 注册的回调函数只会触发一次。如果希望回调函数不断执行,就要不断注册。
function callbackAndRegisterNext(isFirst) {
console.info('requestVideoFrameCallback');
video.requestVideoFrameCallback(callbackAndRegisterNext);
}
video.requestVideoFrameCallback(callbackAndRegisterNext);讲到这里,我们可以发现,与 timeupdate 方案的原理类似,通过在 requestVideoFrameCallback 注册的回调函数中设定定时执行的函数,也可以判断视频是否正在播放,从而显示或隐藏加载中的交互。
function checkPlayingByRVFC() {
onTimeUpdate();
video.requestVideoFrameCallback(checkPlaying);
}
checkPlayingByRVFC();然而,requestVideoFrameCallback 的兼容性相对较差,比如 iOS 最低支持版本是 15.4。甚至有些浏览器虽然表面上支持这个接口,但通过它注册的回调根本不会执行。因此,使用 requestVideoFrameCallback 方案前要先检查兼容性:
function canUseRVFC(video, cb) {
if (video.requestVideoFrameCallback) {
video.requestVideoFrameCallback(function() {
// 触发过一次即为支持
cb(true);
});
} else {
cb(false);
}
}
video.addEventListener('timeupdate', onTimeUpdate, false);
canUseRVFC(video, function(result) {
if (result) {
// 支持 requesVideoFrameCallback 就不需要用 timeupdate 方案了
video.removeEventListener('timeupdate', onTimeUpdate, false);
checkPlayingByRVFC();
}
});直播结束的检测
在直播场景下,HTML Video 元素的 ended 事件是不会触发的,这就需要开发者以其他方式去判断直播是否结束。
常用的方法是在后端维护直播状态,开播端开播时将其设为直播中,开播端下播后将其设为直播结束。前端通过轮询、SSE 或 WebSocket 获取该状态。然而,考虑到兼容性,移动 Web 端通常会采用 HLS 作为直播流协议,延迟通常会达到十几甚至几十秒。也就是说,开播端下播后,观众端也需要这么长的时间,才能播完剩下的内容。由于后端维护的直播状态是实时的,如果前端收到直播状态为结束时就掐断直播,剩下的这部分内容就无法播完。
根据主流云服务厂商的表现,直播结束后的短时间内,HLS 拉流地址就会不存在,返回 404 状态码。不过终端已加载的 ts 片仍然可以继续播放,直到播放完毕,画面就会卡住,然后黑屏。因此,关键点还是在于视频是否在播放。只有后端返回的直播状态是结束,视频也没有在播放,且拉流地址不存在,才可以判定为直播结束。
let isStatusEnd;
let isPlaying;
// 检查拉流地址是否存在
// 由于拉流地址可能不会马上不存在,所以也要轮询
let videoSrcTimer;
async function checkVideoSrc() {
if (videoSrcTimer) { clearTimeout(videoSrcTimer); }
if (isStatusEnd && !isPlaying) {
// 仅需获取状态码,用 HEAD 方法请求足矣
const response = await fetch(video.src, { method: 'HEAD' });
if (response.status === 404) {
console.info('直播结束');
} else {
videoSrcTimer = setTimeout(checkStatus, 5 * 1000);
}
}
}
let rvfcTimer;
function checkEndedByRVFC() {
if (rvfcTimer) { clearTimeout(rvfcTimer); }
isPlaying = true;
rvfcTimer = setTimeout(function() {
isPlaying = false;
checkVideoSrc(); // 直播不在播放时才去检查拉流地址
}, 1000);
video.requestVideoFrameCallback(checkEndedByRVFC);
}
// 每 10s 查询一次后端的直播状态
async function checkStatus() {
const response = await fetch('后端获取直播状态的接口');
const result = await response.json();
if (result.status === 'end') {
isStatusEnd = true;
checkEndedByRVFC(); // 接口返回结束时才检查视频是否在播放
} else {
setTimeout(checkStatus, 10 * 1000);
}
}
checkStatus();
评论 (0条)