如何實現相容 PC 和微信 H5 的全屏播放小視訊

丁香園F2E發表於2018-02-01

對於這個問題,其實網上已經有一些比較好的實踐,但有時候並不明白為什麼這樣配置;如果你想要知道一個弱小,可憐又無助的 <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));
});
複製程式碼

參考:

作者:丁香醫生團隊 顧重

相關文章