基于xgplayer编写一个artc插件,实现阿里云低延迟直播
故事的开始
最近一直在配合业务和集团的小伙伴处理一些直播上的需求。他们起先看上了磐厚年会的那个直播项目,里面的弹幕功能和一些播放功能都是他们觉得符合他们诉求的内容。
矛盾的开始
简单点就是,需求方需要使用阿里云的音视频的artc。o(╥﹏╥)o o(╥﹏╥)o
第一版
为了快速支持artc,写了个简易的播放的js。
<template>
<video class="artc"
autoplay :poster="poster" ref="artc"
></video>
</template>
<script>
import { AliRTS } from 'aliyun-rts-sdk'
const aliRts = AliRTS.createClient()
...
aliRts.subscribe(val)
.then((remoteStream) => {
// mediaElement是媒体标签audio或video
console.log('is ok', remoteStream)
remoteStream.muted = false
remoteStream.play(viodeEle);
viodeEle["disablePictureInPicture"] = true
// viodeEle.play()
}).catch((err) => {
// 订阅失败
console.error('err', err)
})
...
</script>
怎么样,是不是好像挺简单的。但是完全舍弃了之前的xgplayer的一些控制器,自定义的skin以及弹幕功能。需求方由于时间忍了,但是测试的过程中发生了很多对于播放器的问题,包括全屏,播放,poster等等问题。
- 用的纯video,在ios、android上功能上差异很大。
- 一些已经能用的skin和弹幕功能都丢失了。
- 兼容性查。
解决问题看了xgplayer的源码中,提供了很多hls.js,flv 等插件来进行m3u8、flv 的一些兼容播放。我就在想那何不我写个 artc 的插件来播放artc 的内容呢。既保留了 xgplayer 的基础特性和优势,也可以播放artc来提升直播的体验。
代码思路
分析源码的插件思路
// 插件核心代码 pluginsCall () { if(Player.plugins['s_i18n']) { Player.plugins['s_i18n'].call(this, this) } let self = this if (Player.plugins) { let ignores = this.config.ignores Object.keys(Player.plugins).forEach(name => { let descriptor = Player.plugins[name] if(!descriptor || typeof descriptor !== 'function'){ console.warn('plugin name', name , 'is invalid') } else { if (!ignores.some(item => name === item || name === 's_' + item) && name !== 's_i18n') { if (['pc', 'tablet', 'mobile'].some(type => type === name)) { if (name === sniffer.device) { setTimeout(() => { // if destroyed, skip if (!self.video) return; descriptor.call(self, self) }, 0) } } else { descriptor.call(this, this) } } } }) } } // 关键就是一句话 descriptor.call(this, this)
简单点理解就是使用了原型链的call方法,将插件中的方法给覆盖了原方法。
查看hls.js插件的源码进行重写
import Player from '../index' import Hls from 'hls.js' import utils from './utils' class HlsJsPlayer extends Player { ... player.once('complete', () => { if(player.config.isLive) { util.addClass(player.root, 'xgplayer-is-live') if(!util.findDom(player.controls, '.xgplayer-live')) { const live = util.createDom('xg-live', player.lang.LIVE || '正在直播', {}, 'xgplayer-live') player.controls.appendChild(live) } } }) // 第一段核心代码,当完成加载后的处理,live机制 this.start = () => { console.log('start hls') if(!window.XgVideoProxy) { this.root.insertBefore(this.video, this.root.firstChild) } setTimeout(() => { this.emit('complete') if(this.danmu && typeof this.danmu.resize === 'function') { this.danmu.resize() } }, 1) } // 第二段核心代码, 重写了start 方法 Object.defineProperty(player, 'src', { get () { return player.currentSrc }, set (url) { util.removeClass(player.root, 'xgplayer-is-live') const liveDom = document.querySelector('.xgplayer-live') if (liveDom) { liveDom.parentNode.removeChild(liveDom) } // player.config.url = url player.autoplay = true const paused = player.paused if (!paused) { player.pause() } player.hls.stopLoad() player.hls.detachMedia() player.hls.destroy() player.hls = new Hls(player.hlsOpts) player.register(url) player.once('canplay', () => { player.play().catch(err => {}) }) player.hls.loadSource(url) player.hls.attachMedia(player.video) }, configurable: true }) // 第三方核心代码,通过defineproerty,监听了src 并重写了 set 后的执行逻辑 ... }
编写artc.js 插件
import Player from '../index' // 1. 引入 阿里云 rts-sdk import { AliRTS } from 'aliyun-rts-sdk' import utils from './utils' // 2. 默认属性定义 const PLAY_EVENT = { CANPLAY: "canplay", WAITING: "waiting", PLAYING: "playing" } // 3. 定义一个新的class name class ArtcPlayer extends Player { constructor (options) { super(options) let util = Player.util let player = this // 4. 构造函数的时候创建rts对象 let aliRts = AliRTS.createClient() this.aliRts = aliRts; this.videoSub; player.once('complete', () => { if(player.config.isLive) { util.addClass(player.root, 'xgplayer-is-live') if(!util.findDom(player.controls, '.xgplayer-live')) { const live = util.createDom('xg-live', player.lang.LIVE || '正在直播', {}, 'xgplayer-live') player.controls.appendChild(live) } } }) this.browser = utils.getBrowserVersion() // 6. 加入兼容性检查 /** * isSupport检测是否可用 * @param {Object} supportInfo 检测信息 * @param {Boolean} supportInfo.isReceiveVideo 是否拉视频流 * @return {Promise} */ this.aliRts.isSupport(Player.sniffer.device).then(re=> { // 可用 console.log('support', re) }).catch(err=> { // 不可用 console.error(`not support errorCode: ${err.errorCode}`); console.error(`not support message: ${err.message}`); return }) this._start = this.start // 7. 改写 start 方法 this.start = () => { if(!window.XgVideoProxy) { this.root.insertBefore(this.video, this.root.firstChild) this.video.play() } setTimeout(() => { this.emit('complete') if(this.danmu && typeof this.danmu.resize === 'function') { this.danmu.resize() } }, 1) } // 8. 改写 src set方法 Object.defineProperty(player, 'src', { get () { return player.currentSrc }, set (url) { util.removeClass(player.root, 'xgplayer-is-live') const liveDom = document.querySelector('.xgplayer-live') if (liveDom) { liveDom.parentNode.removeChild(liveDom) } player.autoplay = true const paused = player.paused if (!paused) { player.pause() } player.register(url) player.once('canplay', () => { player.play().catch(err => {}) }) }, configurable: true }) this.register(this.config.url) this.once('complete', () => { console.log('????complete', this.video, player) if(!player.config.videoInit) { player.once('canplay', () => { player.play().catch(err => {}) }) } }) this.once('destroy', () => { // @todo }) } switchURL (url) { const player = this player.url = url player.config.url = url let curTime = player.currentTime // player.video.muted = true Player.util.addClass(player.root, 'xgplayer-is-enter') player.once('playing', function(){ Player.util.removeClass(player.root, 'xgplayer-is-enter') // player.video.muted = false }) player.once('canplay', function () { player.currentTime = curTime player.play() }) player.src = url } // 9. 重写 register videoUrl 方法, 重要~~~ register (url) { let util = Player.util let player = this this.videoSub = this.aliRts.subscribe(url) this.videoSub.then(stream => { stream.play(this.video) }).catch(err => { console.log(`error: ${err}`); }) this.aliRts.on('onPLayEvent', play => { switch (play.event) { case PLAY_EVENT.CANPLAY: console.log('canplay') player.play() break; case PLAY_EVENT.WAITING: console.log('WAITING') break; case PLAY_EVENT.PLAYING: console.log('canplay') util.addClass(player.root, 'xgplayer-is-live') if(!util.findDom(player.root, '.xgplayer-live')) { const live = util.createDom('xg-live', player.lang.LIVE || '正在直播', {}, 'xgplayer-live') player.controls.appendChild(live) } break; default: break; } }) } destroy() { super.destroy(); } } export default ArtcPlayer
测试可用性
import '../player' import ArtcPlayer from '../player/artc' this.player = new ArtcPlayer({ id: 'mse', url: this.source, isLive: true, poster: this.poster, width: '100%', height: '100%', autoplay: true, videoInit: true, playsinline: true, 'x5-video-player-type': 'h5', 'x5-video-player-fullscreen': false, danmu: { area: { //弹幕显示区域 start: 0.05, //区域顶部到播放器顶部所占播放器高度的比例 end: .95 //区域底部到播放器顶部所占播放器高度的比例 }, }, })
// 和之前的 hls 的使用方法一模一样,并且可完美运行。nice
成果
感动,控制器,弹幕的功能都完美和之前的hls播放的时候一模一样。
故事的结尾
这次的2022~05-27 的直播应该就会用上改造后的artc 播放器。其实看完的同学可以发现,其实源码的阅读和插件的编写其实没想的那么复杂和艰难。要敢于尝试才会有新的发现。
Xgplayer
xgplayer 的 设计思路和结构都是很不错的,非常值得借鉴和学习。可以使用ts 进行重构,实现一个自己的 player 库哦
转载自:https://segmentfault.com/a/1190000041992498