ブラウザからの(ほぼ)役に立たないウェブカメラストリーミング。 メディアストリームとWebSocket

この記事では、Adobe Flash Player などのサードパーティのブラウザ プラグインを使用せずに、WebSocket 経由でビデオをストリーミングする試みを共有したいと思います。 それから何が起こったのかを知るために読んでください。

Adobe Flash (旧名 Macromedia Flash) は、Web ブラウザで実行されるアプリケーションを作成するためのプラットフォームです。 Media Stream API が導入される前は、Web カメラからビデオや音声をストリーミングしたり、ブラウザでさまざまな種類の会議やチャットを作成したりするための事実上唯一のプラットフォームでした。 メディア情報を送信するためのプロトコル RTMP (Real Time Messaging Protocol) は、実際には長い間閉鎖されていました。これは、ストリーミング サービスを強化したい場合は、Adobe 自体のソフトウェアである Adob​​e Media Server (AMS) を使用することを意味します。

2012 年になってしばらくして、Adobe は「あきらめて、それを一般に吐き出しました」。 仕様 RTMP プロトコルにはエラーが含まれており、本質的に不完全でした。 その時までに、開発者はこのプロトコルの独自の実装を作成し始め、Wowza サーバーが登場しました。 2011 年、Adobe は RTMP 関連特許の違法使用で Wowza に対して訴訟を起こし、4 年後に紛争は友好的に解決されました。

Adobe Flash プラットフォームは 20 年以上前から存在しており、その間に多くの重大な脆弱性が発見され、サポートされています。 約束した 2020年までに終了する予定で、ストリーミングサービスの代替手段はほとんど残されていない。

私のプロジェクトでは、ブラウザでの Flash の使用を完全に放棄することにすぐに決めました。 上で主な理由を示しましたが、Flash はモバイル プラットフォームでもまったくサポートされておらず、Windows (ワイン エミュレータ) での開発のために Adob​​e Flash を導入したくありませんでした。 そこで、JavaScript でクライアントを作成することにしました。 これは単なるプロトタイプになります。後で、ストリーミングは p2p に基づいてはるかに効率的に実行できることを知りました。私にとってはピア - サーバー - ピアになるだけですが、まだ準備ができていないため、これについては別の機会に説明します。

始めるには、実際の WebSocket サーバーが必要です。 私は、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 オブジェクト (ビデオ/オーディオ データのストリーム) を渡します。 このオブジェクトを video 要素の 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の。 メソッドは send と close の XNUMX つだけです。 名前自体がそれを物語っています。 追加されたコード:

ビデオストリームをサーバーに送信します

<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 オブジェクトには、ビデオ/オーディオ ストリームのソース用のプラグ可能バッファが含まれています。 特徴の 8 つは、バッファーが保持できるのは UintXNUMX データのみであるため、そのようなバッファーを作成するには 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>

ストリーミングサービスのプロトタイプが完成しました。 主な欠点は、ビデオの再生が送信側より 100 ミリ秒遅れることです。サーバーに送信する前にビデオ ストリームを分割するときに、これを自分で設定します。 さらに、ノートパソコンで確認してみると、送信側と受信側のラグが徐々に蓄積されていくのがはっきりと分かりました。 このデメリットを克服する方法を探し始めたところ、見つけました。 RTCPeerConnection APIを使用すると、ストリームを分割するなどのトリックを行わずにビデオ ストリームを送信できます。 遅延が蓄積するのは、ブラウザが送信前に各部分を WebM 形式に再エンコードするためだと思います。 私はそれ以上掘り下げず、WebRTC について勉強し始めました。コミュニティにとって興味深いと感じたら、研究結果について別の記事を書こうと思います。

出所: habr.com

コメントを追加します