背景

需要实现扫码开台后自动开启球桌绑定的摄像头,同步进行球桌直播,直播结束后还支持选择录播回放。其实就是将各个镜头的内容同时在一个页面内进行播放。不过直播和录播的生成都是在后端实现,前端只负责视频资源的播放。

思考

对于简单的直播场景,前端需要关注的主要是「编解码格式」「直播协议」,因为这两点直接决定直播能否播放。

「编解码格式:」 视频的编码方式决定了视频的压缩方式,同样的需要对应的解码格式才能正常播放视频。但视频编码这个过程是在推流端做的,通常会采用H.264,目前基本上所有的播放器和浏览器都支持该解码方式,其兼容性基本不用考虑;所以虽然视频的编解码格式很重要,但只要没有特殊的场景如4k,一般无需过多考虑。

「拉流端直播协议:」 不同的直播协议,其兼容性和直播效果有一些差异,而前端对兼容性的差异是敏感的,所以对于Web端,尽量选择兼容性最佳的HLS。

调研了下支付宝微信小程序的视频播放支持格式,取二者均兼容的格式,只有mp4、3gp和m3u8这三种格式能够两个平台安卓和iOS全兼容

为什么使用xgPlayer

在确定了我所需要使用的直播协议后,我调研了一些社区推荐的播放器:tcplayer.js、xgplayer、DPlayer、plyr、ArtPlayer.js、Video.js。

其实以上的播放器在功能上都可以满足我的需求,并且也都支持H.264编码格式;在直播能力的支持上也都会在底层依赖hls.js,flv.js,不过像tcplayer和xgplayer还单独包了一层,使得直播的实现更加的符合播放器的体系;

在我所调研的播放器里xgPlayer的文档是最清晰和完善的,并且xgPlayer使用插件机制,所有功能均可插拔,而且支持自定义扩展能力,十分方便,所以在开发体验上我认为更胜一筹。

基本使用

  1. 初始化
1
2
3
4
5
6
7
8
9
10
import Player from 'xgplayer'
import FlvPlugin from 'xgplayer-flv'
import "xgplayer/dist/index.min.css"

new Player({
id:'dom-id', // 播放器实例化所需的dom
url: 'test.flv', // 视频源
width: '100%',
height: '100%'
})
  1. 多实例

初始化时可以使用选择器id或容器el。但对于同时实例化多个播放器的场景,使用Id很容易导致最终只实例化成功一个,虽然可以通过ID+索引的方式避免,但使用容器el还是更为简洁的。

1
2
3
4
5
6
7
8
<div ref="playerRef"></div>

const playerRef = ref()

new Player({
el: playerRef.value,
...
})
  1. 常用属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    new Player({
    poster // 封面
    autoplay // 自动播放,基本上不支持有声自动播放;
    autoplayMuted // 自动静音播放,需要自动播放可使用改属性
    playsinline // 对于移动 Safari 浏览器来说是必需的,它允许视频播放时不强制全屏模式
    loop // 循环播放
    fitVideoSize // 根据视频内容调整容器宽高
    videoFillMode // 视频画面填充模式
    controls // 是否展示进度条
    videoAttributes // 透传给video标签的属性
    })
  2. 自定义插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// demoPlugin.js
import Vue from 'vue'
import { Events, Plugin } from 'xgplayer'
import Demo from './demoPlugin.vue'
const { POSITIONS } = Plugin

export default class DemoPlugin extends Plugin {
// 插件的名称,将作为插件实例的唯一key值
static get pluginName() {
return 'demoPlugin'
}
static get defaultConfig() {
return {
// 挂载在播放器最上方
position: POSITIONS.ROOT_TOP
}
}
constructor(args) {
super(args)
this.vm = null
}
// 插件实例化之后
afterCreate() {
// 使用vue组件
const Component = Vue.extend(Demo)
this.vm = new Component().$mount('.demo-plugin')
// 视频播放时
this.on(Events.PLAY, () => {
this.vm.hide()
})
// 视频暂停时
this.on([Events.PAUSE], () => {
this.vm.show()
})
}
destroy() {
// 播放器销毁的时候一些逻辑
}
render() {
return '<div class="demo-plugin"></div>'
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// demoPlugin.vue
<template>
<NoticeBar v-show="visible" swipe>
<span style="padding-right: 50px;">我一定会回来的~我一定会回来的~我一定会回来的~</span>
</NoticeBar>
</template>
<script setup lang="ts">
import { ref, defineExpose } from 'vue'
import { NoticeBar } from '@zz-common/zz-ui';

const visible = ref(true)

const show = () => {
visible.value = true
}
const hide = () => {
visible.value = false
}
defineExpose({
show,
hide
})
</script>
1
2
3
4
5
6
7
// 使用
import DemoPlugin from './demoPlugin.js'
...
new Player({
...,
plugins: [DemoPlugin]
})

  1. 跨域

问题的起因是当时的直播流内容总是会间隔性的黑屏,而且后端无法监控到并调整视频源。于是需要前端通过截取画面并分析截图的像素点来判断是否黑屏,以实现黑屏自动切换视频源的能力。

但是当通过canvas获取图片数据时getImageData报了一个SecurityError异常。

经查阅发现这是浏览器的安全策略,不通过CORS使用其他来源的资源,会污染画布。

在”被污染”的 canvas 中调用以下方法将会抛出安全错误:

  • 在 canvas 的上下文上调用getImageData()
  • 在canvas元素本身调用toBlob()、toDataURL()、captureStream()

所以如果要对视频内容进行截图或者对视频画面做一些操作处理,需要给video标签设置crossOrigin属性,在xgplayer中可以通过videoAttributes属性传入。

1
2
3
4
5
6
const player = new Player({
...,
videoAttributes: {
crossOrigin: 'anonymous'
}
})
  1. 内存溢出

当不需要播放器时,记得及时销毁,否则可能会导致内存溢出。(尤其是多实例、切换播放的场景)

1
2
player.destroy() // 销毁播放器
player = null // 将实例引用置空