如何实现兼容 PC 和微信 H5 的全屏播放小视频

7,958 阅读9分钟

对于这个问题,其实网上已经有一些比较好的实践,但有时候并不明白为什么这样配置;如果你想要知道一个弱小,可怜又无助的 <video> 如何变成丰满健壮的视频播放器,那就往下看吧!

需求

来问H5 医生端(PC & 微信)问题详情页面:

  • 支持点击播放按钮唤起播放器播放视频
  • 播放过程中,支持暂停视频、关闭视频、拖动进度条调整视频进度
  • 不区分是否处于 WiFi 环境下,需手动点击播放

踩坑之路

第一阶段

方案

  1. 使用HTML5中 <video> 标签进行视频播放
  2. 仅设置 src 属性

  1. 未播放时 DOM 宽高样式正常;但无封面,并未截取视频第一帧作为封面
  2. 点击播放后会调起系统默认播放器进行全屏播放,播放器初始界面五花八门
  3. 安卓视频播放退出后 DOM 样式错乱,会无视 css 使用视频源宽高显示
  4. 安卓在视频播放完后会追加视频推荐,并且白名单申请入口已经关闭

第二阶段

方案

  1. 增加 poster 属性,值为后端根据视频第一帧生成的封面地址
  2. 增加遮罩层及图标样式模拟播放器初始界面
  3. <video> 标签使用 <div> 容器包裹,并设置 display: none隐藏,用户点击封面时调用 videoplay() 方法进行播放
  4. 腾讯大佬强制显示视频推荐,所以暂时不做处理

  1. 隐藏 <video> 元素后微信中调用play()无反应
  2. PC 端只有声音没有图像。这是因为 PC 端不会打开专门的播放器,只会在 DOM 节点处直接播放,此时 DOM 节点未设置显示区域
  3. 只是要播放视频,响应的是 video.play() 方法,并不代表已经开始播放(会有一段缓冲过程),用户会误以为点击无效

第三阶段

方案

  1. display: none; 或者 width: 0; height: 0; 方式隐藏视频时,元素处于未激活状态,不响应 play() 方法,所以我们设置宽高为1px
  2. 设置 flag 判断环境,若在 PC 环境中,播放后将视频容器拓展为全屏大小,并增加关闭按钮,点击后暂停视频并移除拓展样式
  3. 点击封面时增加一个 loading 效果,PC 环境在视频播放时取消;微信环境则在视频暂停时取消。(在 iOS 中 play() 方法会触发播放事件,但播放器此时并未打开,而全屏播放中暂停视频并不会退出播放器。所以我们可以在视频暂停时取消 loading);

  1. iPad 及 windowsPC 版微信中同样不会新开播放器,而是直接在video标签处播放,导致有声音无图像;iPad 中通过修改 PC 的判断条件可以解决
  2. 有些安卓设备无法播放,需要安装 QQ 浏览器或 QQ 视频播放插件才可以,不过仍有一部分用户无法安装该插件或安装后还是无效

第四阶段:“最佳实践”

此时再使用原生 video 标签事件和属性,已经没办法进一步突破以上的这些坑,解决千差万别的兼容性问题了。因此,我们参考了其他的方案实现了预期的效果。

方案

PS:项目是基于Vue & scss,但该功能可以不依赖这些实现

  1. 使用各类兼容属性以及 x5 内核浏览器的扩展属性 webkit-playsinline, playsinline, x-webkit-airplay, x5-video-player-type, x5-video-player-fullscreen, x5-video-orientation 等解决不同类型设备的播放差异;
  2. 设置 flag 表明是否正在播放或正在全屏状态中
<!-- 视频容器 -->
<div class="video"
     :class="{'full-screen': isFullScreen}">
  <!-- 视频主体 -->
  <div class="video-content">
    <video :controls="isFullScreen"
           :style="isFullScreen ? {} : img.style"
           :class="img.isVertical ? 'vertical-img' : 'horizontal-img'"
           :src="img.url"
           preload="metadata"
           :poster="img.preview_pic_url"
           :ref="`video${img.id}`"
           webkit-playsinline="true"
           playsinline="true"
           x-webkit-airplay="allow"
           x5-video-player-type="h5"
           x5-video-player-fullscreen="true"
           x5-video-orientation="portraint">
      抱歉,您的浏览器不支持内嵌视频!
    </video>
  </div>
  <!-- 遮罩层,显示播放按钮;仅在待播放状态显示 -->
  <div v-if="!isFullScreen"
       class="video-mask">
    <div>
      <img src="~images/play.png" />
    </div>
  </div>
  <!-- 全屏控制按钮;仅在非安卓高版本内核中显示 -->
  <div v-else-if="!inHighTBS"
       class="video-controls">
    <span class="video-controls-close"
          @click.stop="handleVideoControls('close')">
      &times;
    </span>
  </div>
</div>

属性说明

  1. controls: 通过 flag 设置仅在播放时出现,避免初始播放状态不同
  2. style:对待播放 dom 进行绝对定位计算视频偏移量;视频显示区为正方形窗口,因此要横向及纵向视频显示区都在正中间
  3. class:设定横向视频高度或纵向视频宽度
  4. src:视频源
  5. preload:值为预加载但不阻塞;每个问题最多仅有一个视频,保证用户点击播放后立即响应,并且不阻塞其他图片附件的渲染
  6. poster:封面地址
  7. ref:在vue中获取并操作 video 元素
  8. webkit-playsinline:IOS 10中设置有效,视频播放时局域播放,不脱离文档流;可以保证播放界面与PC端一致
  9. playsinline:IOS 微信浏览器支持小窗内播放,和上一个属性一起食用可兼容几乎所有IOS设备
  10. x5-video-player-type:启用H5同层播放器,是 wechat 安卓版特性
  11. x5-video-player-fullscreen:视频播放时将会进入到全屏模式,若不设置还是会新开播放器,但尺寸为原始视口大小(视频未播放前)
  12. x5-video-orientation:控制横竖屏
  /* 外层还有其他已定位容器 */
  .video {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
    transition: all 0.3s;
    background-color: rgba(0, 0, 0, 0.5);
    &.full-screen {
      position: fixed;
      z-index: 99;
      .video-content {
        width: 100%;
        height: 100%;
        video {
          position: initial;
          &.vertical-img {
            height: 100%;
            width: auto;
            margin: 0 auto;
          }
          &.horizontal-img {
            width: 100%;
            height: auto;
            max-height: 100%;
          }
        }
      }
    }
    &-mask {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      background-color: rgba(0, 0, 0, 0.5);
      div {
        width: 100%;
        text-align: center;
        img {
          width: 30%;
          position: inherit;
        }
      }
    }
    &-controls {
      position: absolute;
      right: 5%;
      top: 5%;
      display: flex;
      align-items: center;
      z-index: 1;
      &-close {
        width: 50px;
        height: 50px;
        line-height: 50px;
        color: rgba(255, 255, 255, 0.7);
        background: rgba(0, 0, 0, 0.3);
        text-align: center;
        border-radius: 50%;
        font-size: 2rem;
        cursor: pointer;
        transition: all 0.3s;
        &:hover {
          background: rgba(0, 0, 0, 0.5);
          color: #fff;
        }
      }
    }
    &-content {
      max-width: 768px; // 限制PC端不超过768px;如要PC全屏可不做设置
      margin: 0 auto;
      display: flex;
      align-items: center;
      video {
        display: block;
        position: absolute;
        object-fit: fill;
      }
    }
  }
  .horizontal-img {
    height: 100%;
    top: 0;
  }
  .vertical-img {
    width: 100%;
    left: 0;
  }

小问题

在安卓微信中,就算加上了上面的属性,还会出现上下有黑边,不能全屏。解决给<video>加上object-fit: fill;的样式即可。

修改上线后以前报Bug的用户纷纷反馈好了,没问题,但...

还报Bug?

有一个用户反馈使用锤子的坚果Pro点击视频无反应,找来同型号测试机一番骚操作后...愣是没复现!

之后通过和用户不断沟通发现该设备上居然未使用X5内核浏览器(使用微信打开debugtbs.qq.com可调试X5内核,未安装会有提示)

因此在下面一个安卓兼容性事件判断上报错了,使用try { } catch (e) {}包一下,同样可以正常播放,但这是的播放效果已无法统一。

安卓事件兼容
// 高版本微信安卓环境下会自动加上返回按钮并且点击触发退出全屏事件
// 需做未使用X5内核容错处理
inHighTBS() {
  if (inAndroid) {
      try {
         const [, currentTbsVersion] = window.navigator.userAgent.match(/TBS\/(\d+)/);

        return currentTbsVersion > '036900';
      } catch() {
          return false;
      }
  } else {
    return false;
  }
}

// 安卓环境中会启用同层H5播放器,跳转新窗口,因此监听x5videoexitfullscreen事件可获取状态
// https://x5.tencent.com/tbs/guide/video.html
this.inHighTBS && vDom.addEventListener('x5videoexitfullscreen', () => {
  this.isFullScreen = false;
});

附录

video原生支持事件

const mediaProperties = [
  'loadstart',  // 在媒体开始加载时触发。
  'progress',   // 告知媒体相关部分的下载进度时周期性地触发。有关媒体当前已下载总计的信息可以在元素的buffered属性中获取到。
  'suspend',    // 在媒体资源加载终止时触发,这可能是因为下载已完成或因为其他原因暂停。
  'abort',  //  在播放被终止时触发,例如, 当播放中的视频重新开始播放时会触发这个事件。
  'error',  // 在发生错误时触发。元素的error属性会包含更多信息。参阅Error handling获得详细信息。
  'emptied',    // 媒体被清空(初始化)时触发。
  'stalled',    // 在尝试获取媒体数据,但数据不可用时触发。
  'loadedmetadata', // 媒体的元数据已经加载完毕,现在所有的属性包含了它们应有的有效信息。
  'loadeddata', // 媒体的第一帧已经加载完毕。
  'canplay',    // 在媒体数据已经有足够的数据(至少播放数帧)可供播放时触发。这个事件对应CAN_PLAY的readyState。
  'canplaythrough', // 在媒体的readyState变为CAN_PLAY_THROUGH时触发,表明媒体可以在保持当前的下载速度的情况下不被中断地播放完毕。注意:手动设置currentTime会使得firefox触发一次canplaythrough事件,其他浏览器或许不会如此。
  'playing',    // 在媒体开始播放时触发(不论是初次播放、在暂停后恢复、或是在结束后重新开始)。
  'waiting',    // 在一个待执行的操作(如回放)因等待另一个操作(如跳跃或下载)被延迟时触发。
  'seeking',    // 在跳跃操作开始时触发。
  'seeked', // 在跳跃操作完成时触发。
  'ended',  // 播放结束时触发。
  'durationchange', // 元信息已载入或已改变,表明媒体的长度发生了改变。例如,在媒体已被加载足够的长度从而得知总长度时会触发这个事件。
  'timeupdate', // 元素的currentTime属性表示的时间已经改变。
  'play',   // 在媒体回放被暂停后再次开始时触发。即,在一次暂停事件后恢复媒体回放。
  'pause',  // 播放暂停时触发。
  'ratechange', // 在回放速率变化时触发。
  'resize',
  'volumechange',   // 在音频音量改变时触发(既可以是volume属性改变,也可以是muted属性改变).。
  'mozaudioavailable'   // 当音频数据缓存并交给音频层处理时
 ];

mediaProperties.forEach(item => {
  vDom.addEventListener(item, e => console.log(item));
});

参考:

作者:丁香医生团队 顾重