likes
comments
collection
share

谈谈微信小程序定制视频播放器的实现和遇到的一些坑

作者站长头像
站长
· 阅读数 23

项目背景

项目需要基于原生播放器定制开发一个视频播放器,大致需求是更换一套播放器ui,新增视频倍速控制以及弹窗,视频播放中商品卡片,静音键等功能。项目的技术栈用得是 taro + react。

谈谈微信小程序定制视频播放器的实现和遇到的一些坑

开发实现

开发前看了下小程序市面上的视频类小程序,诸如腾讯视频小程序、爱奇艺小程序用的都是原生播放器。为啥这些一线视频大厂不做定制播放器开发,难道是有坑?我瞬间感觉到了不妙,后来又翻了拼多多和B站的小程序,相比来说,B站的小程序播放器基本没什么大问题,用起来也很流畅。拼多多的bug就比较多了,会不会是部分机型兼容问题呢,这里就不细说了,有兴趣的可以自己去小程序里面去看。

考察结束,想想该怎么开发吧!

首先肯定是基于原生播放器进行扩展。然后再新增一些目前需求里面需要用到的参数,下方都会是简化过的代码。

export interface IMMVideoProps extends VideoProps {
  /** 视频高度 */
  height?: string
  /** 视频宽度 */
  width?: string
  /** currentId currentId必须保持唯一,用于获取视频实例 */
  currentId: number
  /** 商品id */
  goodsId?: number
  /** 商品开始显示的时间 */
  startTime?: string
  /** 商品结束显示的时间 */
  endTime?: string
  /** 商品是否被删除 */
  delId?: number
}
const Component: FC<IMMVideoProps> = props => {
  return (
    <Video {...parentProps} />
  )
}
const MMVideo = memo(Component)
export default MMVideo

其次,看了下ui图,基本上是换了套皮,那原生的那套就不能用了。隐藏掉,更换为自己的一套ui组件,所有的组件都是机遇播放器的absolute定位,因为考虑到后面可能会遇到很多多层级间的点击捕获的穿透问题。直接加了一个 FakeMusk 的顶层遮罩,点击播放器的第一时间会第一时间触发最上层的 FakeMusk ,FakeMusk 隐藏后才允许触发其他的操作。

<Video
  {...parentProps}
  id={`video${currentId}`}
  controls={false}
>
  {/* 顶层遮罩,防止点击穿透 */}
  {FakeMusk}
  <ControlContext.Provider value={controlContextValue}>
    <Mask>
      {/* 中间播放按钮 */}
      <ControlBar.Play />
      {/* 底部控制 */}
      <ControlBar />
    </Mask>
    {/* 倍速弹窗 */}
    <SpeedPopup />
    {/* 商品卡片 */}
    <VideoGoods />
    {/* 视频正在加载的loading */}
    <MMLoading />
  </ControlContext.Provider>
</Video>

写好静态组件后,无非就是各种调用播放器的各种 api 啦。我这里的处理是让 video 的外层组件传进来一个 currentId,然后通过 currentId 去获取播放器的实例,最后把实例存到一个顶层的 state 里面,供后面的子组件使用。比较简单,代码就不赘述了。

遇到的一些问题

无刷新视频获取实例的问题

这里遇到了第一个问题。就是调用播放器组件的页面是个无刷新页面,在切换视频的时候仅变更 currentId 和 src,这种情况下,第一次确实可以拿到实例,但当变更完 currentId 后再去获取的播放器实例却无法调用再调用播放器的各种 api。

所以处理方案是必须让页面切换视频的卸载组件,这样每次的播放器组件都是重新生成的实例,这样使用起来没有什么问题。

至于为什么,大家可以一起评论区讨论一下。原因我稍后会更新出来。

播放器进度条抖动

抖动的问题之前有预料会碰到,这一次一下遇到两个抖动的问题。

第一个抖动问题是因为一开始用了小程序的 slider 组件,实测发现部分机型在拖动的时候 slider 会不停的抖。这个问题不知道其他同学遇到过没有?解决方案是重新手撸一个 slider 组件,放置在原生 slider 的上方,然后让原生 slider 的透明度为0。为什么还需要原生 slider,主要还是因为原生 slider 有现成的滑动事件 onchanging 和 滑动结束事件 onchange,这是我所需要的。

/** 手写的 slider 用来覆盖原生 slider */
<VideoSlider>
<Slider
  block-size={16}
  value={isSlide ? percentSlide : percent}
  onChange={event => onChangeHandle(event)}
  onChanging={event => onChangingHandle(event)}
/>

滑动结束后的抖动问题

onChange 事件结束后通常调用视频的 seek 方法,发现部分机型,主要是安卓,播放器的seek完成时触发的方法 onSeekComplete 有延时。这就导致 slider 的状态更新也有一定的延时,拖动一段距离必然有抖动的问题

/** 拖动结束后触发 */
const onChangeHandle = function(event) {
  const { value } = event.detail
  videoId?.seek((value / 100) * duration)
}
<Video onSeekComplete={() => {
    controlDispatch({ type: 'setIsSlide', isSlide: false })
    controlDispatch({ type: 'SetIsPlay', isPlay: true })
    {/* 这个方法的执行部分安卓机型有延时 */}
}} />

这里的解决方案是设置一个 isSlider 是否正在拖动的状态,在 isSlider 状态变为 true 之前,进度条只会获取拖动的时候进度,直到 isSlide 变为 false。并且在拖动的时候我会暂停视频的进度,在拖动结束后才开始播放视频,通过这两种方式可以有效的解决这个 onSeekComplete 触发延时的问题。

/** 视频播放进度(百分比) */
const percent = useMemo(() => {
  /** 这里的处理主要是部分安卓机器 video 的 onSeekComplete 回调事件有延时,导致 slider 快速滑动的时候滚动条出现抖动 */
  if (isSlide) {
    return percentSlide
  }
  return Number(((currentTime / duration) * 100).toFixed())
}, [currentTime, duration, isSlide, percentSlide])
/** 正在拖动时触发 */
const onChangingHandle = function(event) {
  const { value } = event.detail
  changeSlider(value)
  controlDispatch({ type: 'SetIsPlay', isPlay: false })
}

连续滑动 slider 丢失 onChange 事件

当用户不断把 slider 拖到 0 进度时,会丢失 onChange 事件,这个算是微信 slider 的一个 bug。

模拟器状态下播放器的 onSeekComplete 回调不生效

这个问题小程序一直没有修复,所以在模拟器开发时,只能手动的在 onChange 事件通过执行一遍 onSeekComplete 内部的方法。

视频格式兼容性

实际使用发现这个视频播放器对视频支持程度非常差劲,官方文档对于编码参数压根没提,各种黑屏或者无声音。于是决定记录下来处理的过程和我测试过的兼容性列表,方便未来查阅。

首先先看看官方文档是怎么说的。这里摘录出官方文档的表格。

格式iOSAndroid
mp4
movx
m4vx
3gp
avix
m3u8
webmx

支持的编码格式

格式iOSAndroid
H.264
HEVC
MPEG-4
VP9x

但是按照这个标准上传完视频后,依然会有不少视频会出现卡顿,黑屏等问题。

原因是官方文档缺少了编码中对 profile 的描述,我自己做了各种尝试,完善了一下兼容性测试。首先由于 iOS 和 Android 都支持mp4,因此我主要测试了 mp4 支持的音频和视频编码,以及相关规格,最终整理成下文的表格,测试设备为 Android 和 ios设备

视频编码profileAndroid、ios
H.264Baseline
H.264Main
H.264Highx
  • Baseline 主要用于视频通话、手机视频等;
  • Main 用于主流消费类电子产品规格如低解码(相对而言)的mp4、便携的视频播放器、PSP 和 Ipod 等;
  • High 用于广播及视频碟片存储(蓝光影片),高清电视的应用。

解决方法是将将 Format profile 调低进行转码,转到 High@L4 以下时,绝大多数 Android 或 ios 机就已经可以正常播放了。除了用客户端软件进行转码,还可以使用七牛云或者腾讯云提供的视频云转码服务,但是需要写简单的几何程序,转码完成后会提供回调通知。