关于Video标签直播的思考
背景
前提:在公司直播H5SDK应用中要求直播过程中不可以暂停直播。某天用户反馈了几个bug,第一:说通过耳机听课可以暂停直播(公司不允许直播过程中暂停),第二:说在回放的时候通过耳机暂停播放时UI并没有发生变化,还是播放状态(当前斗鱼也存在这个问题),第三:在safari浏览器中会出现直播倒退的情况。
解决方案
解决第一个问题,通过耳机听课可以暂停直播:通过监听video标签的pause事件,当发生回调时候就再次调用play方法,代码如下:
var ele = document.querySelector('video');
// 当用户通过耳机暂停时,调用play方法
ele.addEventListenr('pause', function() {
ele.play();
});
解决第二个问题,回放过程中通过耳机暂停播放时UI并没有发生改变:通过监听video标签pause事件,当发生回调的时候同时调用一下SDK暴露出来的接口改变UI(这样弊端会调用两次pause()方法)。代码如下:
var ele = document.querySelector('video');
var sdk = new H5SDK();
ele.addEventListener('pause', function() {
// sdk内部暴露给外部调用的方法,调用该事件UI变为暂停
sdk.stop();
});
ele.addEventListener('play', function() {
// sdk内部暴露给外部调用的方法,调用该事件UI发变为播放
sdk.resume();
})
解决第三个问题,首先分析为什么只有在safari中直播会出现倒退情况。经过分析发现直播过程中如果流断了(网络断开或者服务端推流断了)safari浏览器内核会自己触发一个addEventListenr('pause')回调,导致我们在修改第一个bug的时候写的ele.play()执行了。那么为什么会倒退呢?safari中直播流断了它认为这是一个完整回放文件然后一个完整回放文件播放到最后的时候在调用ele.play()函数时就重头播放了,所以产生了直播倒退。那么为什么只有safari中会有这种情况呢?因为chrome等浏览器直播流断开后不会触发addEventListenr('pause')回调,自然不会有问题。那么要从根本解决这个问题就要区分出来是用户主动触发暂停事件(主动暂停)还是由于用户网络或者服务端推流导致buffer不足(被动暂停)导致addEventListenr('pause')回调。
主动暂停与被动暂停
我们需要写一个通用的工具类,该工具类对外提供video暂停时候是用户主动触发的还是buffer不足导致的。
该类需要注意第一点:兼容性,兼容safari以及其他浏览器。第二点:功能完善性,提供暂停时不同的事件类型(用户触发还是buffer不足)
基本思路:对于safari浏览器在buffer不足的时候总是返回-0.001xxxxx,可以粗略的理解当小于0.001的时候就是buffer不足情况。所以我们监听pause回调只要buffer<0.001就认为是buffer不足,其他情况就认为是用户主动触发。对于其他浏览器,我们通过开启定时器定期检查<a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState">Video的state</a>(这个api在safari一直返回状态是4),来确定buffer是否为空。
封装代码如下:
VideoHelp.ts
import Browser from './browser';
const Safari_Min_Buffer = 0.001;
const rState = {
HAVE_NOTHING: 0,
HAVE_METADATA: 1,
HAVE_CURRENT_DATA: 2,
HAVE_FUTURE_DATA: 3,
HAVE_ENOUGH_DATA: 4,
};
const Pause_Type = {
Self: '1', // 主动暂停
Other: '2', // 网络等因素导致暂停
}
const getBufferLength = (videoMedia) => {
const me = videoMedia;
if (!me || !me.buffered || me.buffered.length === 0) {
return 0;
}
const len = me.buffered.length;
const currentTime = me.currentTime;
const lastBufferedEnd = me.buffered.end(len - 1);
// console.log((lastBufferedEnd - currentTime)); -0.0012346326666663465
return parseFloat((lastBufferedEnd - currentTime).toFixed(3)); // -0.001
};
class VideoHelp {
private ele: HTMLVideoElement | HTMLAudioElement;
private pauseCb: Function;
private bufferEmpty: boolean = false;
constructor(ele: HTMLVideoElement, pauseCb: Function) {
this.ele = ele;
this.pauseCb = pauseCb;
this.ele.addEventListener('pause', this.handleVideoPause.bind(this));
this.ele.addEventListener('canplay', this.handleCanplay.bind(this));
this.checkBufferEmpty();
}
private handleCanplay() {
this.bufferEmpty = false;
}
private checkBufferEmpty() {
if (!this.bufferEmpty && (this.ele.readyState <= rState.HAVE_CURRENT_DATA)) {
this.bufferEmpty = true;
this.pauseCb(Pause_Type.Other);
}
setTimeout(this.checkBufferEmpty.bind(this), 50);
}
private handleVideoPause() {
if (Browser.safari) {
const buffer = getBufferLength(this.ele);
console.log(buffer);
buffer <= Safari_Min_Buffer ? this.pauseCb(Pause_Type.Other) : this.pauseCb(Pause_Type.Self);
} else {
this.pauseCb(Pause_Type.Self);
}
}
}
export default VideoHelp;
外部调用过程
import VideoHelp from './video-help.ts'
const ele = document.getElementById('video');
const vh = new VideoHelp(ele, function(type) {
// type为1时候是主动暂停
// type为2时候是buffer不足暂停
console.log(type);
});
摘自flv.js的浏览器判断类
/*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <xqq@xqq.im>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
let Browser = {};
function detect() {
// modified from jquery-browser-plugin
let ua = self.navigator.userAgent.toLowerCase();
let match = /(edge)\/([\w.]+)/.exec(ua) ||
/(opr)[\/]([\w.]+)/.exec(ua) ||
/(chrome)[ \/]([\w.]+)/.exec(ua) ||
/(iemobile)[\/]([\w.]+)/.exec(ua) ||
/(version)(applewebkit)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+).*(version)[ \/]([\w.]+).*(safari)[ \/]([\w.]+)/.exec(ua) ||
/(webkit)[ \/]([\w.]+)/.exec(ua) ||
/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
/(msie) ([\w.]+)/.exec(ua) ||
ua.indexOf('trident') >= 0 && /(rv)(?::| )([\w.]+)/.exec(ua) ||
ua.indexOf('compatible') < 0 && /(firefox)[ \/]([\w.]+)/.exec(ua) ||
[];
let platform_match = /(ipad)/.exec(ua) ||
/(ipod)/.exec(ua) ||
/(windows phone)/.exec(ua) ||
/(iphone)/.exec(ua) ||
/(kindle)/.exec(ua) ||
/(android)/.exec(ua) ||
/(windows)/.exec(ua) ||
/(mac)/.exec(ua) ||
/(linux)/.exec(ua) ||
/(cros)/.exec(ua) ||
[];
let matched = {
browser: match[5] || match[3] || match[1] || '',
version: match[2] || match[4] || '0',
majorVersion: match[4] || match[2] || '0',
platform: platform_match[0] || ''
};
let browser = {};
if (matched.browser) {
browser[matched.browser] = true;
let versionArray = matched.majorVersion.split('.');
browser.version = {
major: parseInt(matched.majorVersion, 10),
string: matched.version
};
if (versionArray.length > 1) {
browser.version.minor = parseInt(versionArray[1], 10);
}
if (versionArray.length > 2) {
browser.version.build = parseInt(versionArray[2], 10);
}
}
if (matched.platform) {
browser[matched.platform] = true;
}
if (browser.chrome || browser.opr || browser.safari) {
browser.webkit = true;
}
// MSIE. IE11 has 'rv' identifer
if (browser.rv || browser.iemobile) {
if (browser.rv) {
delete browser.rv;
}
let msie = 'msie';
matched.browser = msie;
browser[msie] = true;
}
// Microsoft Edge
if (browser.edge) {
delete browser.edge;
let msedge = 'msedge';
matched.browser = msedge;
browser[msedge] = true;
}
// Opera 15+
if (browser.opr) {
let opera = 'opera';
matched.browser = opera;
browser[opera] = true;
}
// Stock android browsers are marked as Safari
if (browser.safari && browser.android) {
let android = 'android';
matched.browser = android;
browser[android] = true;
}
browser.name = matched.browser;
browser.platform = matched.platform;
for (let key in Browser) {
if (Browser.hasOwnProperty(key)) {
delete Browser[key];
}
}
Object.assign(Browser, browser);
}
detect();
export default Browser;
总结
- 分析了如果在直播过程中不允许用户暂停问题
- 分析了用户通过耳机控制暂停播放事件时同步给UI解决方案
- 提供了区分用户主动暂停和被动暂停解决问题思路