基于xgplayer编写一个artc插件

318 阅读4分钟

故事的开始

最近一直在配合业务和集团的小伙伴处理一些直播上的需求。他们起先看上了磐厚年会的那个直播项目,里面的弹幕功能和一些播放功能都是他们觉得符合他们诉求的内容。

矛盾的开始

简单点就是,需求方需要使用阿里云的音视频的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来提升直播的体验。

代码思路

  1. 分析源码的插件思路

    // 插件核心代码 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方法,将插件中的方法给覆盖了原方法。

  1. 查看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 后的执行逻辑 ... }

  2. 编写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

  3. 测试可用性

    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 的 设计思路和结构都是很不错的,非常值得借鉴和学习。可以使用ts 进行重构,实现一个自己的 player 库哦