HTTP協議更優
目前幾乎所有的視頻點播網站全部采用HTTP協議傳輸數據。因為相對於諸如RTMP等協議來說,HTTP協議是無狀態的,數據傳輸完畢就斷開連接,這樣服務器就可以騰出資源來服務更多的用戶。而RTMP則會在用戶播放期間一直維護一個連接,這樣服務器的負載就非常有限。而且HTTP服務器,CDN等都已經是非常成熟的技術,成本低性能好。另外HTTP的請求可以直接使用瀏覽器Cookie,容易和網站業務打通。最后,HTTP還能使用瀏覽器緩存,這算優點也算缺點,優點是請求同樣的資源可以直接從緩存中取,缺點是安全性差了點。
HTTP擁有更好的性能,但是沒法傳輸太實時性的東西,否則性能還不如RTMP,比如視頻聊天,直播這些。
安全性
有時候我們被訪問的視頻可能需要做一些限制,比如防盜鏈,視頻收費等等。如果采用HTTP協議的話,傳統的鑒權方式就足夠,Cookie里帶token什么的判斷是否有權限訪問視頻資源,細節我就不說了。唯一的問題是一旦用戶有權限訪問視頻,就有可能把視頻下載下來用作他用。
通過分段進一步提升負載能力
為了讓HTTP能服務更多的用戶,同時維護更少的連接,我們需要傳輸盡快完畢。但這是我們分段的理由嗎?不是,因為無論分段不分段,一個用戶加載完全部的視頻數據對服務器占用時間是一定的(假設傳輸速度一定),甚至會多占用很多創建連接和銷毀連接的資源。
但是我們看到各大視頻網站實際上都是有對視頻分段的,這里我就談談視頻分段的好處。
- 節約網站流量,也就是節約服務器資源提高負載能力。當用戶打開一個視頻的時候,很有可能不會把視頻看完,只看一部分。如果不對視頻做分段,用戶一打開網站就把所有視頻數據加載完,那么對流量就是極大的浪費。把視頻分段后我們可以一段一段加載視頻,做到用戶看多少我們就加載多少。
- 更加靈活的seek(拖動),對於一個不做任何分段的視頻,比如HTTP服務器上的靜態視頻文件,我們是無法通過NetStream對象seek方法跳轉到未加載的視頻部分的。所以為了解決這個問題,apache和nginx都提供了flv模塊,支持start參數。當指定start參數的時候,我們可以重新從指定位置加載視頻,解決了上述問題。但是帶來的問題是流量浪費,可能原本加載過的地方又要重新加載一遍。如果我們采用分段的方式,就可以避免這個問題,具體實現方法后面會詳細介紹。
NetStream對象
我們在Flash端如何播放視頻很大程度上受NetStream提供的功能所限。所以這里大致介紹下NetStream提供的功能和一些限制,這也是為什么后面程序要這么設計的原因。
- NetStream提供兩種可以播放HTTP視頻的模式,普通模式和數據生成模式。
- 在普通模式下,往NetStream傳入我們要播放的HTTP視頻資源地址,NetStream就會開始加載視頻並開始播放。我們可以暫停視頻播放,但是不能暫停數據的加載,我們可以在已經加載過的數據部分隨意seek,但是不能seek到未加載的部分。數據加載完畢之后我們任然可以進行播放,seek等操作,但是如果調用了close方法關閉流,那么如果數據未加載完畢,就會停止加載,並且不能做任何播放,seek等操作,這相當於我們原來加載的數據都白費,不能再使用。所以如果我們要把視頻分段后隨意在各個視頻分段里來回seek,我們必須讓一個分段視頻對應一個NetStream實例,換句話說有幾個分段就需要幾個NetStream伺候他們(我們暫且這么認為,后面我們會對這個問題做優化)。
- 在數據生成模式下,NetStream提供更加靈活的加載方式。NetStream通過appendBytes方法可以添加外部的二進制數據來播放視頻,添加數據的順序就是播放的順序。這種情況下我們可以通過URLStream對象加載視頻文件數據,理論上所有加載過的數據都可以被重復利用。但是注意不要把所有數據往內存里塞,否則內存會被撐爆。具體的緩存策略后續具體講。
- 和其他平台的視頻播放器不同,Flash不能直接訪問本地文件,但是可以通過加載已經加載過的視頻讓瀏覽器從緩存中快速取得視頻數據。所以如何有效利用緩存是優化的關鍵。
- 不要迷信NetStream的NetStatusEvent事件,在不同服務器和瀏覽器環境下,這個事件發生的時機可能略有差別,所以事件只能做參考,需要另外做一些前提判斷。
視頻分段需要的服務器支持
- 靜態分段:把視頻分為固定的可以獨立播放的幾段保存到服務器上,播放的時候需要獲得一個視頻地址列表。每個靜態分片都只能從頭開始請求不能從切片的中間開始請求。這是最容易做到也是性能做好的方式。
- 靜態分片+start參數:第一個方案的改進,可以支持從分片的中間開始請求到分片的結尾。優酷土豆都是這么做的噢。這樣方便seek。也有現成的nginx和apache模塊可以支持。
- 動態分片:同時提供start和end參數,這樣可以由播放器來決定如何請求分片,對播放器來說更靈活,對服務器的文件管理來說也更方便。這個解決服務器解決方案nginx和apache應該也有,沒有細究過。
- 以上三種最后請求出來的數據都是一個能完整獨立播放的視頻文件,服務器會自動幫你加上視頻文件頭。如果Flash使用的是數據生成模式,那么實際上返回的直接是一個文件數據片段就行了,不需要另外加上文件頭。
朴素的分段視頻播放
直接播放單個視頻文件的方式我就不說了,我這里介紹的是如何像播一個完整文件一樣播放經過分段的視頻。這個方案有些許瑕疵,后續的方案都是基於這個方案進行優化的。
服務器我們采用上面提到的第一種最簡單的靜態分段。並且在視頻開始播放前我們會拿到一個包含視頻分段的開始時間,結束時間,以及分段地址的列表,還有個總的視頻metadata信息。
當視頻列表加載完畢后就可以開始依次通過NetStream加載播放各個視頻分片了,每個分片用一個NetStream實例控制。如圖所示。
我們可以設定一個最大緩沖距離,結合當前播放進度,算出一個允許緩沖位置,在這個允許緩沖位置之內的切片都可以依次開始加載,開始加載的時候暫停住不播放。當一個切片開始加載之后是不會停止的,所以實際緩沖進度可能會大於允許緩沖位置。
當一個切片播放完畢之后不要急着把它關掉,它可能需要留着供后續的seek使用。緊接着,我們把下一個分片執行resume方法來讓他播放。這樣多個分片按照順序播放,對外界來說就像播放一個完整的視頻一樣。
這種結構下,若外界需要對視頻進行seek操作,可以分三種情況:
- seek到已加載分片的已加載部分,這種情況效率最高,直接暫停當前播放的分片(如果是seek的位置就是當前切片這步都可以省了),讓seek目標時間所在分片seek到對應位置恢復播放就行了。
- seek到已加載分片的未加載部分,操作和上面的類似,由於要seek的部分還未加載完,所以我們只能seek到該分片已加載的最接近位置讓視頻盡快開始播放。
- seek到一個還未開始加載的切片的某個位置,同樣暫停當前播放的切片,轉到目標切片讓目標切片開始加載並盡快開始播放。(當前正在加載的切片有兩種處理策略,一種是還讓切片繼續加載完畢,另外一種是直接關閉。還有一種折中策略,如果已經加載超過一半就讓他繼續加載完,否則關閉)
分段視頻播放改進:增加start參數
所以我們可以看到,靜態分片方式的在seek的處理還是還是有很多不足的,對未加載部分內容的seek都不能做到非常精確。不過如果將切片切得比較短小的話這個問題可以有所改善,但是還會帶來另外的問題,這個問題我后面講。另外我們可以再靜態分片的基礎上引入了start參數,也就是上文提到的“靜態分片+start參數”類型服務器。
引入了start參數后對上面的2、3兩種seek情況進行了改進:
- seek到已加載分片的未加載部分,關閉這個分片正在加載的流,並用這個分片的NetStream重新從seek位置指定的位置開始加載(通過指定start參數)。不過這個start參數不是隨便什么都可以的,需要是視頻關鍵幀位置,否則返回回來將不能播。關鍵幀位置在metadata里面可以查詢到。
- seek到未加載切片也同樣,從根據seek位置設定start參數后開始加載。
如此以來在任何情況下seek都可以精確到關鍵幀,缺點是把正在加載的切片關掉會造成數據浪費。從切片中間開始加載也會造成一個切片內容不完整。下次seek的時候如果不巧是在這個切片start位置之前,就需要重新加載該切片。這些都會造成數據浪費。好在一般用戶不會吃飽了沒事兒seek來seek去。
通過連接池來限制連接數量
從上文的幾個策略可以看出,如果視頻分得越短小,無論對seek的精確度,還是數據浪費情況都是有好處的,但是這帶來的一個問題是需要實例化更多的NetStream來維護切片。另外對於時長較長的視頻來說,NetStream的數量也會變得很多。但實際上NetStream能同時開啟的連接數量是有限的,這不是內存問題,而是Flash提供的連接數有限。超過了這個限制NetStream就沒辦法正常工作了,而且也不報錯。這個限制在不同瀏覽器下還不一樣,我懷疑這和瀏覽器底層有關。
所以為了限制NetStream的數量,我們需要設計一個NetStream連接池來管理所有的NetStream。連接池上限不能小於最大緩沖舉例可能加載的最多分片數,否則邏輯上就是有問題滴。
我們可以從連接池中取得一個新的NetStream來使用(這個NetStream可能是別的NetStream關閉后的,不過你可以把它當新的用)當連接池數量滿的時候,他就會自動把一些老的處於連接狀態的NetStream關閉掉。這個淘汰原則是基於空間局部性原理的,也就是說和當前播放頭位置距離最遠的切片應該首先被關掉(處於最大緩沖距離之內的切片不能關閉)。因為根據概率統計發現大部分的seek都出現在播放頭附近(可能為了找什么情節)。
推薦使用數據生成模式
通過多個NetStream切換的方式播放視頻,在切換的時候會出現不明顯的爆音,但是仔細聽還是能夠發現。這也是我在上文中提到的文件分割得太短小出現的另外一個問題,爆音太頻繁了,可能影響視頻觀看。
所以要從根本上解決這個問題,我們就要放棄NetStream切換的方式,轉用數據生成模式。數據生成模式可以把請求的切片做得很小(但也不要太小,否則服務器性能降低)。切片做小的一個好處是請求更快的完成,那么請求被打斷的幾率就會降低,當請求完成之后,下次請求同樣的資源就能從瀏覽器緩沖中取。所以小切片更容易被緩存。而上文中的小切片產生的問題在這里不復存在。如圖所示。
我們根據播放頭的位置,往后加載分片數據,直到最大緩沖距離,這和前面提到的方式類似。而后我們把這些加載的二進制數據保存在內存中。從播放后往后一定的距離(我們稱作NS緩沖長度),如果有分片進入,那么就把它appendBytes到用於播放的NetStream中。圖中所示的藍色部分就是保存在內存中的數據,它也有前面提到的連接池類似的淘汰機制用於控制內存總大小。被從內存中釋放掉的數據,我們可以在瀏覽器緩存中找到(因為已經加載過了),如果要使用的話,我們可以像請求服務端數據一樣的方式快速請求到這些數據(當然比從內存中慢一些)。圖中白色方框的是還未加載過的數據,他們在服務器上等待加載。如圖所示就是數據的三級查詢。
如果用戶進行seek:
- seek的位置位於內存中的數據:先清空NetStream的緩沖,然后把內存中響應位置的數據往后一定距離(NS緩沖長度以上)加入到NetStream中用於播放。
- seek的位置位於緩存中:先把緩存中的數據加載到內存中,然后通過第一條的方式實現。
- seek的位置位於服務器上:從服務器上加載數據分片數據到內存中,然后通過第一條的方式實現。
如果分片數據較大,seek的位置在分片中間,那么也可以從分片中間開始加載,這樣可以從邏輯上把一個分片分為了兩個。
數據生成模式從本質上保證了播放質量,杜絕了數據浪費,保證了seek精確度,服務器實現上也異常簡單,真是視頻播放首選!
贈品:用分段方式做直播
這種方式需要服務器做實時分片並分發到CDN。比如服務器從直播數據源里把30秒的視頻數據打包成一個數據包分發到CDN上,所以理論上直播至少會延遲30秒。不過對於實時性不是特別強的直播,這種方式的負載能力會更好。
傳統的長連接方式直播,需要客戶端和服務器一直保持連接,服務器需要維護每個客戶端的連接,但實際上傳輸30秒的視頻數據只需要1秒,所以如果采用HTTP的方式,因為傳輸完畢就可以服務別人了,所以理論上維護連接的效率可以提高30倍。
這里我們要求服務器提供一個視頻地址列表,列表里提供了最新的N個視頻分片地址。這樣客戶端通過輪詢這個視頻列表就能讓客戶端和直播保持同步。
如圖所示客戶端維護着一個切片列表隊列,通過輪詢服務器,我們把最新的視頻地址添加到隊列中,而播放模塊則從隊列中取出最老的切片地址加載播放。
如果用戶網絡較差,那么播放就會卡頓,所以從隊列中取出切片地址的頻率就會降低,隊列會越來越長。隊列越長說明視頻播放的延遲越大。
所以當隊列長於某一個臨界值時(我們設定的),我們就把隊列清空到只剩一個最近的地址,直到下一次這個地址被取出時,才允許隊列繼續變長。這個隊列清空的操作實際上是對因播放卡頓引起的延遲做了矯正,讓直播不要延遲得太厲害。