这个月被「视频播放」坑惨了,曝光八大坑

2022-05-13 13:12:17 浏览数 (2)

工作压力大,听一首江南缓解下情绪~

前言

大家肯定会很奇怪我为什么要写前端的东西?因为我是一名全栈攻城狮,是不是该贡献点前端的实战经验?

一个月前我们的一个在线教育项目需要添加一个视频专区,我们采用了小程序的视频播放组件,其中遇到了很多坑,兜兜转转我盘了它一个月,终于上线了,必须将最佳实践和避坑指南分享给大家。

本文目录

一、video 组件使用

1.1 引入组件

1.2 属性用法

1.3 绑定事件

1.4 API 使用

二、小程序视频业务分享

2.1 瀑布式布局

2.2 视频权限的交互

2.3 视频播放的网络交互

2.4 全屏横屏播放

2.5 视频播放业务处理

2.6 视频 URL 过期处理

三、八大坑

一、video 组件使用

1.1 引入组件

当我们要使用小程序 video 组件的时候,我们要先引入 video 组件,引入 video 组件的代码如下,该组件的属性和事件有 39 个,引入的代码里面我就不贴了,实在太长了,文中也会一一讲到。

代码语言:javascript复制
<video id="videoPlayer"/>

1.2 属性用法

首先我会给大家分享 video 组件中控件显示和隐藏的属性,如下图:

属性对应屏幕中的元素

上图是视频全屏之后的截图,我在上面标出了各个功能组件的控制属性,下面我来说说各个属性的作用和注意的地方。

duration: 类型为 number;指定视频时长(最小单位为 秒),非必填,如果不引用该属性则小程序组件会自动识别视频的时长,设置则显示指定时长。这里我们需要注意的是,如果我们设置的 duration 的值小于视频的实际时长的话会出现下面这种情况:

配置小于时长

我们会发现就算播放进度条已经 100%,视频还是会继续播放,直到视频播放完毕。反之,则会出现视频播放完毕,进度条没有拉满的情况。

show-progress: 该属性是用来控制播放进度条显示,类型为 boolean;默认为 true。视频宽度大于 240px 才会显示进度条,反之不显示。下图为宽度为 240px, 进度条不显示效果图 。

进度条不显示问题

show-mute-btn: 类型为 boolean;是否显示静音按钮;默认为 false。

mute: 类型为 boolean;是否静音播放;默认为 false。

这两个关于静音的属性使用时,建议搭配使用,因为如果只使用 show-mute-btn 这一属性的话,它显示的是一个静音了的喇叭,但是视频播放的时候是有声音的。

show-fullscreen-btn: 类型为 boolean;是否显示全屏按钮;默认为 true。

show-play-btn: 类型为 boolean;是否显示视频底部控制栏的播放按钮;默认为 true。

show-center-play-btn: 类型为 boolean;是否显示视频中间的播放按钮;默认为 true。

play-btn-position: 类型为 string;播放按钮的位置;默认为 bottom。

上面三个关于播放按钮的属性,使用时我们需要注意 show-play-btn 和 show-center-play-btn 属性是可以同时使用的,show-center-play-btn 属性是控制视频加载完之后,视频中间的播放按钮是否显示的,show-play-btn 是控制底部控制栏的播放按钮,两者控制的播放按钮是不同的。而使用 play-btn-position 属性后,show-play-btn 属性是无效的的。

show-casting-button: 类型为 boolean;显示投屏按钮。安卓在同层渲染下生效,支持 DLNA 协议;iOS 支持 AirPlay 和 DLNA 协议;默认为 false。

show-screen-lock-button: 类型为 boolean; 是否显示锁屏按钮,仅在全屏时显示,锁屏后控制栏的操作;默认为 false。

show-snapshot-button: 类型为 boolean; 是否显示截屏按钮,仅在全屏时显示;默认为 false。

controls: 类型为 Boolean ; 是否显示默认播放控件(播放/暂停按钮、播放进度、时间)非必填 默认为 true , 如果需要自定义播放控件则需要设置为 false。

这个 controls 属性的使用大家就需要注意了,虽然官方说这个组件是控制播放/暂停按钮、播放进度、时间的,但实际上它是控制所有(除了 show-screen-lock-button) 看得见的功能控件的。

danmu-list: 类型为 Array; 弹幕列表,数据声明示例:

代码语言:javascript复制
danmuList: [
    {
      text: '第 1s 出现的弹幕', // 弹幕内容
      color: '#ff0000', // 弹幕颜色
      time: 1, // 该条弹幕出现的时间
    },
    {
      text: '第 3s 出现的弹幕',
      color: '#ff00ff',
      time: 3
    }
  ],

danmu-btn: 类型为 boolean; 是否显示弹幕按钮,只在初始化时有效,不能动态变更,默认值为 false;enable-danmu: 类型为 boolean; 是否展示弹幕,只在初始化时有效,不能动态变更,默认值为 false;

在使用这三个弹幕相关的属性的时候,我们会发现与我们使用的其他播放软件不一样,使用这个组件我们只能看得我们自己发送的弹幕。其实我们只需要在发送弹幕的时候,把发送弹幕的内容按照 danmuList 的数据结构存储起来,在 video 组件渲染时赋值给 danmu-list 属性,我们就会在播放该视频时看到别人发送的弹幕哦。

autoplay: 类型为 boolean;是否自动播放;默认为 false

loop: 类型为 boolean;是否循环播放;默认为 false

initial-time: 类型为 number; 指定视频初始播放位置;默认值为 0

direction: 类型为 number; 指定视频初始播放位置;设置全屏时视频的方向,不指定则根据宽高比自动判断,该属性是用于 video 全屏后旋转的角度。它的的合法值为 0(正常竖向), 90(屏幕逆时针 90 度), -90(屏幕顺时针 90 度)

enable-progress-gesture: 类型为 boolean;是否开启控制进度的手势;默认为 true

object-fit: 类型为 string;当视频大小与 video 容器大小不一致时,视频的表现形式;默认为 contain;该属性的合法值有三种 contain(包含),fill(填充),cover(覆盖)

poster: 类型为 string;视频封面的图片网络资源地址或云文件 ID(2.3.0)。若 controls 属性值为 false 则设置 poster 无效

enable-play-gesture: 类型为 boolean;是否开启播放手势,即双击切换播放/暂停;默认为 false; 使用该属性时,最好是 play-btn-position 属性 为 bottom,因为如果 play-btn-position 属性为 center 点击一次就能 暂停/播放切换

auto-pause-if-navigate: 类型为 boolean;当跳转到本小程序的其他页面时,是否自动暂停本页面的视频播放;默认为 true

auto-pause-if-open-native: 类型为 boolean;当跳转到本小程序的其他页面时,是否自动暂停本页面的视频播放;默认为 true

vslide-gesture: 类型为 boolean;在非全屏模式下,是否开启亮度与音量调节手势(同 page-gesture);默认为 false

vslide-gesture-in-fullscreen: 类型为 boolean;在全屏模式下,是否开启亮度与音量调节手势;默认为 true

ad-unit-id: 类型为 string;视频前贴广告单元 ID;小程序管理后台新建广告的 id

picture-in-picture-mode: 类型为 string/Array;设置小窗模式:push, pop,空字符串或通过数组形式设置多种模式(如:["push", "pop"]);即 push 代表进入下一个也没时小窗,pop 是返回上一个页面时小窗。

picture-in-picture-show-progress: 类型为 boolean; 是否在小窗模式下显示播放进度;默认为 true

enable-auto-rotation: 类型为 boolean; 是否开启手机横屏时自动全屏,当系统设置开启自动旋转时生效;默认为 false。经测试该属性对 ios 手机有效,安卓手机无效。

1.3 绑定事件

从上面引入 video 组件的代码可以看出,video 组件提供了一些事件,是 video 在不同操作时触发不同的事件,来实现更多的交互。在学习这些事件的时候我把各个事件的返回结果打印了出来,它们的结构大致一样,如下图:

不同事件返回的我们所需要的值都在 detail 字段里面,不同事件返回的值如下面的代码:

timeupdate: 播放进度变化时触发,event.detail = {currentTime, duration} 。触发 频率 250ms 一次

代码语言:javascript复制
/**
 * timeupdate: {
 *   currentTime: 0.181185 // 当前播放时长 (单位:秒)
 *   duration: 881.458361999999 // 视频总时长 (单位:秒)
 * }
 */
timeupdate(e) {
  console.log('timeupdate', e.detail);
}
  • 当播放到末尾时触发 ended 事件。
  • 当开始/继续播放时触发 play 事件。
  • 当暂停播放时触发 pause 事件。
  • 视频出现缓冲时触发 waiting 事件。
  • 加载进度变化时触发 progress 事件。
  • 视频元数据加载完成时触发 loadedmetadata
  • 切换 controls 显示隐藏时触发 controlstoggle
  • 播放器进入小窗触发 enterpictureinpicture。
  • 视频进入和退出全屏时触发 screenChange
代码语言:javascript复制
screenChange:{
  fullScreen: true  // 是否全屏
  fullscreen: true  // 同上
  direction: "vertical" // vertical 为竖屏;horizontal 为横屏
}

1.4 API 使用

我们已经了解了 video 组件的属性的简单用法和功能,现在我们来了解一下微信官方提供的 video 组件的 api。

如果我们要使用 video 组件的 api,首先我们需要创建 video 上下文 VideoContext 对象,创建方法为 wx.createVideoContext(string id, Object this),string id 为 video 组件 id、Object this 为当前页面/组件实例,在页面使用时 this 可以省略,但是切记在自定义组件中 this 不可省略,否则创建无效。

代码语言:javascript复制
const videoContext = wx.createVideoContext('videoPlayer', this);
// 退出全屏
videoContext.exitFullScreen();
// 退出小窗,该方法可在任意页面调用
videoContext.exitPictureInPicture();
// 隐藏状态栏,仅在 iOS 全屏下有效
videoContext.hideStatusBar();
// 暂停视频
videoContext.pause();
// 播放视频
videoContext.play();
// 设置倍速播放  支持 0.5/0.8/1.0/1.25/1.5,2.6.3 起支持 2.0 倍速
videoContext.playbackRate(rate);
// 进入全屏。若有自定义内容需在全屏时展示,需将内容节点放置到 video 节点内
// 设置全屏时视频的方向,不指定则根据宽高比自动判断。支持 (0 正常竖向 || 90 屏幕
// 逆时针 90 度 || -90 屏幕顺时针 90 度);
videoContext.requestFullScreen({direction: 90});
// 跳转到指定位置  position 跳转到的位置,单位 s
videoContext.seek(position);
// 发送弹幕
videoContext.sendDanmu({text: '弹幕文字', color: '弹幕颜色'});
// 显示状态栏,仅在 iOS 全屏下有效
videoContext.showStatusBar();
// 停止视频
videoContext.stop();

当在一个页面使用多个 video 组件时,需要保证 video 组件 id 的唯一性。

二、小程序视频业务分享

在这个项目中,视频专区的主要包括以下几个功能:

  • 视频列表,负责展示所有的视频;
  • 视频详情,负责播放视频;
  • 我的已购视频列表;
  • 我的订单,展示用户已购买的订单。

2.1 瀑布式布局

当时做视频列表的时候,第三方的 UI 给了一个如下布局的样式给我,如下图:

甲方要求的

由于没有接触过瀑布式布局我也尝试了一些方法。首先遇到这种布局需求我最先考虑到的是使用纯 css 代码实现,当然也确实有实现瀑布式布局的 css 属性如下代码:

代码语言:javascript复制
.waterfall-layout {
  column-count: 2; // 把 div 中的文本分为多少列
  column-width: 340rpx; // 规定列宽
  column-gap: 20rpx; // 规定列间隙
  break-inside: avoid; // 在制作手机站瀑布流时候,会出现图片错乱,请使用这个属性:避免元素内部断行并产生新列;
}

这个 css 属性实现的瀑布式的布局效果如下图:

纯CSS实现方案

大家看,上下两张图的区别在哪里?

第一张图中的 2 是排在第一排的,而第二张图中的 2 是排在第二排的。第二种的实现方式并不能达到客户的要求,所以放弃这种纯 css 的布局,当时也考虑过使用 float 布局,但是这样布局其中小的模块会被大的挡住,导致布局混乱不适用于瀑布式布局。最后我采用的 js css 的布局方式实现,先将数据源分为 2 个数组,然后才有 flex 流式布局实现了瀑布式布局。

2.2 视频权限的交互

在项目里面视频分为单个视频和视频专辑(多个视频)。首先视频的是否免费播放由货架上 goods 的价格控制,免费就免费观看,价格不为 0 则考虑商品的免费标签,如果会员含有该商品的免费标签,则视频免费观看,如果视频非免费观看则考虑商品里面的视频是否免费。当然,购买的商品是可以直接播放的。

交互逻辑

通过这个视频权限分析,实现播放的几种交互如下:

  • 视频免费或者已购买该视频

视频免费或者已购买该视频

  • 视频试看

视频试看

当用户非 wifi 环境播放试看视频时,会先提示 “非 wifi 环境,请注意流量使用” 然后提示 “正在试看,立即兑换全片”。

  • 试看结束

试看结束

  • 付费视频

付费视频

  • 视频资源被删除

视频资源被删除

  • 视频已过期

这是该项目的一种特殊情况,因为这个项目是一个线上教育的视频,所以他们期望用户购买的视频课程在一年后会自动过期,所以就有了这个交互。

视频已过期

2.3 视频播放的网络交互

在视频播放的时候,我们会像其它播放软件一样,有一些网络的交互。

当用户切换到非 WIFI 网络时

当用户网络断开时

视频播放的网络交互,不仅仅是在视频渲染完后获取当前网路状态来实现交互,还需要实时监听用户的网络状态的变化,来实现对应的交互。这就得依靠微信官方提供的 wx.getNetworkType() 和 wx.onNetworkStatusChange() 接口。

当视频初始化的时候,我们需要通过 wx.getNetworkType() 接口获取当前的网路状态实现对应的交互,代码如下:

代码语言:javascript复制
wx.getNetworkType({
  success(res) {
    that.setData({ networkType: res.networkType });
  },
});

当视频在播放途中,网络发生变化,这个时候我们需要实时调用监控网络变化的接口,来实现对应的交互和业务处理。该接口返回的结果为 isConnected(是否连网)networkType(网络类型)。该接口的使用示例如下:

2.4 全屏横屏播放

在前面学习属性的时候,我们知道 enable-auto-rotation 可以实现手机横屏全屏的效果,但是这个属性对安卓手机无效,所以放弃了使用这个属性,于是选择了使用 video 组件的 videoContext.requestFullScreen 和 videoContext.exitFullScreen()api 来实现全屏和退出全屏。而要手机横屏全屏,我们则需要知道手机是否横屏了,这时候需要监听设备的方向。而微信官方也提供了这样的接口,wx.stopDeviceMotionListening() 和 wx.onDeviceMotionChange()。

其中 wx.onDeviceMotionChange()api 返回的数据正是我们用来判断手机横屏竖屏的依据,其返回的参数为 alpha、beta 和 gamma。官方声明如下:

x,y,z轴示例图

beta、gamma 可以参照 alpha 方式了解他们的方位,通过实时测试得出角度,下面代码示例中的角度是我实测出来的,大家可以做的更精确一些。

实现全屏代码示例如下:

全屏代码

2.5 视频播放业务处理

这个项目要求用户在播放了一个视频之后,再次打开该视频是会继续播放的,实现方式是使用 bindtimeupdate 这个事件来获取当前视频播放事件,缓存在本地。当再次打开这个视频的时候会获取该视频之前播放的时间,使用 init-time 属性 或者 seek() API 来使视频跳转到指定的时间,从而实现继续上次播放。

视频播放业务处理逻辑

这个是给大家分享的项目视频播放结束的处理。在这个项目中,视频分为单个视频和专辑(多个视频),如果是单个视频,播放完则考虑是否有推荐视频,有则播放推荐视频,没有则播放结束;如果是专辑,播放完单个视频后,会播放下一个视频,视频全部播放完毕则会考虑是否有推荐视频,有则播放推荐视频,没有则播放结束。其中专辑播放下一个视频是使用的 bindended 事件处理,播放结束触发该事件则刷新 video 信息。

2.6 视频 URL 过期处理

在这个项目的背景下,视频资源由第三方提供,第三方为保证视频资源的安全性,每个视频资源的 URL(视频地址) 是有时效性的,时效为 5 小时。为避免频繁请求第三方接口,我们采用 redis 存储,在有效时间内,我们会直接返回 URL,如果超出有效时间,我们则会请求第三方接口,刷新 URL。

当然,也需要处理视频在视频资源在有效时间内失效的情况。此时我们需要使用 video 组件的 binderror 属性绑定处理 error 的事件。

目前额处理为如果视频播放时失效则会去请求获取新的 URL, 如若发现新 URL 与旧的是一样的则说明视频资源在有效时间内失效了,然后直接调取不走 redis 的接口获取 URL, 若 URL 不存在则走资源不存在的交互。

三、八大坑

  • duration 属性在使用时要确保传的值和视频真实时长一致,否则会出现播放进度与实际不一致的情况;
  • show-progress 属性在使用时,不管设置的值如何,只要视频宽度小于等于 240px 则不显示进度条;
  • show-mute-btn 和 mute 建议一起使用,注意单独使用 show-mute-btn 属性时,显示的是一个静音的小喇叭,实际播放还是有声音的。
  • enable-auto-rotation: 使用该属性时,要注意该属性对安卓机无效。因为自己用的 ios 的手机,折腾了很久才发现这个问题。最后使用 api 实现的全屏播放功能。
  • 使用 bindseekcomplete 事件时,要注意当视频 seek 完毕后无法触发该事件。
  • 在自定义组件中通过 wx.createVideoContext(string id, Object this) 获取视频上下文对象时,切记别忽略 this(当前组件实例) ,否则创建无效,后面调 api 是调不通的。
  • 使用 requestFullScreen api 实现全屏需要注意该接口不受手机设备的方向锁定控制。
  • 在使用 onDeviceMotionChange 接口获取设备方向来控制手机横屏全屏时,不仅要考虑 gamma 的值,而且要考虑 beta 的值,不然在临界值的时候手机会一直全屏或退出全屏。

- END -

0 人点赞