Flash視頻播放器開發經驗總結

HTTP協議更優

目前幾乎全部的視頻點播網站所有采用HTTP協議傳輸數據。由於相對於諸如RTMP等協議來講,HTTP協議是無狀態的,數據傳輸完畢就斷開鏈接,這樣服務器就能夠騰出資源來服務更多的用戶。而RTMP則會在用戶播放期間一直維護一個鏈接,這樣服務器的負載就很是有限。並且HTTP服務器,CDN等都已是很是成熟的技術,成本低性能好。另外HTTP的請求能夠直接使用瀏覽器Cookie,容易和網站業務打通。最後,HTTP還能使用瀏覽器緩存,這算優勢也算缺點,優勢是請求一樣的資源能夠直接從緩存中取,缺點是安全性差了點。nginx

HTTP擁有更好的性能,可是無法傳輸太實時性的東西,不然性能還不如RTMP,好比視頻聊天,直播這些。apache

安全性

有時候咱們被訪問的視頻可能須要作一些限制,好比防盜鏈,視頻收費等等。若是採用HTTP協議的話,傳統的鑑權方式就足夠,Cookie裏帶token什麼的判斷是否有權限訪問視頻資源,細節我就不說了。惟一的問題是一旦用戶有權限訪問視頻,就有可能把視頻下載下來用做他用。瀏覽器

經過分段進一步提高負載能力

爲了讓HTTP能服務更多的用戶,同時維護更少的鏈接,咱們須要傳輸儘快完畢。但這是咱們分段的理由嗎?不是,由於不管分段不分段,一個用戶加載徹底部的視頻數據對服務器佔用時間是必定的(假設傳輸速度必定),甚至會多佔用不少建立鏈接和銷燬鏈接的資源。緩存

可是咱們看到各大視頻網站實際上都是有對視頻分段的,這裏我就談談視頻分段的好處。安全

  1. 節約網站流量,也就是節約服務器資源提升負載能力。當用戶打開一個視頻的時候,頗有可能不會把視頻看完,只看一部分。若是不對視頻作分段,用戶一打開網站就把全部視頻數據加載完,那麼對流量就是極大的浪費。把視頻分段後咱們能夠一段一段加載視頻,作到用戶看多少咱們就加載多少。
  2. 更加靈活的seek(拖動),對於一個不作任何分段的視頻,好比HTTP服務器上的靜態視頻文件,咱們是沒法經過NetStream對象seek方法跳轉到未加載的視頻部分的。因此爲了解決這個問題,apache和nginx都提供了flv模塊,支持start參數。當指定start參數的時候,咱們能夠從新從指定位置加載視頻,解決了上述問題。可是帶來的問題是流量浪費,可能本來加載過的地方又要從新加載一遍。若是咱們採用分段的方式,就能夠避免這個問題,具體實現方法後面會詳細介紹。

NetStream對象

咱們在Flash端如何播放視頻很大程度上受NetStream提供的功能所限。因此這裏大體介紹下NetStream提供的功能和一些限制,這也是爲何後面程序要這麼設計的緣由。服務器

  1. NetStream提供兩種能夠播放HTTP視頻的模式,普通模式和數據生成模式。
  2. 在普通模式下,往NetStream傳入咱們要播放的HTTP視頻資源地址,NetStream就會開始加載視頻並開始播放。咱們能夠暫停視頻播放,可是不能暫停數據的加載,咱們能夠在已經加載過的數據部分隨意seek,可是不能seek到未加載的部分。數據加載完畢以後咱們任然能夠進行播放,seek等操做,可是若是調用了close方法關閉流,那麼若是數據未加載完畢,就會中止加載,而且不能作任何播放,seek等操做,這至關於咱們原來加載的數據都白費,不能再使用。因此若是咱們要把視頻分段後隨意在各個視頻分段裏來回seek,咱們必須讓一個分段視頻對應一個NetStream實例,換句話說有幾個分段就須要幾個NetStream伺候他們(咱們暫且這麼認爲,後面咱們會對這個問題作優化)。
  3. 在數據生成模式下,NetStream提供更加靈活的加載方式。NetStream經過appendBytes方法能夠添加外部的二進制數據來播放視頻,添加數據的順序就是播放的順序。這種狀況下咱們能夠經過URLStream對象加載視頻文件數據,理論上全部加載過的數據均可以被重複利用。可是注意不要把全部數據往內存裏塞,不然內存會被撐爆。具體的緩存策略後續具體講。
  4. 和其餘平臺的視頻播放器不一樣,Flash不能直接訪問本地文件,可是能夠經過加載已經加載過的視頻讓瀏覽器從緩存中快速取得視頻數據。因此如何有效利用緩存是優化的關鍵。
  5. 不要迷信NetStream的NetStatusEvent事件,在不一樣服務器和瀏覽器環境下,這個事件發生的時機可能略有差異,因此事件只能作參考,須要另外作一些前提判斷。

視頻分段須要的服務器支持

  1. 靜態分段:把視頻分爲固定的能夠獨立播放的幾段保存到服務器上,播放的時候須要得到一個視頻地址列表。每一個靜態分片都只能從頭開始請求不能從切片的中間開始請求。這是最容易作到也是性能作好的方式。
  2. 靜態分片+start參數:第一個方案的改進,能夠支持從分片的中間開始請求到分片的結尾。優酷土豆都是這麼作的噢。這樣方便seek。也有現成的nginx和apache模塊能夠支持。
  3. 動態分片:同時提供start和end參數,這樣能夠由播放器來決定如何請求分片,對播放器來講更靈活,對服務器的文件管理來講也更方便。這個解決服務器解決方案nginx和apache應該也有,沒有細究過。
  4. 以上三種最後請求出來的數據都是一個能完整獨立播放的視頻文件,服務器會自動幫你加上視頻文件頭。若是Flash使用的是數據生成模式,那麼實際上返回的直接是一個文件數據片斷就好了,不須要另外加上文件頭。

樸素的分段視頻播放

直接播放單個視頻文件的方式我就不說了,我這裏介紹的是如何像播一個完整文件同樣播放通過分段的視頻。這個方案有些許瑕疵,後續的方案都是基於這個方案進行優化的。網絡

服務器咱們採用上面提到的第一種最簡單的靜態分段。而且在視頻開始播放前咱們會拿到一個包含視頻分段的開始時間,結束時間,以及分段地址的列表,還有個總的視頻metadata信息。app

當視頻列表加載完畢後就能夠開始依次經過NetStream加載播放各個視頻分片了,每一個分片用一個NetStream實例控制。如圖所示。性能

咱們能夠設定一個最大緩衝距離,結合當前播放進度,算出一個容許緩衝位置,在這個容許緩衝位置以內的切片均可以依次開始加載,開始加載的時候暫停住不播放。當一個切片開始加載以後是不會中止的,因此實際緩衝進度可能會大於容許緩衝位置。優化

當一個切片播放完畢以後不要急着把它關掉,它可能須要留着供後續的seek使用。緊接着,咱們把下一個分片執行resume方法來讓他播放。這樣多個分片按照順序播放,對外界來講就像播放一個完整的視頻同樣。

這種結構下,若外界須要對視頻進行seek操做,能夠分三種狀況:

  1. seek到已加載分片的已加載部分,這種狀況效率最高,直接暫停當前播放的分片(若是是seek的位置就是當前切片這步均可以省了),讓seek目標時間所在分片seek到對應位置恢復播放就好了。
  2. seek到已加載分片的未加載部分,操做和上面的相似,因爲要seek的部分還未加載完,因此咱們只能seek到該分片已加載的最接近位置讓視頻儘快開始播放。
  3. seek到一個還未開始加載的切片的某個位置,一樣暫停當前播放的切片,轉到目標切片讓目標切片開始加載並儘快開始播放。(當前正在加載的切片有兩種處理策略,一種是還讓切片繼續加載完畢,另一種是直接關閉。還有一種折中策略,若是已經加載超過一半就讓他繼續加載完,不然關閉)

分段視頻播放改進:增長start參數

因此咱們能夠看到,靜態分片方式的在seek的處理仍是仍是有不少不足的,對未加載部份內容的seek都不能作到很是精確。不過若是將切片切得比較短小的話這個問題能夠有所改善,可是還會帶來另外的問題,這個問題我後面講。另外咱們能夠再靜態分片的基礎上引入了start參數,也就是上文提到的「靜態分片+start參數」類型服務器。

引入了start參數後對上面的二、3兩種seek狀況進行了改進:

  1. seek到已加載分片的未加載部分,關閉這個分片正在加載的流,並用這個分片的NetStream從新從seek位置指定的位置開始加載(經過指定start參數)。不過這個start參數不是隨便什麼均可以的,須要是視頻關鍵幀位置,不然返回回來將不能播。關鍵幀位置在metadata裏面能夠查詢到。
  2. 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:

  1. seek的位置位於內存中的數據:先清空NetStream的緩衝,而後把內存中響應位置的數據日後必定距離(NS緩衝長度以上)加入到NetStream中用於播放。
  2. seek的位置位於緩存中:先把緩存中的數據加載到內存中,而後經過第一條的方式實現。
  3. seek的位置位於服務器上:從服務器上加載數據分片數據到內存中,而後經過第一條的方式實現。

若是分片數據較大,seek的位置在分片中間,那麼也能夠從分片中間開始加載,這樣能夠從邏輯上把一個分片分爲了兩個。

數據生成模式從本質上保證了播放質量,杜絕了數據浪費,保證了seek精確度,服務器實現上也異常簡單,真是視頻播放首選!

贈品:用分段方式作直播

這種方式須要服務器作實時分片並分發到CDN。好比服務器從直播數據源裏把30秒的視頻數據打包成一個數據包分發到CDN上,因此理論上直播至少會延遲30秒。不過對於實時性不是特別強的直播,這種方式的負載能力會更好。

傳統的長鏈接方式直播,須要客戶端和服務器一直保持鏈接,服務器須要維護每一個客戶端的鏈接,但實際上傳輸30秒的視頻數據只須要1秒,因此若是採用HTTP的方式,由於傳輸完畢就能夠服務別人了,因此理論上維護鏈接的效率能夠提升30倍。

這裏咱們要求服務器提供一個視頻地址列表,列表裏提供了最新的N個視頻分片地址。這樣客戶端經過輪詢這個視頻列表就能讓客戶端和直播保持同步。

如圖所示客戶端維護着一個切片列表隊列,經過輪詢服務器,咱們把最新的視頻地址添加到隊列中,而播放模塊則從隊列中取出最老的切片地址加載播放。

若是用戶網絡較差,那麼播放就會卡頓,因此從隊列中取出切片地址的頻率就會下降,隊列會愈來愈長。隊列越長說明視頻播放的延遲越大。

因此當隊列長於某一個臨界值時(咱們設定的),咱們就把隊列清空到只剩一個最近的地址,直到下一次這個地址被取出時,才容許隊列繼續變長。這個隊列清空的操做其實是對因播放卡頓引發的延遲作了矯正,讓直播不要延遲得太厲害。

相關文章
相關標籤/搜索