【更新2019.6.20】播放結束後的跳轉至某時間節點繼續播放(見底部)html
記一次項目總結: 騰訊視頻iframeAPI 的使用及踩坑指南vue
注:代碼技術棧-
react
react
目前業務的某平臺內容呈現方式有圖文、圖片等形式,這次項目爲增長視頻的呈現。 PM同窗調研後,決定使用騰訊視頻來接入,其中如下兩個功能點是在本次項目中耗時較長的點。ios
騰訊視頻統一播放器是視頻應用於全平臺播放(電腦,手機,平板電腦,電視[Sumsang],支持點播和直播,支持自定義插件的JavaScript框架。(對內使用的簡稱爲 js api)api
若是從A視頻,點擊進入B視頻,且點擊了播放,再返回A視頻,從頭進行播放(但記住B的播放位置) 若是從A視頻,點擊進入B視頻,但未點擊播放,再返回A視頻,繼續上次播放位置數組
前期技術調研時,發現可經過參數控制斷點續播緩存
調研後,感受難度不大。框架
然而進入開發以後,發現一個悲傷的事情……異步
對於外部,騰訊視頻提供IFrame Player API方式調用(下述簡稱 iframe api)ide
iframe api 提供的不管是參數仍是api都遠少於內部使用的 js api。 也就是說,咱們調研時的那幾個 api 均不可用。
好比想要獲取當前播放視頻的總時長。(咱們只能用後者)
// js api
var duration = player.getDuration()
console.log(`總時長爲:${duration}`)
// iframe api
player.getDuration().then(t => {
console.log(`總時長爲:${t}`)
}
複製代碼
統計用戶實際播放時長相對來講比較好解決。斷點續播走了很多彎路……
這個功能還好,依賴於大數據的埋點統計功能,我只須要監聽 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 個細節點是要注意的
因爲視頻播放過程當中是流式緩存,播放過程當中可能產生多個定時器。 故在輪詢時,定時器產生的 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('已中止 中止發送埋點')
}
複製代碼
假定咱們每3秒輪詢一次,視頻長度爲61秒,那麼最後1秒將記錄不到。故還需在播放結束時再強制發送一次埋點
onChangePlayState () {
const statusFn = {
0: () => {
// 結束時強制發送一次埋點,將視頻總時長做爲參數之一發送出去
// ...
},
// ...
}
// ...
}
複製代碼
先回顧一下需求:
- 若是從A視頻,點擊進入B視頻,且點擊了播放,再返回A視頻,從頭進行播放(但記住B的播放位置)
- 若是從A視頻,點擊進入B視頻,但未點擊播放,再返回A視頻,繼續上次播放位置
整理初步思路以下:
player.getCurrentTime()
獲取視頻當前播放時間 seektimeplayer.seek()
跳轉進度到 seektime根據初步思路,實現以下
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)
})
}
複製代碼
分析找到緣由:
視頻在初始化以後,其狀態剛開始是 -1 (未開始)。過一段時間後,纔會完成緩衝(3) 變成暫停狀態(2)。
因爲咱們沒法控制用戶的操做,且視頻具體什麼時候纔會由 -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)
}
// ...
}
// ...
}
// ...
}
複製代碼
經排查,發現 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()
}
}
複製代碼
經調試又發現:咱們原覺得第一時間就會觸發的 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)
}
複製代碼
其實還有一些其餘的問題,篇幅所限,暫時寫到這裏。 通過此次項目,反思總結以下:
熟讀文檔的重要性。第三方開發文檔必定要仔仔細細研讀,有的 api 用法可能就是不同凡響地藏在角落裏(異步api的回調)。
開闊思路,若是嘗試多種方案均沒法達成目標,優雅降級處理也何嘗不可(ios 沒法監聽 unbeforeload 事件,pagehide 事件中的代碼也未執行,解決辦法:使用上一次發送視頻播放埋點的時間)
注:如下代碼技術棧-
vue
點擊跳轉對應的時間點進行播放。
當視頻播放完畢後,點擊選項直接執行 txplayer.seek(t)
, 會致使視頻一直在加載中……
修改 jumpToStamp
方法。
判斷視頻是否完整播放過一遍(hasPlayToEnd
)
/** * @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()
, 並設置 isReplayToSeek
爲 true
,但不進行跳轉(跳轉在 onChangePlayState
中完成 )
在onChangePlayState
方法中:
當播放狀態爲1時(播放中), 判斷isReplayToSeek
是否爲 true
, 如果,則進行跳轉(執行 seek()
), 並重置 isReplayToSeek
爲 false
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]()
// ...
})
},
複製代碼