网络日志

基于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来提升直播的体验。

代码思路

  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

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