記一次視頻項目總結(by 騰訊視頻iframe api)

【更新2019.6.20】播放結束後的跳轉至某時間節點繼續播放(見底部)html

記一次項目總結: 騰訊視頻iframeAPI 的使用及踩坑指南vue

注:代碼技術棧-reactreact

背景

目前業務的某平臺內容呈現方式有圖文、圖片等形式,這次項目爲增長視頻的呈現。 PM同窗調研後,決定使用騰訊視頻來接入,其中如下兩個功能點是在本次項目中耗時較長的點。ios

騰訊視頻統一播放器是視頻應用於全平臺播放(電腦,手機,平板電腦,電視[Sumsang],支持點播和直播,支持自定義插件的JavaScript框架。(對內使用的簡稱爲 js api)api

1 統計用戶實際播放時長

圖截取自騰訊視頻官方api文檔
【上圖截取自騰訊視頻官方api文檔】

2 斷點續播

若是從A視頻,點擊進入B視頻,且點擊了播放,再返回A視頻,從頭進行播放(但記住B的播放位置) 若是從A視頻,點擊進入B視頻,但未點擊播放,再返回A視頻,繼續上次播放位置數組

前期技術調研時,發現可經過參數控制斷點續播緩存

調研後,感受難度不大。框架

問題

然而進入開發以後,發現一個悲傷的事情……異步

對於外部,騰訊視頻提供IFrame Player API方式調用(下述簡稱 iframe api)ide

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 事件中的代碼也未執行,解決辦法:使用上一次發送視頻播放埋點的時間)


6.20 更新

注:如下代碼技術棧-vue

背景

點擊跳轉對應的時間點進行播放。

問題

當視頻播放完畢後,點擊選項直接執行 txplayer.seek(t), 會致使視頻一直在加載中……

解決

初始解決方案

核心代碼

修改 jumpToStamp 方法。

判斷視頻是否完整播放過一遍(hasPlayToEnd)

  • true:代表已經播放完過
  • false:代表正在播放中
/** * @method: jumpToStamp * @desc: 播放器跳轉到指定時間節點 * @param {number} timestamp 時間戳 */
jumpToStamp(timestamp) {
  console.log('** 跳轉到指定時間點 **', timestamp)
  // 還在播放過程當中:直接跳轉
  if (!this.hasPlayToEnd) {
    window.txplayer.seek(Number(timestamp))
    return
  }
 
  // 已經發送過完整播放過一次: 先從新play() 再跳轉 seek()
  // 諮詢過騰訊視頻的同窗,目前建議使用延時調用的方式處理
  window.txplayer.play(this.txVideoId)
  setTimeout(() => {
    window.txplayer.seek(Number(timestamp))
  }, 200)
},

複製代碼

修改監聽視頻播放狀態變化的方法

/** * @method: onChangePlayState * @desc: 監聽 player 播放狀態變動 * 狀態值:-1(未開始)| 0(已結束)| 1(正在播放)| 2(已暫停)| 3(正在緩衝) * @author: tangli008 */
onChangePlayState() {
  const statusFn = {
    0: () => {
      this.hasPlayToEnd = true
      // ...
    },
    1: () => {
      if (this.hasPlayToEnd) {
        this.hasPlayToEnd = false
      }
      // ...
    },
    2: () => {
      // console.log('已暫停')
    },
    3: () => {
      // console.log('正在緩衝')
    },
  }
  window.txplayer.on('playStateChange', (status) => {
    statusFn[status] && statusFn[status]()
    // ...
  })
},
複製代碼

存在問題

依然有偶發性失敗(大約10次裏有1~2次失敗,表現爲視頻一直轉圈圈)

緣由:定時器設置的時間不必定準,從新 play() 也是異步有延時,時間並不可控

最終解決方案

思路

額外定義一個變量: isReplayToSeek, 標識 是否爲從新播放完以後觸發的定點播放

jumpToStamp 方法中:

判斷如果播放完成後觸發跳轉,則執行 play(), 並設置 isReplayToSeektrue,但不進行跳轉(跳轉在 onChangePlayState 中完成 )

onChangePlayState 方法中:

當播放狀態爲1時(播放中), 判斷isReplayToSeek 是否爲 true, 如果,則進行跳轉(執行 seek()), 並重置 isReplayToSeekfalse

核心代碼

jumpToStamp(timestamp) {
  console.log('** 跳轉到指定時間點 **', timestamp)
  // 還在播放過程當中:直接跳轉
  if (!this.hasPlayToEnd) {
    window.txplayer.seek(Number(timestamp))
    return
  }

  // 播放已結束:從新play(),再在監聽的 playState 中去跳轉
  this.timestamp = timestamp
  window.txplayer.play(this.txVideoId)
  this.isReplayToSeek = true
},
onChangePlayState() {
  const statusFn = {
    0: () => {
      this.hasPlayToEnd = true
      // ...
    },
    1: () => {
      if (this.hasPlayToEnd) {
        this.hasPlayToEnd = false
      }
      // isReplayToSeek-true: 代表須要跳轉到指定位置播放
      if (this.isReplayToSeek) {
        window.txplayer.seek(this.timestamp)
        this.isReplayToSeek = false
      }
      // ...
    },
    2: () => {
      // console.log('已暫停')
    },
    3: () => {
      // console.log('正在緩衝')
    },
  }
  window.txplayer.on('playStateChange', (status) => {
    statusFn[status] && statusFn[status]()
    // ...
  })
},

複製代碼
相關文章
相關標籤/搜索