H5-Video 实践
如何更快速的跟进H5播放器相关业务迭代
奇舞团
胡尊杰
Video 的应用
- 最基本的 HTML 标签
- 常用媒体容器格式
- 用video JS-API实现RPG
- 如何播放本地媒体文件
- 摄像头媒体流的采集应用
- 使用JS生成动态媒体流
最基本的video标签应用
<video src="https://chimee.org/vod/1.mp4" controls>
您的浏览器不支持Video标签。
</video>
video 标签效果
使用source标签给video设置多个备用媒体资源
<video controls>
<source src="https://chimee.org/vod/2.webm">
<source src="https://chimee.org/vod/2.ogg">
<source src="https://chimee.org/vod/2.mp4">
<!--
<source src='video.mp4' type='video/mp4; codecs="mp4v.20.8, mp4a.40.2"'>
<source src='video.mp4' type='video/mp4; codecs="mp4v.20.240, mp4a.40.2"'>
-->
<p>当前环境不支持video标签。</p>
</video>
video 标签效果
H5视频容器格式
- MP4
- OGG
- WebM
- M3U8
- ...
媒体容器类型兼容判断
let videoEl = document.createElement("video");
// 是否支持 MP4
videoEl.canPlayType('video/mp4') !== '';
// 是否支持 MP4 & 特定编码的
videoEl.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') !== '';
// 是否支持 webm & 特定编码的
videoEl.canPlayType('video/webm; codecs="vp8, vorbis"') !== '';
// 是否支持 ogg & 特定编码的
videoEl.canPlayType('video/ogg; codecs="theora, vorbis"') !== '';
// 是否支持 HLS 的 m3u8
videoEl.canPlayType('application/vnd.apple.mpegURL') !== '';
// 是否支持 HLS 的 TS 切片
videoEl.canPlayType('video/mp2t; codecs="avc1.42E01E,mp4a.40.2"') !== '';
使用 canPlayType 判断容器类型兼容性
Video 交互
- 通过 JS API 进行状态修改
- 通过事件监听响应状态的变化
一个基于video交互实现的 RPG 广告片(片段)
播放本地媒体文件
let iptFileEl = document.querySelector('input[type="file"]');
let videoEl = document.querySelector('video');
iptFileEl.onchange = e =>{
let file = iptFileEl.files && iptFileEl.files[0];
playFile(file);
};
function playFile(file){
if(file){
let fileReader = new FileReader();
fileReader.onload = evt => {
if(FileReader.DONE == fileReader.readyState){
videoEl.src = fileReader.result;
}else{
console.log('FileReader Error:', evt);
}
}
fileReader.readAsDataURL(file);
}else{
videoEl.src = '';
}
}
基于 FileReader API 播放本地文件
一个线上应用场景:https://gif.75team.com/
采集并播放摄像头视频流
navigator.getUserMedia(
{ audio: false, video: true},
function(stream) {
let video = document.querySelector('video');
video.srcObject = stream;
video.onloadedmetadata = () => video.play();
window.streamTrack = stream.getTracks()[0];
},
function(err) {
alert('getUserMedia error: ' + err.message);
}
);
基于 getUserMedia API 播放摄像头视频流
配合 canvas 实现人脸识别
配合 MediaRecorder 实现视频录制
使用由JS动态创建的媒体源
var video = document.querySelector('video');
var mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', function() {
var sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E, mp4a.40.2"');
fetchAB('https://nickdesaulniers.github.io/netfix/demo/frag_bunny.mp4', function (buf) {
sourceBuffer.addEventListener('updateend', function () {
mediaSource.endOfStream();
video.play();
});
sourceBuffer.appendBuffer(buf);
});
});
function fetchAB (url, cb) {
var xhr = new XMLHttpRequest();
xhr.open('get', url);
xhr.responseType = 'arraybuffer';
xhr.onload = function () { cb(xhr.response) };
xhr.send();
};
未来很美好
Web前端可以做更多更有趣的事情了诶~
现状还比较悲催
无法避免的环境差异
- UI 不统一
- API的实现与支持程度
- 事件交互行为不一致
- 媒体格式的支持不同
各环境 UI 不统一
各环境API实现与支持程度的差异
- controls 不一定有效
- poster 设置未必有效
- autoplay 不一定有效
-
部分环境劫持 video playsinline webkit-playsinline="true" x-webkit-airplay="true" x5-video-player-type="h5"
- 可能不认识source标签
- VideoElement.error 未必支持
- 不支持无缝切源...
事件交互不一致
- 进度变化相关事件触发频率不同
- 部分事件触发时相应状态值未必可靠
- 部分场景缺少事件
- seek时不一定触发play
MediaEvents&API 检测:https://www.w3.org/2010/05/video/mediaevents.html
媒体格式的支持无法保证
- MP4 支持较好
- M3U8 的兼容问题
- 现存大量 flv 播放需求
- 系统层面不同
媒体播放开发的业务问题
- 状态处理容易存在冲突
- 交互与层级管理的矛盾
- 日志收集上报易耦合
状态处理容易存在冲突
资源争夺可能带来未知的BUG
交互与层级管理的矛盾
日志收集上报易耦合
如果已经实现了业务支持
后续迭代优化,在业务代码中插入收集逻辑?
让琐碎问题的影响最小化
- 环境差异问题无法避免
- 技术依然在不停的前进
- 尽可能避免重复趟过浑水
一套更理想的解决方案
- 统一的 API 与事件行为
- 有可控的状态管理机制
- UI 表现一致并支持自定义
- 插件解耦,支持热插拔
- 兼容主流业务媒体资源
- 模块化适应更多开发场景
Chimee
一套基于video实现的H5多容器兼容播放器
Chimee 的模块化分层设计
import ChimeePlayer from 'chimee-player';
new ChimeePlayer({
wrapper: '.chimee-container',
src: 'https://chimee.org/vod/1.mp4',
controls: true
});
// 直播
new ChimeePlayer({
wrapper: '#wrapper',
src: 'http://chimee.org/xxx/fff.flv',
box: 'flv',
isLive: true,
autoplay: true,
controls: true
});
ChimeePlayer 的快速应用
chimee-player 默认包含 flv、hls 解码器,controlbar、center-state、contextmenu、log 插件
ChimeePlayer 应用
向播放器实例中的插件传参
new ChimeePlayer({
wrapper: '#wrapper', // video dom容器
src: 'http://chimee.org/vod/1.mp4',
autoplay: true,
controls: true,
plugins: [{
name: 'chimeeLog',
// 告诉 chimeeLog 插件你有一个可以接受日志上报的服务端接口
logPostUrl: 'https://myDomain.xx/log_push'
}]
});
使用自定义插件
import ChimeePlayer from 'chimee-player';
// 基于 popup 工厂方法灵活控制插件展示位置
var aggdPlugin = ChimeePlayer.popupFactory({
name: 'my-plugin',
className: 'css-cls',
title: false,
body: '<em>广告示例</em>',
offset: '0px 10px auto auto',
operable: false
});
ChimeePlayer.install(aggdPlugin);
var player = new ChimeePlayer({
wrapper: '.chimee-container',
src: 'http://chimee.org/vod/1.mp4',
isLive: false,
autoplay: false,
controls: true,
plugin: [aggdPlugin.name]
});
自定义插件示例
编写一个复杂的插件
const plugin = {
// 插件名为 controller
name: 'controller',
// 插件实体为按钮
el: '<button>play</button>',
data: {
text: 'play'
},
methods: {
changeVideoStatus () {
this[this.text]();
},
changeButtonText (text) {
this.text = text;
this.$dom.innerText = this.text;
}
},
// 在插件创建的阶段,我们为插件绑定事件。
create () {
this.$dom.addEventListener('click', this.changeVideoStatus);
// 可以 watch 监听当前插件或video上的属性变化
this.$watch('controls', (newVal, oldVal) => console.log(newVal, oldVal));
},
// 插件会在播放暂停操作发生后改变自己的文案及相应的行为
events: {
pause () {
this.changeButtonText('play');
},
// 视频播放钩子,如果返回的是 false 或者 Promise.reject(), 则事件被阻截。
// 如果返回的是处于 pending 状态的 Promise, 则可以理解为事件被挂起。
beforePlay () { },
play () {
this.changeButtonText('pause');
}
},
computed: {
type_str(){
return this.$videoConfig.isLive?'live':'vod';
}
}
};
更多功能用法,参见 plugin-API
基于 chimee 为业务做定制化开发
import Chimee from 'chimee';
import chimeeControl from 'chimee-plugin-controlbar';
import chimeeCenterState from 'chimee-plugin-center-state';
import chimeeContextmenu from 'chimee-plugin-contextmenu';
import chimeeLog from 'chimee-plugin-log';
import popupFactory from 'chimee-plugin-popup';
import chimeeKernelHls from 'chimee-kernel-hls';
import {isObject, isArray} from 'chimee-helper';
import './index.css';
// 为播放器增加“水滴直播”台标
const shuidiLogo = popupFactory({
name: 'shuidiLogo',
className: 'shuidi-logo',
title: false,
body: '<a href="http://jia.360.cn/pc" target="_blank"></a>',
offset: '5px',
create () {
const $logo = this.$domWrap.find('a');
const {topicid, sn, channel} = this.$videoConfig;
// 有在实例化时候传入台号,则在台标处展示之
channel && $logo.text(channel + ' 台').addClass('sd-channel');
// 为台标增加链接
const idQuery = topicid ? `topicid=${topicid}` : sn ? `sn=${sn}` : '';
idQuery !== '' && $logo.attr('href', 'http://jia.360.cn/pc/view.html?' + idQuery);
}
});
Chimee.install(chimeeControl);
Chimee.install(chimeeCenterState);
Chimee.install(chimeeContextmenu);
Chimee.install(chimeeLog);
Chimee.install(shuidiLogo);
class ShuiDiPlayer extends Chimee {
constructor (config) {
if(!isObject(config)) throw new TypeError('You must pass an Object as config when you new ShuiDiPlayer');
if(!isArray(config.plugin)) config.plugin = [];
const innerPlugins = [
chimeeControl.name, chimeeCenterState.name,
chimeeContextmenu.name, chimeeLog.name,
shuidiLogo.name
];
const configPluginNames = config.plugin.map(item => isObject(item) ? item.name : item);
innerPlugins.forEach(name => {
if(configPluginNames.indexOf(name) > -1) return;
config.plugin.push(name);
});
if(!isObject(config.preset)) {
config.preset = {};
}
if(!config.preset.hls) {
config.preset.hls = chimeeKernelHls;
}
super(config);
this.on('play', () => {
this.chimeeContextmenu.updatemenu([{text: '暂停', action: 'pause'}]);
});
this.on('pause', () => {
this.chimeeContextmenu.updatemenu([{text: '播放', action: 'play'}]);
});
}
}
定制效果:水滴直播
Chimee 的现状
- 发布18个独立 NPM 模块
- 已经开源到 GitHub
- 更适合 PC 端场景接入
- Flash 降级方案实现中
- 但不建议使用降级方案
谢谢
反馈
H5-Video实践 & 如何更快速的跟进H5视频播放相关业务迭代
By 胡尊杰
H5-Video实践 & 如何更快速的跟进H5视频播放相关业务迭代
- 1,548