遇到的挑戰
移動端HTML5使用原生<video>標簽播放視頻,要做到兩個基本原則,速度快和體驗佳,先來分析一下這兩個問題。
下載速度
以一個8s短視頻為例,wifi環境下提供的高清視頻達到1000kbps,文件大小大約1MB;非wifi環境下提供的低碼率視頻是500kbps左右,文件大小大約500KB;參考QzoneTouch多普勒測速,2g網絡的平均速度是14KB/s,那么下載一個低碼率視頻耗時35s;那么要想流暢播放視頻,就需要一個加載等待的過程,這個過程要有明確的反饋,不能讓用戶有“壞掉了”的感覺。
多普勒測速數據參考
# | dns(s) | conn(s) | rtt(s) | tran(kb/s) |
---|---|---|---|---|
2g | 3.85785 | 2.33482 | 2.57478 | 14.0374 |
3g | 1.60643 | 0.743109 | 0.608047 | 60.1967 |
wifi | 0.986921 | 0.550208 | 0.444332 | 70.8728 |
用戶體驗
視頻是否可以自動播放,是否能循環播放,是否能顯示下載進度,播放的時候如何隱藏控制條,暫停的時候又能顯示出來呢。這些問題看上去貌似簡單,但是由於PC/iOS/Android這些不同平台、不同的瀏覽器內核、甚至相同內核的不同版本,所實現的<video>屬性、方法和事件差異較大,解決兼容性問題又給開發造成了很大困擾。
分析原因
事件差異
下面是播放一個短視頻,在不同平台觸發事件和獲取屬性的差異表現。
PC
# | event | readyState | currentTime (s) | buffered (s) | duration (s) | 視頻狀態 |
---|---|---|---|---|---|---|
1 | loadstart | NOTHING | 0 | - | - | - |
2 | suspend | NOTHING | 0 | - | - | - |
3 | play | NOTHING | 0 | - | - | - |
4 | waiting | NOTHING | 0 | - | - | - |
5 | durationchange | METADATA | 0 | 5.35 | 7.91 | 獲取到視頻長度 |
6 | loadedmetadata | METADATA | 0 | 0.66 | 7.91 | 獲取到元數據 |
7 | loadeddata | ENOUGHDATA | 0 | 0.66 | 7.91 | - |
8 | canplay | ENOUGH_DATA | 0 | 0.66 | 7.91 | - |
9 | playing | ENOUGH_DATA | 0 | 0.66 | 7.91 | 開始播放 |
10 | canplaythrough | ENOUGH_DATA | 0 | 0.66 | 7.91 | 可以流暢播放 |
11 | progress | ENOUGH_DATA | 0.11 | 3.68 | 7.91 | 持續下載 |
12 | timeupdate | ENOUGH_DATA | 0.14 | 4.44 | 7.91 | 播放進度變化 |
… | … | … | … | … | … | … |
23 | progress | ENOUGH_DATA | 1.77 | 7.91 | 7.91 | 下載完畢 |
24 | suspend | ENOUGH_DATA | 1.77 | 7.91 | 7.91 | - |
25 | timeupdate | ENOUGH_DATA | 1.9 | 7.91 | 7.91 | 繼續播放中 |
… | … | … | … | … | … | … |
48 | timeupdate | ENOUGH_DATA | 7.7 | 7.91 | 7.91 | - |
49 | timeupdate | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
50 | seeking | METADATA | 0 | 7.91 | 7.91 | - |
51 | waiting | METADATA | 0 | 7.91 | 7.91 | - |
52 | timeupdate | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
53 | seeked | ENOUGH_DATA | 0 | 7.91 | 7.91 | 播放完畢進度回到起點 |
54 | canplay | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
55 | playing | ENOUGH_DATA | 0 | 7.91 | 7.91 | 循環播放 |
56 | canplaythrough | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
57 | timeupdate | ENOUGH_DATA | 0.19 | 7.91 | 7.91 | - |
… | … | … | … | … | … | … |
iOS
# | event | readyState | currentTime (s) | buffered (s) | duration (s) | 視頻狀態 |
---|---|---|---|---|---|---|
1 | loadstart | NOTHING | 0 | - | - | - |
2 | play | NOTHING | 0 | - | - | - |
3 | waiting | NOTHING | 0 | - | - | - |
4 | durationchange | METADATA | 0 | - | 7.91 | 獲取到視頻長度 |
5 | loadedmetadata | METADATA | 0 | - | 7.91 | 獲取到元數據 |
6 | loadeddata | ENOUGHDATA | 0 | - | 7.91 | - |
7 | canplay | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
8 | canplaythrough | ENOUGH_DATA | 0 | 7.91 | 7.91 | 可以流暢播放 |
9 | playing | ENOUGH_DATA | 0 | 7.91 | 7.91 | 開始播放 |
10 | progress | ENOUGH_DATA | 0 | 7.91 | 7.91 | 下載完畢 |
11 | suspend | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
12 | timeupdate | ENOUGH_DATA | 0.02 | 7.91 | 7.91 | 播放進度變化 |
… | … | … | … | … | … | … |
43 | timeupdate | ENOUGH_DATA | 7.8 | 7.91 | 7.91 | - |
44 | timeupdate | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
45 | seeked | ENOUGH_DATA | 0 | 7.91 | 7.91 | 播放完畢進度回到起點 |
46 | timeupdate | ENOUGH_DATA | 0.22 | 7.91 | 7.91 | 循環播放 |
… | … | … | … | … | … | … |
Android
# | event | readyState | currentTime (s) | buffered (s) | duration (s) | 視頻狀態 |
---|---|---|---|---|---|---|
1 | loadstart | NOTHING | 0 | - | - | - |
2 | play | NOTHING | 0 | - | - | - |
3 | waiting | NOTHING | 0 | 0 | - | - |
4 | durationchange | ENOUGH_DATA | 0 | 0 | 0 | - |
5 | durationchange | ENOUGH_DATA | 0 | 0 | 7.91 | 獲取到視頻長度 |
6 | loadedmetadata | ENOUGH_DATA | 0 | 0 | 7.91 | 獲取到元數據 |
7 | loadeddata | ENOUGHDATA | 0 | 0 | 7.91 | - |
8 | canplay | ENOUGH_DATA | 0 | 0 | 7.91 | - |
9 | canplaythrough | ENOUGH_DATA | 0 | 0 | 7.91 | - |
10 | playing | ENOUGH_DATA | 0 | 0 | 7.91 | - |
11 | timeupdate | ENOUGH_DATA | 0 | 0 | 7.91 | - |
12 | progress | ENOUGH_DATA | 0 | 3.57 | 7.91 | 下載中 |
13 | timeupdate | ENOUGH_DATA | 0.2 | 6.89 | 7.91 | 開始播放 |
14 | progress | ENOUGH_DATA | 0 | 7.91 | 7.91 | 下載完畢 |
… | … | … | … | … | … | … |
49 | timeupdate | ENOUGH_DATA | 7.79 | 7.91 | 7.91 | - |
50 | progress | ENOUGH_DATA | 7.87 | 7.91 | 7.91 | - |
51 | timeupdate | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
52 | seeking | ENOUGH_DATA | 0 | 7.91 | 7.91 | 播放完畢進度回到起點 |
53 | timeupdate | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
54 | seeked | ENOUGH_DATA | 0 | 7.91 | 7.91 | 循環播放失敗卡住了 |
55 | progress | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
56 | stalled | ENOUGH_DATA | 0 | 7.91 | 7.91 | - |
一些常用且需要重點關注的<video>事件
event | iOS | Android |
---|---|---|
****************** | *********************************************** | *********************************************** |
play | 只是要播放視頻,響應的是video.play()方法,並不代表已經開始播放 | 和iOS一樣,僅是響應video.play()方法 |
durationchange | 會執行一次,一定會獲取到視頻的duration | 可能會執行多次,只有最后一次才能獲取到真實的duration,前面的duration都是0;但低版本Android可能獲取到的duration是0或1;(本文提到的低版本Android大部分是4.1以下) |
canplay | 可以認為是視頻元素沒有問題,可以運行,沒有更多含義了,基本用不上 | 同iOS |
canplaythrough | 會有明確的緩沖,表示可以流暢播放了; | 沒有什么用,視頻仍然會卡住,數據可能還沒有開始加載; |
playing | 明確表示播放開始了; | 依然沒有用,視頻可能並沒有開始播放; |
progress | 有明確的下載,可以獲取到當前的buffer,並且全部下載完畢后不在觸發; | 不一定有明確的數據下載,並且全部下載完畢后依然繼續觸發; |
timeupdate | 會有明確的進度變化,可以獲取到currentTime; | 進度不一定變化,currentTime可能總是0,但是第一次有currentTime變化的timeupdate事件一定代表了視頻開始播放了; |
error | iOS中會有明確的錯誤拋出; | Android中某些瀏覽器會莫名其妙的拋出error; |
stalled | 網絡狀況不佳,導致視頻下載中斷; | 在沒有play之前,也可能會拋出該事件。 |
屬性差異
attributes | iOS | android |
---|---|---|
****************** | *********************************************** | *********************************************** |
poster 封面圖片 |
支持,但是加載速度明顯比在<img>中要慢; | 不一定支持(瀏覽器廠商的實現標准不統一); |
preload 預加載 |
iPhone不支持; | 可能支持; |
autoplay 自動播放 |
iPhone Safari中不支持,但在webview中可能被開啟;iOS開發文檔明確說明蜂窩網絡下不允許autoplay; | 可能支持; |
loop 循環播放 |
支持 | 可能支持; |
controls 控制條 |
支持,但是需要開始播放了才顯示 | 基本都支持顯示或者不顯示 |
width和height | 一定給出明確的屬性設置,切不能為0; | 如果不設置,僅僅通過CSS樣式去控制視頻大小,可能會導致標簽失效。 |
其他怪異bug和不友好表現
iOS | android |
---|---|
********************************************************* | ********************************************************* |
物理位置覆蓋在<video>區域上的元素,click和touch等事件會失效,比如一個<a>鏈接如果覆蓋在<video>上,那么點擊后沒有任何效果。 | - |
iOS8.0+中,單頁面播放視頻超過16個,再播放的視頻全部MediaError解碼異常無法播放。 | - |
iPhone的Safari會彈出一個全屏的播放器來播放視頻,iPad則支持內聯播放。iOS7+ 如果webview(比如微信)開啟了webview.allowsInlineMediaPlayback = YES; ,可以通過設置webkit-playsinline 屬性支持內聯播放; |
支持內聯播放,但某些廠商會用自己的播放器劫持原生的視頻播放; |
下載視頻時,會先發送一個2字節的請求來獲取視頻元數據(比如時長),然后再不斷的發送分包續傳(206)請求來下載視頻,抓包顯示請求數和請求量至少有一倍的冗余(x2),這個嚴重的bug在iOS8中有明顯的修復,但是分包的206請求仍然會有冗余數據的下載,浪費了流量。 | 比iOS的處理方式好,沒有第一個2字節請求,沒有流量損耗; |
- | 低版本Android(<=4.0.4)中,<video>如果在有相對和決定定位的層中,可能會導致整個頁面錯位。 |
- | 某些瀏覽器廠商會劫持<video>,用其“自己”的播放器來播放視頻,“破壞”了產品本身的播放體驗,那么只能case by case的解決了。 |
加載視頻時沒有進度提示,視覺上看不出是播放完了還是卡住了; | 加載視頻時,大都會顯示一個自帶的loading UI(菊花)。 |
最佳實踐
視頻初始化
如果將一個<video>直接顯示在頁面中,那么就會看到各種五花八門的播放器初始效果;
這顯然不是一個好的視覺體驗,那么通常的做法是制作一個模擬的視頻播放視圖,比如一個封面加一個播放按鈕。
而真實的<video>視頻元素要隱藏起來,如何隱藏呢?最好不要用{display: none}
或者{width:0;height:0;}
的方式,因為這樣視頻元素會處於未激活的狀態,給后續的處理帶來麻煩。最佳的方式是將視頻設置成1×1像素大小,放在視覺邊緣的位置。
1
2
3
4
5
|
<!--iOS-->
<video webkit-playsinline width="1" height="1" class="vplayinside notaplink" x-webkit-airplay controls loop="loop" src="<%=src%>"></video>
<!--Android-->
<video width="1" height="1" controls loop="loop" src="<%=src%>"></video>
|
自動播放
autoplay的支持依賴內核和網絡狀況,比如iPhone在蜂窩網絡下明確禁用了autoplay;
經過試驗,在沒有明確的用戶操作的情況下,直接通過video.play()
也是無法激活播放的;
並且在產品設計上,自動播放也不是一個舒服的用戶體驗,所以產品設計上盡量避免使用自動播放。
點擊播放
之前提到,視頻最好通過1px大小隱藏起來,那么這時如何觸發播放呢?
經過試驗,當在明確的用戶操作(touch、click)時,通過這些用戶行為事件的回調函數,用video.play()
是可以觸發視頻播放的,那么能否在用戶操作后,再去同步的創建和播放視頻呢?答案是肯定的,這無疑是一個視頻元素初始化的最佳實踐,但是有些差異需要注意。
iOS6+
可以在用戶的touch時間中動態創建並播放視頻。
iOS < 6
可以在用戶的touch時間中動態創建視頻,但不能播放;要再追加一個click事件來啟動播放;也就是說,給偽造的視頻播放按鈕同時綁定tap和click事件,在tap的時候創建,在之后300毫秒的click中去播放。
Android
大部分高版本Android可以像iOS6+那樣去處理,但是低版本的不行,必須要通過click事件去傳遞video.play()
,為了保持兼容,最好是用幫tap和click兩個事件來分別完成視頻的初始化和播放。
我們還發現,有些低版本Android中,無法通過video.play()
來播放視頻,必須有真實的用戶點擊視頻元素才能播放;這種情況,有一個技巧就是在tap的時候初始化並放大視頻覆蓋在播放視圖中,讓300毫秒后的真實點擊行為穿透點擊在視頻元素上來實現播放。
循環播放
如果視頻需要循環播放,那么就增加loop
屬性,是否能循環播放就看瀏覽器是否支持了,因為還沒有找到hack技巧來強制循環播放;
即使,在不支持循環播放的Android中,通過監聽seeked
事件知道了播放進度到了終點或起點暫停了,此時也無法通過video.play()
來讓視頻重新播放。
監控下載進度
如何獲取視頻時長和已經下載的時長?
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 視頻時長
var duration = video.duration
// 獲取視頻已經下載的時長
function getEnd(video) {
var end = 0
try {
end = video.buffered.end(0) || 0
end = parseInt(end * 1000 + 1) / 1000
} catch(e) {
}
return end
}
|
progress事件表示視頻在加載,但是它的觸發頻率和時機並不規律,最佳做法是通過一個定時器去實時獲取end,當end >= duration時,表示已經下載完畢,再終止定時器。
1
2
3
4
5
6
7
8
9
10
|
var timer = setInterval(function() {
var end = getEnd(video),
duration = video.duration
if(end < duration) {
return
}
clearInterval(timer)
}, 1000)
|
全部下載后再播放
假設播放短視頻,如果網絡不佳,會造成播放斷斷續續,在iOS中這種停頓還沒有一個明確的等待提示,這不是一個好的體驗,那么是否可以將視頻全部下載完畢再播放呢?
在iOS中,可以在視頻剛開始下載的時候馬上暫停,此時下載還將繼續,可以做一個loading的菊花告知視頻正在加載,然后等到視頻全部下載完再開始播放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
$(video).one('loadeddata', function() {
// 暫停,但下載還在繼續
video.pause()
// 啟動定時器檢測視頻下載進度
var timer = setInterval(function() {
var end = getEnd(video),
duration = video.duration
if(end < duration) {
return
}
var width = $(video).parent().width()
// 下載完了,開始播放吧
$(video).attr{
width: width,
height: width
}
video.play()
clearInterval(timer)
}, 1000)
})
|
緩沖播放——邊下邊播時,選擇開始播放的最佳時間點
當視頻越來越長或者網絡慢時,等待視頻全部下載完再播放也不是好的體驗,最好能邊下邊播,緩沖到流暢狀態就開始播放,那什么時候播放才是最佳時間點呢?
在iOS中,canplaythrough事件就是這個最佳時間點,它是通過動態計算緩沖量和下載速度得出的視頻可以流暢播放的狀態反饋。
canplaythrough event: The user agent estimates that if playback were to be started now, the media resource could be rendered at the current playback rate all the way to its end without having to stop for further buffering.
注意:下載完再播放和緩沖播放只適用於iOS。
統計播放時間和播放次數
要統計實際的播放時間,要累加timeupdate事件變化的時間,再減去中間可能暫停的時間。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
$video.on('playing', function() {
// 開始播放是打點
$video.attr('data-updateTime', +new Date())
})
$video.on('pause', function() {
// 暫停播放時清除打點
$video.removeAttr('data-updateTime')
})
// 累加播放時間
$video.on('timeupdate', function(event) {
var $video = $(event.target),
updateTime = parseInt($video.attr('data-updateTime') || 0),
playingTime = parseInt($video.attr('data-playingTime') || 0),
times = parseInt($video.attr('data-times') || 0),
newtimes = 0,
video = $video.get(0),
duration = parseFloat($video.attr('data-duration') || 0),
now = +new Date()
// 播放時間
playingTime = playingTime + now - updateTime
// 播放次數
newtimes = Math.ceil(playingTime / 1000 / duration)
$video.attr('data-playingTime', playingTime)
$video.attr('data-updateTime', now)
})
|
異常處理
對error事件做詳細的上報;
對stalled事件做統計上報,並提示用戶網絡慢等。
參考數據
微視觸屏版iOS視頻測速
網絡環境 | 視頻碼率 | 獲取到視頻時長 時間點(s) |
開始流暢播放 時間點(s) |
全部下載完畢 時間點(s) |
視頻長度(s) |
---|---|---|---|---|---|
wifi | 1000kbps | 2.86 | 3.97 | 5.85 | 8.69 |
非wifi | 500kbps | 4.56 | 8 | 10.62 | 8.67 |
參考資料
- HTML5 Video Events and API檢測工具 http://www.w3.org/2010/05/video/mediaevents.html
- W3C video 標准 http://www.w3.org/TR/html5/embedded-content-0.html#the-video-element
- 如何在iOS7+的webview中內聯播放視頻 http://darktalker.com/2014/play-video-inline-iphone-ios7
- 視頻事件流水查看工具 http://z.weishi.qq.com/app/video.html