網頁播放器js代碼「js播放器是什麼」

本文記錄一下在使用 flv.js 播放監控視頻時踩過的各種各樣的坑。雖然官網給的 Getting Started 只有短短几行代碼,跑一個能播視頻的 demo 很容易,但是播放時各種各樣的異常會搞到你懷疑人生。

究其原因,一方面 GitHub 上文檔比較晦澀,說明也比較簡陋;另一方面是受「視頻播放」思維的影響,沒有對的足夠認識以及缺乏處理流的經驗。

下面我將自己踩過的坑,以及踩坑過程中補充的相關知識,詳細總結一下。

大綱預覽

本文介紹的內容包括以下方面:

  • 直播與點播
  • 靜態數據與流數據
  • 為什麼選 flv?
  • 協議與基礎實現
  • 細節處理要點
  • 樣式定製

點播與直播

啥是直播?啥是點播?

直播就不用說了,抖音普及之下大家都知道直播是幹嘛的。點播其實就是視頻播放,和咱們嗶哩嗶哩看視頻一摸一樣沒區別,就是把提前做好的視頻放出來,就叫點播。

點播對於我們前端來說,就是拿一個 mp4 的鏈接地址,放到 video 標籤裏面,瀏覽器會幫我們處理好視頻解析播放等一些列事情,我們可以拖動進度條選擇想看的任意一個時間。

但是直播不一樣,直播有兩個特點:

  1. 獲取的是流數據
  2. 要求實時性

先看一下什麼叫流數據。大部分沒有做過音視頻的前端同學,我們常接觸的數據就是 ajax 從接口獲取的 json 數據,特別一點的可能是文件上傳。這些數據的特點是,它們都屬於一次性就能拿到的數據。我們一個請求,一個響應,完整的數據就拿回來了。

但是流不一樣,流數據獲取是一幀一幀的,你可以理解為是一小塊一小塊的。像直播流的數據,它並不是一個完整的視頻片段,它就是很小的二進制數據,需要你一點一點的拼接起來,才有可能輸出一段視頻。

再看它的實時性。如果是點播的話,我們直接將完整的視頻存儲在服務器上,然後返回鏈接,前端用 video 或播放器播就行了。但是直播的實時性,就決定了數據源不可能在服務器上,而是在某一個客戶端。

數據源在客戶端,那麼又是怎麼到達其他客戶端的呢?

這個問題,請看下面這張流程圖:

用一個 flv.js 播放監控的例子,帶你深撅直播流技術

如圖所示,發起直播的客戶端,向上連着流媒體服務器,直播產生的視頻流會被實時推送到服務端,這個過程叫做推流。其他客戶端同樣也連接着這個流媒體服務器,不同的是它們是播放端,會實時拉取直播客戶端的視頻流,這個過程叫做拉流。

推流—> 服務器-> 拉流,這是目前流行的也是標準的直播解決方案。看到了吧,直播的整個流程全都是流數據傳輸,數據處理直面二進制,要比點播複雜了幾個量級。

具體到我們業務當中的攝像頭實時監控預覽,其實和上面的完全一致,只不過發起直播的客戶端是攝像頭,觀看直播的客戶端是瀏覽器而已。

靜態數據與流數據

我們常接觸的文本,json,圖片等等,都屬於靜態數據,前端用 ajax 向接口請求回來的數據就是靜態數據。

像上面說到的,直播產生的視頻和音頻,都屬於流數據。流數據是一幀一幀的,它的本質是二進制數據,因為很小,數據像水流一樣連綿不斷的流動,因此非常適合實時傳輸。

靜態數據,在前端代碼中有對應的數據類型,比如 string,json,array 等等。那麼流數據(二進制數據)的數據類型是什麼?在前端如何存儲?又如何操作?

首先明確一點,前端是可以存儲和操作二進制的。最基本的二進制對象是 ArrayBuffer,它表示一個固定長度,如:

let buffer = new ArrayBuffer(16) // 創建一個 16 位元組 的 buffer,用 0 填充
alert(buffer.byteLength) // 16

ArrayBuffer 只是用於存儲二進制數據,如果要操作,則需要使用 視圖對象

視圖對象,不存儲任何數據,作用是將 ArrayBuffer 的數據做了結構化的處理,便於我們操作這些數據,說白了它們是操作二進制數據的接口。

視圖對象包括:

  • Uint8Array:每個 item 1 個位元組
  • Uint16Array:每個 item 2 個位元組
  • Uint32Array:每個 item 4 個位元組
  • Float64Array:每個 item 8 個位元組

按照上面的標準,一個 16 位元組 ArrayBuffer,可轉化的視圖對象和其長度為:

  • Uint8Array:長度 16
  • Uint16Array:長度 8
  • Uint32Array:長度 4
  • Float64Array:長度 2

這裡只是簡單介紹流數據在前端如何存儲,為的是避免你在瀏覽器看到一個長長的 ArrayBuffer 不知道它是什麼,記住它一定是二進制數據。

為什麼選 flv?

前面說到,直播需要實時性,延遲當然越短越好。當然決定傳輸速度的因素有很多,其中一個就是視頻數據本身的大小。

點播場景我們最常見的 mp4 格式,對前端是兼容性最好的。但是相對來說 mp4 的體積比較大,解析會複雜一些。在直播場景下這就是 mp4 的劣勢。

flv 就不一樣了,它的頭部文件非常小,結構簡單,解析起來又塊,在直播的實時性要求下非常有優勢,因此它成了最常用的直播方案之一。

當然除了 flv 之外還有其他格式,對應直播協議,我們一一對比一下:

  • RTMP: 底層基於 TCP,在瀏覽器端依賴 Flash。
  • HTTP-FLV: 基於 HTTP 流式 IO 傳輸 FLV,依賴瀏覽器支持播放 FLV。
  • WebSocket-FLV: 基於 WebSocket 傳輸 FLV,依賴瀏覽器支持播放 FLV。
  • HLS: Http Live Streaming,蘋果提出基於 HTTP 的流媒體傳輸協議。HTML5 可以直接打開播放。
  • RTP: 基於 UDP,延遲 1 秒,瀏覽器不支持。

其實早期常用的直播方案是 RTMP,兼容性也不錯,但是它依賴 Flash,而目前瀏覽器下 Flash 默認是被禁用的狀態,已經被時代淘汰的技術,因此不做考慮。

HLS 協議也很常見,對應視頻格式就是 m3u8。它是由蘋果推出,對手機支持非常好,但是致命缺點是延遲高(10~30 秒),因此也不做考慮。

RTP 不必說,瀏覽器不支持,剩下的就只有 flv 了。

但是 flv 又分為 HTTP-FLV 和 WebSocket-FLV,它兩看着像兄弟,又有什麼區別呢?

前面我們說過,直播流是實時傳輸,連接創建後不會斷,需要持續的推拉流。這種需要長連接的場景我們首先想到的方案自然是 WebSocket,因為 WebSocket 本來就是長連接實時互傳的技術。

不過呢隨着 js 原生能力擴展,出現了像 fetch 這樣比 ajax 更強的黑科技。它不光支持對我們更友好的 Promise,並且天生可以處理流數據,性能很好,而且使用起來也足夠簡單,對我們開發者來說更方便,因此就有了 http 版的 flv 方案。

綜上所述,最適合瀏覽器直播的是 flv,但是 flv 也不是萬金油,它的缺點是前端 video 標籤不能直接播放,需要經過處理才行。

處理方案,就是我們今天的主角:flv.js

協議與基礎實現

前面我們說到,flv 同時支持 WebSocket 和 HTTP 兩種傳輸方式,幸運的是,flv.js 也同時支持這兩種協議。

選擇用 http 還是 ws,其實功能和性能上差別不大,關鍵看後端同學給我們什麼協議吧。我這邊的選擇是 http,前後端處理起來都比較方便。

接下來我們介紹 flv.js 的具體接入流程,官網在這裡

假設現在有一個直播流地址:
http://test.stream.com/fetch-media.flv,第一步我們按照官網的快速開始建一個 demo:

import flvjs from 'flv.js'
if (flvjs.isSupported()) {
  var videoEl = document.getElementById('videoEl')
  var flvPlayer = flvjs.createPlayer({
    type: 'flv',
    url: 'http://test.stream.com/fetch-media.flv'
  })
  flvPlayer.attachMediaElement(videoEl)
  flvPlayer.load()
  flvPlayer.play()
}

首先安裝 flv.js,代碼的第一行是檢測瀏覽器是否支持 flv.js,其實大部分瀏覽器是支持的。接下來就是獲取 video 標籤的 DOM 元素。flv 會把處理後的 flv 流輸出給 video 元素,然後在 video 上實現視頻流播放。

接下來是關鍵之處,就是創建 flvjs.Player 對象,我們稱之為播放器實例。播放器實例通過 flvjs.createPlayer 函數創建,參數是一個配置對象,常用如下:

  • type:媒體類型,flv 或 mp4,默認 flv
  • isLive:可選,是否是直播流,默認 true
  • hasAudio:是否有音頻
  • hasVideo:是否有視頻
  • url:指定流地址,可以是 https(s) or ws(s)

上面的是否有音頻,視頻的配置,還是要看流地址是否有音視頻。比如監控流只有視頻流沒有音頻,那即便你配置 hasAudio: true 也是不可能有聲音的。

播放器實例創建之後,接下來就是三步走:

  • 掛載元素:flvPlayer.attachMediaElement(videoEl)
  • 加載流:flvPlayer.load()
  • 播放流:flvPlayer.play()

基礎實現流程就這麼多,下面再說一下處理過程中的細節和要點。

細節處理要點

基本 demo 跑起來了,但若想上生產環境,還需要處理一些關鍵問題。

暫停與播放

點播中的暫停與播放很容易,播放器下面會有一個播放/暫停按鍵,想什麼時候暫停都可以,再點播放的時候會接着上次暫停的地方繼續播放。但是直播中就不一樣了。

正常情況下直播應該是沒有播放/暫停按鈕以及進度條的。因為我們看的是實時信息,你暫停了視頻,再點播放的時候是不能從暫停的地方繼續播放的。為啥?因為你是實時的嘛,再點播放的時候應該是獲取最新的實時流,播放最新的視頻。

具體到技術細節,前端的 video 標籤默認是帶有進度條和暫停按鈕的,flv.js 將直播流輸出到 video 標籤,此時如果點擊暫停按鈕,視頻也是會停住的,這與點播邏輯一致。但是如果你再點播放,視頻還是會從暫停處繼續播放,這就不對了。

那麼我們換個角度,重新審視一下直播的播放/暫停邏輯。

直播為什麼需要暫停?拿我們視頻監控來說,一個頁面會放好幾個攝像頭的監控視頻,如果每個播放器一直與服務器保持連接,持續拉流,這會造成大量的連接和消耗,流失的都是白花花的銀子。

那我們是不是可以這樣:進去網頁的時候,找到想看的攝像頭,點擊播放再拉流。當你不想看的時候,點擊暫停,播放器斷開連接,這樣是不是就會節省無用的流量消耗。

因此,直播中的播放/暫停,核心邏輯是拉流/斷流

理解到這裡,那我們的方案應該是隱藏 video 的暫停/播放按鈕,然後自己實現播放和暫停的邏輯。

還是以上述代碼為例,播放器實例(上面的 flvPlayer 變量)不用變,播放/暫停代碼如下:

const onClick = isplay => {
  // 參數 isplay 表示當前是否正在播放
  if (isplay) {
    // 在播放,斷流
    player.unload()
    player.detachMediaElement()
  } else {
    // 已斷流,重新拉流播放
    player.attachMediaElement(videoEl.current)
    player.load()
    player.play()
  }
}

異常處理

用 flv.js 接入直播流的過程會遇到各種問題,有的是後端數據流的問題,有的是前端處理邏輯的問題。因為流是實時獲取,flv 也是實時轉化輸出,因此一旦發生錯誤,瀏覽器控制台會循環連續的打印異常。

如果你用 react 和 ts,滿屏異常,你都無法開發下去了。再有直播流本來就可能發生許多異常,因此錯誤處理非常關鍵。

官方對異常處理的說明不太明顯,我簡單總結一下:

首先,flv.js 的異常分為兩個級別,可以看作是 一級異常 和 二級異常。

再有,flv.js 有一個特殊之處,就是它的 事件 和 錯誤 都是用枚舉來表示,如下:

  • flvjs.Events:表示事件
  • flvjs.ErrorTypes:表示一級異常
  • flvjs.ErrorDetails:表示二級異常

下面介紹的異常和事件,都是基於上述枚舉,你可以理解為是枚舉下的一個 key 值。

一級異常有三類:

  • NETWORK_ERROR:網絡錯誤,表示連接問題
  • MEDIA_ERROR:媒體錯誤,格式或解碼問題
  • OTHER_ERROR:其他錯誤

二級級異常常用的有三類:

  • NETWORK_STATUS_CODE_INVALID:HTTP 狀態碼錯誤,說明 url 地址有誤
  • NETWORK_TIMEOUT:連接超時,網絡或後台問題
  • MEDIA_FORMAT_UNSUPPORTED:媒體格式不支持,一般是流數據不是 flv 的格式

了解這些之後,我們在播放器實例上監聽異常:

// 監聽錯誤事件
flvPlayer.on(flvjs.Events.ERROR, (err, errdet) => {
  // 參數 err 是一級異常,errdet 是二級異常
  if (err == flvjs.ErrorTypes.MEDIA_ERROR) {
    console.log('媒體錯誤')
    if(errdet == flvjs.ErrorDetails.MEDIA_FORMAT_UNSUPPORTED) {
      console.log('媒體格式不支持')
    }
  }
  if (err == flvjs.ErrorTypes.NETWORK_ERROR) {
    console.log('網絡錯誤')
    if(errdet == flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID) {
      console.log('http狀態碼異常')
    }
  }
  if(err == flvjs.ErrorTypes.OTHER_ERROR) {
    console.log('其他異常:', errdet)
  }
}

除此之外,自定義播放/暫停邏輯,還需要知道加載狀態。可以通過以下方法監聽視頻流加載完成:

player.on(flvjs.Events.METADATA_ARRIVED, () => {
  console.log('視頻加載完成')
})

樣式定製

為什麼會有樣式定製?前面我們說了,直播流的播放/暫停邏輯與點播不同,因此我們要隱藏 video 的操作欄元素,通過自定義元素來實現相關功能。

首先要隱藏播放/暫停按鈕,進度條,以及音量按鈕,用 css 實現即可:

/* 所有控件 */
video::-webkit-media-controls-enclosure {
  display: none;
}
/* 進度條 */
video::-webkit-media-controls-timeline {
  display: none;
}
video::-webkit-media-controls-current-time-display {
  display: none;
}
/* 音量按鈕 */
video::-webkit-media-controls-mute-button {
  display: none;
}
video::-webkit-media-controls-toggle-closed-captions-button {
  display: none;
}
/* 音量的控制條 */
video::-webkit-media-controls-volume-slider {
  display: none;
}
/*  播放按鈕 */
video::-webkit-media-controls-play-button {
  display: none;
}

播放和暫停的邏輯上面講了,樣式這邊自定義一個按鈕即可。除此之外我們還可能需要一個全屏按鈕,看一下全屏的邏輯怎麼寫:

const fullPage = () => {
  let dom = document.querySelector('.video')
  if (dom.requestFullscreen) {
    dom.requestFullscreen()
  } else if (dom.webkitRequestFullScreen) {
    dom.webkitRequestFullScreen()
  }
}

其他自定義樣式,比如你要做彈幕,在 video 上面蓋一層元素自行實現就可以了。

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/207514.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-08 14:34
下一篇 2024-12-08 14:34

相關推薦

發表回復

登錄後才能評論