(幾乎)無用的網絡攝像頭從瀏覽器流式傳輸。 媒體流和 Websockets

在本文中,我想分享我在不使用 Adob​​e Flash Player 等第三方瀏覽器插件的情況下透過 Websocket 串流影片的嘗試。 請繼續閱讀以了解結果。

Adobe Flash(以前稱為 Macromedia Flash)是一個用於建立在 Web 瀏覽器中執行的應用程式的平台。 在引入 Media Stream API 之前,它實際上是唯一用於從網路攝影機傳輸視訊和語音以及在瀏覽器中建立各種會議和聊天的平台。 傳輸媒體訊息的協定RTMP(即時訊息協定)實際上已經關閉了很長一段時間,這意味著:如果你想提升你的串流服務,請善意地使用Adobe自己的軟體-Adobe Media Server (AMS )。

2012 年一段時間後,Adobe 向公眾「放棄並吐出了它」。 規格 RTMP 協定包含錯誤且本質上不完整。 到那時,開發人員開始自己實現該協議,Wowza 伺服器出現了。 2011年,Adobe對Wowza提起訴訟,指控其非法使用RTMP相關專利;4年後,衝突已友善解決。

Adobe Flash 平台已有 20 多年的歷史,在此期間發現了許多嚴重漏洞,支持 答應的 到 2020 年結束,串流媒體服務幾乎沒有其他選擇。

對於我的項目,我立即決定完全放棄在瀏覽器中使用 Flash。 我在上面指出了主要原因;行動平台上根本不支援Flash,我真的不想在Windows(wine模擬器)上部署Adobe Flash進行開發。 所以我開始用 JavaScript 寫一個客戶端。 這只是一個原型,因為後來我了解到基於 p2p 可以更有效地完成串流媒體,僅對我來說它將是對等 - 伺服器 - 對等,但下次會詳細介紹,因為它還沒有準備好。

首先,我們需要實際的 websockets 伺服器。 我根據melody go包做了一個最簡單的:

伺服器程式碼

package main

import (
	"errors"
	"github.com/go-chi/chi"
	"gopkg.in/olahol/melody.v1"
	"log"
	"net/http"
	"time"
)

func main() {
	r := chi.NewRouter()
	m := melody.New()

	m.Config.MaxMessageSize = 204800

	r.Get("/", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, "public/index.html")
	})
	r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
		m.HandleRequest(w, r)
	})

         // Бродкастим видео поток 
	m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
		m.BroadcastBinary(msg)
	})

	log.Println("Starting server...")

	http.ListenAndServe(":3000", r)
}

在客戶端(串流媒體端),您首先需要存取攝影機。 這是透過 媒體流API.

我們透過以下方式獲得對攝影機/麥克風的存取(許可) 媒體裝置API。 這個API提供了一個方法 MediaDevices.getUserMedia(),顯示彈出視窗。 一個窗口,詢問用戶是否允許訪問攝影機和/或麥克風。 我想指出的是,我在 Google Chrome 中進行了所有實驗,但我認為在 Firefox 中一切都會相同。

接下來,getUserMedia() 傳回一個 Promise,並向其傳遞一個 MediaStream 物件 - 視訊音訊資料流。 我們將此物件指派給視訊元素的 src 屬性。 代碼:

轉播方

<style>
  #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Здесь в этом "окошечке" клиент будет видеть себя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>

<script type="application/javascript">
  var
        video = document.getElementById('videoObjectHtml5ApiServer');

// если доступен MediaDevices API, пытаемся получить доступ к камере (можно еще и к микрофону)
// getUserMedia вернет обещание, на которое подписываемся и полученный видеопоток в колбеке направляем в video объект на странице

if (navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
          // видео поток привязываем к video тегу, чтобы клиент мог видеть себя и контролировать 
          video.srcObject = stream;
        });
}
</script>

要透過套接字廣播視訊串流,您需要在某處對其進行編碼、緩衝並分段傳輸。 原始視訊串流無法透過 websocket 傳輸。 這就是我們需要幫助的地方 媒體記錄器 API。 該 API 允許您對流進行編碼並將其分解為多個片段。 我進行編碼來壓縮視訊串流,以便透過網路發送更少的位元組。 將其分成幾部分後,您可以將每一部分發送到 websocket。 代碼:

我們對視訊串流進行編碼,將其分成幾個部分

<style>
  #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Здесь в этом "окошечке" клиент будет видеть себя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>

<script type="application/javascript">
  var
        video = document.getElementById('videoObjectHtml5ApiServer');

// если доступен MediaDevices API, пытаемся получить доступ к камере (можно еще и к микрофону)
// getUserMedia вернет обещание, на которое подписываемся и полученный видеопоток в колбеке направляем в video объект на странице

if (navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
          // видео поток привязываем к video тегу, чтобы клиент мог видеть себя и контролировать 
          video.srcObject = s;
          var
            recorderOptions = {
                mimeType: 'video/webm; codecs=vp8' // будем кодировать видеопоток в формат webm кодеком vp8
              },
              mediaRecorder = new MediaRecorder(s, recorderOptions ); // объект MediaRecorder

               mediaRecorder.ondataavailable = function(e) {
                if (e.data && e.data.size > 0) {
                  // получаем кусочек видеопотока в e.data
                }
            }

            mediaRecorder.start(100); // делит поток на кусочки по 100 мс каждый

        });
}
</script>

現在讓我們透過 websocket 添加傳輸。 令人驚訝的是,你所需要的只是一個對象 WebSocket的。 它只有發送和關閉兩個方法。 這些名字不言而喻。 新增的程式碼:

我們將視訊串流傳輸到伺服器

<style>
  #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Здесь в этом "окошечке" клиент будет видеть себя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>

<script type="application/javascript">
  var
        video = document.getElementById('videoObjectHtml5ApiServer');

// если доступен MediaDevices API, пытаемся получить доступ к камере (можно еще и к микрофону)
// getUserMedia вернет обещание, на которое подписываемся и полученный видеопоток в колбеке направляем в video объект на странице

if (navigator.mediaDevices.getUserMedia) {
        navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
          // видео поток привязываем к video тегу, чтобы клиент мог видеть себя и контролировать 
          video.srcObject = s;
          var
            recorderOptions = {
                mimeType: 'video/webm; codecs=vp8' // будем кодировать видеопоток в формат webm кодеком vp8
              },
              mediaRecorder = new MediaRecorder(s, recorderOptions ), // объект MediaRecorder
              socket = new WebSocket('ws://127.0.0.1:3000/ws');

               mediaRecorder.ondataavailable = function(e) {
                if (e.data && e.data.size > 0) {
                  // получаем кусочек видеопотока в e.data
                 socket.send(e.data);
                }
            }

            mediaRecorder.start(100); // делит поток на кусочки по 100 мс каждый

        }).catch(function (err) { console.log(err); });
}
</script>

轉播端已準備就緒! 現在讓我們嘗試接收視訊串流並將其顯示在客戶端上。 為此我們需要什麼? 首先,當然是套接字連結。 我們將「偵聽器」附加到 WebSocket 物件並訂閱「訊息」事件。 收到一段二進位資料後,我們的伺服器將其廣播給訂閱者,即客戶端。 在這種情況下,與「訊息」事件的「偵聽器」關聯的回調函數在客戶端被觸發;物件本身被傳遞到函數參數中 - 由 vp8 編碼的視訊流片段。

我們接受視訊串流

<style>
  #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Здесь в этом "окошечке" клиент будет видеть тебя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>

<script type="application/javascript">
  var
        video = document.getElementById('videoObjectHtml5ApiServer'),
         socket = new WebSocket('ws://127.0.0.1:3000/ws'), 
         arrayOfBlobs = [];

         socket.addEventListener('message', function (event) {
                // "кладем" полученный кусочек в массив 
                arrayOfBlobs.push(event.data);
                // здесь будем читать кусочки
                readChunk();
            });
</script>

很長一段時間我試圖理解為什麼不可能立即將接收到的片段發送到視頻元素進行播放,但事實證明這是無法做到的,當然,你必須先將片段放入綁定到的特殊緩衝區中video 元素,只有這樣它才會開始播放視訊串流。 為此你需要 媒體來源API и 檔案讀取器API.

MediaSource 充當媒體播放物件和媒體串流源之間的一種中介。 MediaSource 物件包含視訊/音訊串流來源的可插入緩衝區。 一個特點是緩衝區只能保存 Uint8 數據,因此您需要一個 FileReader 來建立這樣的緩衝區。 看一下程式碼就更清楚了:

播放視訊串流

<style>
  #videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- Здесь в этом "окошечке" клиент будет видеть тебя -->
<video autoplay id="videoObjectHtml5ApiServer"></video>

<script type="application/javascript">
  var
        video = document.getElementById('videoObjectHtml5ApiServer'),
         socket = new WebSocket('ws://127.0.0.1:3000/ws'),
        mediaSource = new MediaSource(), // объект MediaSource
        vid2url = URL.createObjectURL(mediaSource), // создаем объект URL для связывания видеопотока с проигрывателем
        arrayOfBlobs = [],
        sourceBuffer = null; // буфер, пока нуль-объект

         socket.addEventListener('message', function (event) {
                // "кладем" полученный кусочек в массив 
                arrayOfBlobs.push(event.data);
                // здесь будем читать кусочки
                readChunk();
            });

         // как только MediaSource будет оповещен , что источник готов отдавать кусочки 
        // видео/аудио потока
        // создаем буфер , следует обратить внимание, что буфер должен знать в каком формате 
        // каким кодеком был закодирован поток, чтобы тем же способом прочитать видеопоток
         mediaSource.addEventListener('sourceopen', function() {
            var mediaSource = this;
            sourceBuffer = mediaSource.addSourceBuffer("video/webm; codecs="vp8"");
        });

      function readChunk() {
        var reader = new FileReader();
        reader.onload = function(e) { 
          // как только FileReader будет готов, и загрузит себе кусочек видеопотока
          // мы "прицепляем" перекодированный в Uint8Array (был Blob) кусочек в буфер, связанный
          // с проигрывателем, и проигрыватель начинает воспроизводить полученный кусочек видео/аудио
          sourceBuffer.appendBuffer(new Uint8Array(e.target.result));

          reader.onload = null;
        }
        reader.readAsArrayBuffer(arrayOfBlobs.shift());
      }
</script>

串流媒體服務的原型已經準備就緒。 主要缺點是視訊播放會落後於發送端100ms;我們在將視訊串流傳輸到伺服器之前分割視訊串流時自己設定這個。 而且,當我檢查我的筆記型電腦時,發送端和接收端之間的延遲逐漸累積,這一點清晰可見。 我開始尋找克服這個缺點的方法,並且......遇到了 RTCPeerConnection API,它允許您傳輸視訊串流,而無需使用諸如將串流分割成片段之類的技巧。 我認為,累積的延遲是由於瀏覽器在傳輸之前將每個片段重新編碼為 webm 格式造成的。 我沒有進一步挖掘,而是開始研究 WebRTC。如果我發現社群對我的研究結果感興趣,我想我會寫一篇單獨的文章。

來源: www.habr.com

添加評論