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 交互

  1. 通过 JS API 进行状态修改
  2. 通过事件监听响应状态的变化

一个基于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实现与支持程度的差异

  1. controls 不一定有效
  2. poster 设置未必有效
  3. autoplay 不一定有效
  4. 部分环境劫持 video
    playsinline webkit-playsinline="true" 
    x-webkit-airplay="true"
    x5-video-player-type="h5"
  5. 可能不认识source标签
  6. VideoElement.error 未必支持
  7. 不支持无缝切源...

事件交互不一致

  1. 进度变化相关事件触发频率不同
  2. 部分事件触发时相应状态值未必可靠
  3. 部分场景缺少事件
  4. seek时不一定触发play

媒体格式的支持无法保证

  1. MP4 支持较好
  2. M3U8 的兼容问题
  3. 现存大量 flv 播放需求
  4. 系统层面不同

媒体播放开发的业务问题

  • 状态处理容易存在冲突
  • 交互与层级管理的矛盾
  • 日志收集上报易耦合

状态处理容易存在冲突

资源争夺可能带来未知的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 默认包含 flvhls 解码器,controlbarcenter-statecontextmenulog 插件

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视频播放相关业务迭代

  • 877