记一次视频项目总结(by 腾讯视频iframe api)

记一次项目总结: 腾讯视频iframeAPI 的使用及踩坑指南

背景

目前业务的某平台内容呈现方式有图文、图片等形式,此次项目为增加视频的呈现。 PM同学调研后,决定使用腾讯视频来接入,其中以下两个功能点是在本次项目中耗时较长的点。

腾讯视频统一播放器是视频应用于全平台播放(电脑,手机,平板电脑,电视[Sumsang],支持点播和直播,支持自定义插件的JavaScript框架。(对内使用的简称为 js api)

1 统计用户实际播放时长

【上图截取自腾讯视频官方api文档】

2 断点续播

如果从A视频,点击进入B视频,且点击了播放,再返回A视频,从头进行播放(但记住B的播放位置) 如果从A视频,点击进入B视频,但未点击播放,再返回A视频,继续上次播放位置

前期技术调研时,发现可通过参数控制断点续播

调研后,感觉难度不大。

问题

然而进入开发之后,发现一个悲伤的事情……

对于外部,腾讯视频提供IFrame Player API方式调用(下述简称 iframe api)

1 可用接口与配置参数变少

iframe api 提供的无论是参数还是api都远少于内部使用的 js api。 也就是说,我们调研时的那几个 api 均不可用。

2 接口调用方式变化

比如想要获取当前播放视频的总时长。(我们只能用后者)

// js api
var duration = player.getDuration()
console.log(`总时长为:${duration}`)

// iframe api
player.getDuration().then(t => {
   console.log(`总时长为:${t}`)
}
复制代码

解决

统计用户实际播放时长相对来说比较好解决。断点续播走了不少弯路……

1 统计用户实际播放时长

这个功能还好,依赖于大数据的埋点统计功能,我只需要监听 player 的播放状态,控制其在播放时轮询发送埋点,暂停与结束时取消轮询即可。核心代码如下

componentDidMount () {
  // ① 初始化播放器
  this.initPlayer()
  // ② 监听 player 播放状态变更
  this.onChangePlayState()
  // ...
}

// 监听 player 播放状态变更
// -1(未开始)| 0(已结束)| 1(正在播放)| 2(已暂停)| 3(正在缓冲)
onChangePlayState () {
  const statusFn = {
    0: () => {
      // 若已结束,清空定时器
      console.log('已结束')
      this.cancelSendDigPoint()
    },
    1: () => {
      // 轮询:点击播放发送埋点,记录播放时长
      console.log('播放中')
      this.sendDigPoint()
    },
    2: () => {
      console.log('已暂停')
      //  暂停不记录当前播放时长,停止轮询
      this.cancelSendDigPoint()
    },
    3: () => {
      console.log('正在缓冲')
    }
  }
  player.on('playStateChange', (status) => {
    console.log('播放状态变更', status)
    statusFn[status] && statusFn[status]()
  })
}

// 发送埋点:统计用户实际播放时长
sendDigPoint () {}
  
// 停止发送埋点: 暂停时触发
cancelSendDigPoint () {}
复制代码

这里有 2 个细节点是要注意的

1.1 定时器的清空

由于视频播放过程中是流式缓存,播放过程中可能产生多个定时器。 故在轮询时,定时器产生的 timer 应该放到数组中,在清除定时器时再去遍历数组清空。

// 发送埋点:统计用户实际播放时长
sendDigPoint () {
  let timer = setInterval(() => {
    // 发送埋点
  }, 3000)
  // 因为视频是流式播放,播放过程中可能会产生多个 timerId。
  // 故保存在数组中,以便清空定时器时能完全清掉
  txvTimers.push(timer)
}

// 停止发送埋点: 暂停时触发
cancelSendDigPoint () {
  console.error('停止发送埋点', txvTimers)
  txvTimers && txvTimers.length && txvTimers.forEach(timer => {
    clearInterval(timer)
  })
  // clearInterval(this.timer)
  console.error('已停止 停止发送埋点')
}

复制代码

1.2 播放结束时的处理

假定我们每3秒轮询一次,视频长度为61秒,那么最后1秒将记录不到。故还需在播放结束时再强制发送一次埋点

onChangePlayState () {
  const statusFn = {
    0: () => {
      // 结束时强制发送一次埋点,将视频总时长作为参数之一发送出去
      // ...
    },
    // ...
  }
  // ... 
}
复制代码

2 断点续播

先回顾一下需求:

  • 如果从A视频,点击进入B视频,且点击了播放,再返回A视频,从头进行播放(但记住B的播放位置)
  • 如果从A视频,点击进入B视频,但未点击播放,再返回A视频,继续上次播放位置

整理初步思路如下:

  1. 监听离开当前页面前的事件(beforeunload)
  2. 在离开页面前,通过 player.getCurrentTime() 获取视频当前播放时间 seektime
  3. 将 seektime 保存在 localStorage 中
  4. 监听进入页面事件 (load), 从 localStorage 中获取当前视频的 seektime
  5. 通过 player.seek() 跳转进度到 seektime
  6. 监听视频播放状态,如果为播放中,则清空 localStorage

根据初步思路,实现如下

componentDidMount () {
  // ① 初始化播放器
  this.initPlayer()
  // ② 监听 player 播放状态变更
  this.onChangePlayState()
  // ③进入页面时:查询是否有续播时间,如有,则定位视频播放进度到该时间
  window.addEventListener('load', beforeLoad, false)
  // 离开页面前:暂停播放 => 由 ③ 可将当前播放时间存储到 localStorage 中
  window.addEventListener('beforeunload', beforeUnload, false)
}

// 监听 player 播放状态变更
// -1(未开始)| 0(已结束)| 1(正在播放)| 2(已暂停)| 3(正在缓冲)
onChangePlayState () {
  const statusFn = {
    // ...
    1: () => {
      console.log('播放中')
      // ① 只要开始播放, 就清空 localStorage 中存的所有 seektimes(@PM需求)
      STORE.remove(SEEK_TIME_KEY)
      this.sendDigPoint()
    },
    // ...
  }
  // ... 
}


// 函数方法 (getSeektimeByStore、setSeektimeToStore 略)
function beforeLoad () {
  let seektime = getSeektimeByStore(txVideoId)
  if (player && seektime) {
    player.seek(seektime)
    player.pause()
  }
}

function beforeUnload () {
  player.pause()
  player.getCurrentTime().then(t => {
    setSeektimeToStore(txVideoId, seektime)
  })
}
复制代码

2.1 问题1:视频会偶发性从头播放

分析找到原因:

视频在初始化之后,其状态刚开始是 -1 (未开始)。过一段时间后,才会完成缓冲(3) 变成暂停状态(2)。

  • 当视频的状态还是 -1 时,如果用户点击了播放按钮,将会从头播放
  • 当视频的状态变为 2 后,用户点击播放,会从 seektime(上次离开时记录的时间) 开始播放

由于我们无法控制用户的操作,且视频具体何时才会由 -1 变至 2 也无从得知。该写法是无法满足需求的。

解决方案:在监听播放器播放状态变更时,再去取一遍 seektime,强制跳转

onChangePlayState () {
  const statusFn = {
    // ...
    1: () => {
      console.log('播放中')
      let seektime = getSeektimeByStore(txVideoId)
      // 注:仅仅依靠在 componentDidMount 中的 player.seek() 方法,不能完全让视频从上次播放位置起播
      // 原因:当 player 的状态值还是 -1 时,点击播放仍会从 0 开始。
      if (seektime) {
        // 故要再 player.seek() 一次
        player.seek(seektime)
      }
      // ...
    }
    // ...
  }
  // ... 
}
复制代码

2.2 问题2: 苹果手机仍会从头播放

经排查,发现 iso 无法监听 beforeunload 事件,于是改用 pagehide 做监听

componentDidMount () {
  // ...
  window.addEventListener('load', beforeLoad, false)
  window.addEventListener('pageshow', beforeLoad, false)
  window.addEventListener('beforeunload', beforeUnload, false)
  window.addEventListener('pagehide', beforeUnload, false)
}
复制代码

然而悲伤地发现,pagehide 中的方法仍未生效

解决:降级处理。在每次轮询发送埋点时,记录当前播放时间到 localStorage 中(简称 maidianTime),在 beforeLoad 时,如果从 localStorage 中取不到 seektime,就使用 maidianTime 作为跳转进度值

// 发送埋点:统计用户实际播放时长
sendDigPoint () {
  let timer = setInterval(() => {
    // 发送埋点
    
    // 因为 ios 监听不到页面离开的事件,无法在离开页面时去存值
    // 发送埋点时,额外存一份t到 localStorage 中
    // 另存一份而不是在原来的 SEEK_TIME_KEY 中存的原因:存在 SEEK_TIME_KEY 中将无法拖动进度条
    STORE.set(txVideoId, t)
  }, 3000)
  txvTimers.push(timer)
}

function beforeLoad () {
  let seektime = getSeektimeByStore(txVideoId) || STORE.get(txVideoId)
  setSeektimeToStore(txVideoId, seektime)
  if (player && seektime) {
    player.seek(seektime)
    player.pause()
  }
}
复制代码

2.3 问题3:load 事件不一定第一时间触发

经调试又发现:我们原以为第一时间就会触发的 load 事件,并非如此。大多数情况下能立马触发,偶尔会隔好久才触发。这就导致当其还未触发时,若苹果手机用户点击了播放,会从头播放。

解决:不再监听 load 事件,直接写在 componentDidMount 中

componentDidMount () {
  // ...
  // 不再监听 load/pageshow 事件,因为监听到的时机不可控
  let seektime = getSeektimeByStore(txVideoId) || STORE.get(txVideoId)
  setSeektimeToStore(txVideoId, seektime)

  window.addEventListener('beforeunload', beforeUnload, false)
  window.addEventListener('pagehide', beforeUnload, false)
}
复制代码

反思与总结

其实还有一些其他的问题,篇幅所限,暂时写到这里。 经过这次项目,反思总结如下:

  1. 熟读文档的重要性。第三方开发文档一定要仔仔细细研读,有的 api 用法可能就是与众不同地藏在角落里(异步api的回调)。

  2. 开阔思路,如果尝试多种方案均无法达成目标,优雅降级处理也未尝不可(ios 无法监听 unbeforeload 事件,pagehide 事件中的代码也未执行,解决办法:使用上一次发送视频播放埋点的时间)

转载于:https://juejin.im/post/5c6e542f6fb9a049bd42e5fa

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值