背景
分秒幀是一個音視頻生產(chǎn)協(xié)作平臺,其中用戶可以通過在視頻的某個時間點提出意見或分享來溝通對視頻的修改意見。由于客戶有時需要對時間精確到幀進行定位,我們需要保證不同轉碼視頻在播放時,時間定位能夠精確到毫秒級別。在滿足這一要求的同時,我們還必須考慮不同網(wǎng)絡條件、不同端和不同使用場景。我們在解決這些問題的過程中發(fā)現(xiàn)了一些問題,本文將對這些問題進行討論。
為什么需要畫面幀的準確性?
當用戶發(fā)送批注需要審閱者根據(jù)批注意見做出修改時,如果沒有畫面校準,此時審閱者一臉黑人問號, 哪來的“T” ? 然后再私下溝通嗎?信息存在誤差, 審閱批注就毫無意義。
問題一:保證瀏覽器中 Video 標簽時間定位在 pause 時的準確性
當用戶在播放視頻時暫停,并對視頻進行批注,然后繼續(xù)播放時,有時會發(fā)現(xiàn)定位回原始批注時間點時畫面會有一幀的偏差。這是因為,我們在暫停時記錄了視頻的當前時間(即 currentTime)并通過 seek() 方法回到該時間點,但是這個方法并不能保證回到的畫面完全準確。
現(xiàn)象
暫停批注時 沒有矯正currentTime,當批注發(fā)送成功后,自動跳回批注點,畫面發(fā)生了變化,以下是用戶所不想看到的畫面:
JS代碼如下:
JavaScript var videoDom = document.getElementsByTagName('video')[0] videoDom.pause() var currenttime = videoDom.currentTime videoDom.currentTime = currentTime // 此時畫面有概率發(fā)生改變
問題產(chǎn)生原因
我們在解決這個問題時發(fā)現(xiàn),這個問題是由 JavaScript 執(zhí)行機制導致的。在瀏覽器中,JavaScript 是單線程執(zhí)行的。當我們調用 pause 方法時,實際上是將該操作添加到了事件隊列中。當事件輪詢到這個暫停操作時,才會真正執(zhí)行 pause 方法。而在這個過程中,獲取 currentTime 的操作已經(jīng)完成了。這就導致了兩個操作之間的時間差。如果這個時間差恰好發(fā)生在視頻幀切換的時候,就會導致畫面偏差一幀。
舉個例子,如果一個視頻有 25 幀,那么第 0-40ms 是第一幀畫面,第 41-80ms是第二幀畫面,以此類推。
當用戶在播放第一幀畫面時按下暫停按鈕,我們認為JavaScript 會立即執(zhí)行邏輯并通知 Video 標簽停止播放,但實際上暫停操作會被加入事件隊列中等待執(zhí)行。如果暫停操作前面還有其他事件正在排隊,等執(zhí)行到暫停操作時就會有一定的時間差。如果這個時間差恰好發(fā)生在第 41 ms,畫面會跳到下一幀畫面。但是,我們拿到的currentTime還是第一幀畫面的。
解決方案
為了確保在暫停時和查看批注時 currentTime 的一致性,我們在暫停時對 currentTime 進行了矯正。這樣,當用戶暫停時進行批注,然后再設置 currentTime查看批注時,就不會出現(xiàn)畫面偏差問題。通過這種方式,我們就能保證畫面在暫停時和查看批注時的準確性。
問題二:HLS流中視頻 duration 值變化異常
在我們的應用中,我們需要確保各端的視頻總時長和總幀數(shù)一致。為了實現(xiàn)這個目的,我們通常會在瀏覽器 Video 標簽的 durationchange 事件觸發(fā)時獲取視頻總時長,并通過幀率計算出總幀數(shù)。durationchange 事件是當視頻總時長發(fā)生改變時觸發(fā)的。當視頻加載前,總時長為默認值"NaN",當視頻加載完成后,durationchange 事件觸發(fā),總時長會變成視頻的實際總時長。
在加載和播放視頻時,瀏覽器會用Video標簽來追蹤視頻的狀態(tài)。共有五個狀態(tài),分別是:[1]。
Constant | Value | Description |
HAVE_NOTHING | 0 | 沒有關于音頻/視頻是否就緒的信息 |
HAVE_METADATA | 1 | 音頻/視頻已初始化 |
HAVE_CURRENT_DATA | 2 | 數(shù)據(jù)已經(jīng)可以播放 (當前位置已經(jīng)加載) 但沒有數(shù)據(jù)能播放下一幀的內容 |
HAVE_FUTURE_DATA | 3 | 當前及至少下一幀的數(shù)據(jù)是可用的 (換句話來說至少有兩幀的數(shù)據(jù)) |
HAVE_ENOUGH_DATA | 4 | 可用數(shù)據(jù)足以開始播放 - 如果網(wǎng)速得到保障 那么視頻可以一直播放到底 |
在視頻加載和播放過程中,瀏覽器Video標簽的 readyState 會發(fā)生變化。在這個過程中,MP4文件和HLS文件的 duration 變更時機是不同的。
MP4
在 MP4 文件的加載過程中,durationchange 事件會在資源開始加載(loadstart)之后,在元數(shù)據(jù)已加載(loadedmetadata)之前觸發(fā)。此時,瀏覽器會解析 MP4 文件中的 moov box,并獲取視頻時長。因此,在 durationchange 事件觸發(fā)時,可以獲取到較為準確的 duration 。
HLS
我們發(fā)現(xiàn)在加載 HLS 流時,瀏覽器 video 標簽的 duration 會發(fā)生多次變更。
第一次變更在loadstart之后 loadedmetadata 之前 并且 readyState === 0 時調用,此時已拿到相對準確的 duration,≈ ffmpeg取到的 durantion。
舉個例子,ffmpeg截圖如下:
第二次變更在loadstart之后 loadedmetadata 之前 并且 readyState === 1 時調用,此時拿到的時長由 m3u8 文件解析得到。
第三次變更在加載到最后一片 ts 時調用。我們發(fā)現(xiàn)這三次變更的時長并不一致。因此我們需要在這三次變更中取一個更準確的時長作為視頻時長。
舉個例子,三次時長比較:
HLS三次取值時長不一致的原因
第一次:在loadstart后loadedmetadata前readyState === 0時調用,視頻的實際時長已被解析出來,時機和機制類似于MP4文件(第一次調用時就可以獲取到duration的值)。
第二次:在loadstart后loadedmetadata前readyState === 1時調用,hls.js解析完m3u8索引文件并通過#EXTINF計算出視頻的實際時長。
舉個例子,以下是一個m3u8文件信息:
第三次:當加載完最后一片ts 此時所有音頻和視頻幀信息已經(jīng)可以全部拿到。
舉個例子,以下是幀信息:
best_effort_timestamp_time :媒體流中的一個標識符,用于標識每一幀的時間戳。
pkt_duration_time :媒體流中的一個標識符,用于標識每一幀的持續(xù)時間。
通常,best_effort_timestamp_time 和 pkt_duration_time 會用在音視頻同步、流量控制、緩存等方面。
尾音頻/視頻信息中的 best_effort_timestamp_time 和 pkt_duration_time 可用來計算音頻/視頻的結束時長。在這個案例中,音頻結束時長由 best_effort_timestamp_time 和 pkt_duration_time 相加所得(即 96.230300 + 0.023211 = 96.253511),視頻結束時長也是如此(即 96.229778 + 0.016667 = 96.246445)。
我們發(fā)現(xiàn),音頻結束時長 - 音頻首個best_effort_timestamp_time約等于第三次獲取的duration。具體來說,音頻的結束時間比視頻的結束時間長,同時音頻的第一個時間戳早于視頻的第一個時間戳。為了包含最完整的時間長度,需要將音頻和視頻時間戳中的最小值和最大值來進行計算。這種情況可能出現(xiàn)在音頻和視頻的錄制或處理過程中,需要進行相應的調整以確保兩者之間的同步和一致性。
審核編輯:劉清
-
JAVA
+關注
關注
19文章
2960瀏覽量
104563 -
javascript
+關注
關注
0文章
516瀏覽量
53798 -
HLS
+關注
關注
1文章
128瀏覽量
24043
原文標題:在線視頻協(xié)同:探究畫面幀的準確性
文章出處:【微信號:livevideostack,微信公眾號:LiveVideoStack】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論