(Fast) nutzloses Webcam-Streaming über einen Browser. Medienstream und Websockets

In diesem Artikel möchte ich meine Versuche teilen, Videos über Websockets zu streamen, ohne Browser-Plugins von Drittanbietern wie Adobe Flash Player zu verwenden. Lesen Sie weiter, um herauszufinden, was dabei herausgekommen ist.

Adobe Flash, früher Macromedia Flash, ist eine Plattform zum Erstellen von Anwendungen, die in einem Webbrowser ausgeführt werden. Vor der Einführung der Media Stream API war sie praktisch die einzige Plattform zum Streamen von Video und Sprache von einer Webcam sowie zum Erstellen verschiedener Arten von Konferenzen und Chats im Browser. Das Protokoll zur Übertragung von Medieninformationen RTMP (Real Time Messaging Protocol) war eigentlich lange Zeit geschlossen, was bedeutete: Wenn Sie Ihren Streaming-Dienst steigern möchten, seien Sie so freundlich, Software von Adobe selbst zu verwenden – Adobe Media Server (AMS).

Nach einiger Zeit im Jahr 2012 gab Adobe „auf und spuckte es der Öffentlichkeit aus“. Spezifikation RTMP-Protokoll, das Fehler enthielt und im Wesentlichen unvollständig war. Zu diesem Zeitpunkt begannen Entwickler, ihre eigenen Implementierungen dieses Protokolls zu erstellen, und der Wowza-Server erschien. Im Jahr 2011 reichte Adobe eine Klage gegen Wowza wegen illegaler Nutzung von RTMP-bezogenen Patenten ein; nach vier Jahren wurde der Konflikt einvernehmlich beigelegt.

Die Adobe Flash-Plattform ist mehr als 20 Jahre alt, in dieser Zeit wurden viele kritische Schwachstellen entdeckt, Support versprochen bis 2020 enden, so dass für den Streaming-Dienst nur noch wenige Alternativen übrig bleiben.

Für mein Projekt habe ich mich sofort entschieden, komplett auf die Verwendung von Flash im Browser zu verzichten. Ich habe oben den Hauptgrund angegeben; Flash wird auch auf mobilen Plattformen überhaupt nicht unterstützt, und ich wollte Adobe Flash wirklich nicht für die Entwicklung unter Windows (Wine-Emulator) einsetzen. Also machte ich mich daran, einen Client in JavaScript zu schreiben. Dies wird nur ein Prototyp sein, da ich später erfahren habe, dass Streaming auf Basis von P2P viel effizienter erfolgen kann, nur für mich wird es Peer – Server – Peers sein, aber dazu ein anderes Mal mehr, da es noch nicht fertig ist.

Um zu beginnen, benötigen wir den eigentlichen Websockets-Server. Ich habe das einfachste basierend auf dem Melody-Go-Paket erstellt:

Servercode

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)
}

Auf dem Client (Streaming-Seite) müssen Sie zunächst auf die Kamera zugreifen. Dies geschieht durch MediaStream-API.

Wir erhalten Zugriff (Erlaubnis) auf die Kamera/das Mikrofon durch Mediengeräte-API. Diese API stellt eine Methode bereit MediaDevices.getUserMedia(), das ein Popup anzeigt. Ein Fenster, in dem der Benutzer um Erlaubnis zum Zugriff auf die Kamera und/oder das Mikrofon gebeten wird. Ich möchte anmerken, dass ich alle Experimente in Google Chrome durchgeführt habe, aber ich denke, dass in Firefox alles ungefähr gleich funktionieren wird.

Als nächstes gibt getUserMedia() ein Promise zurück, an das es ein MediaStream-Objekt übergibt – einen Stream von Video-Audio-Daten. Wir weisen dieses Objekt der src-Eigenschaft des Videoelements zu. Code:

Rundfunkseite

<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>

Um einen Videostream über Sockets zu übertragen, müssen Sie ihn irgendwo kodieren, puffern und in Teilen übertragen. Der Rohvideostream kann nicht über Websockets übertragen werden. Hier kommt es uns zu Hilfe MediaRecorder-API. Mit dieser API können Sie den Stream kodieren und in Stücke zerlegen. Ich führe eine Kodierung durch, um den Videostream zu komprimieren und so weniger Bytes über das Netzwerk zu senden. Nachdem Sie es in Stücke gebrochen haben, können Sie jedes Stück an einen Websocket senden. Code:

Wir kodieren den Videostream und zerlegen ihn in Teile

<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>

Fügen wir nun die Übertragung über Websockets hinzu. Überraschenderweise braucht man dafür nur einen Gegenstand WebSocket. Es gibt nur zwei Methoden: Senden und Schließen. Die Namen sprechen für sich. Code hinzugefügt:

Wir übermitteln den Videostream an den Server

<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>

Die Sendeseite ist fertig! Versuchen wir nun, einen Videostream zu empfangen und auf dem Client anzuzeigen. Was brauchen wir dafür? Erstens natürlich der Steckdosenanschluss. Wir hängen einen „Listener“ an das WebSocket-Objekt an und abonnieren das „message“-Ereignis. Nachdem unser Server ein Binärdatenstück empfangen hat, sendet es es an Abonnenten, also Clients. In diesem Fall wird die Rückruffunktion, die dem „Listener“ des „message“-Ereignisses zugeordnet ist, auf dem Client ausgelöst; das Objekt selbst wird an das Funktionsargument übergeben – ein Teil des von vp8 codierten Videostreams.

Wir akzeptieren Videostreams

<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>

Ich habe lange versucht zu verstehen, warum es unmöglich ist, die empfangenen Stücke sofort zur Wiedergabe an das Videoelement zu senden, aber es stellte sich heraus, dass dies nicht möglich ist. Sie müssen die Stücke natürlich zuerst in einen speziellen Puffer legen, an den sie gebunden sind das Videoelement und erst dann beginnt die Wiedergabe des Videostreams. Dafür benötigen Sie MediaSource-API и FileReader-API.

MediaSource fungiert als eine Art Vermittler zwischen dem Medienwiedergabeobjekt und der Quelle dieses Medienstreams. Das MediaSource-Objekt enthält einen steckbaren Puffer für die Quelle des Video-/Audiostreams. Eine Besonderheit besteht darin, dass der Puffer nur Uint8-Daten aufnehmen kann. Daher benötigen Sie einen FileReader, um einen solchen Puffer zu erstellen. Schauen Sie sich den Code an und es wird klarer:

Wiedergabe des Videostreams

<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>

Der Prototyp des Streaming-Dienstes ist fertig. Der Hauptnachteil besteht darin, dass die Videowiedergabe um 100 ms hinter der Sendeseite zurückbleibt; dies stellen wir selbst ein, wenn wir den Videostream vor der Übertragung an den Server aufteilen. Als ich außerdem auf meinem Laptop nachschaute, häufte sich die Verzögerung zwischen der Sende- und der Empfangsseite allmählich an, das war deutlich sichtbar. Ich begann nach Möglichkeiten zu suchen, diesen Nachteil zu überwinden, und... stieß darauf RTCPeerConnection-API, mit dem Sie einen Videostream ohne Tricks wie das Aufteilen des Streams in Stücke übertragen können. Die zunehmende Verzögerung ist meines Erachtens auf die Tatsache zurückzuführen, dass der Browser jedes Stück vor der Übertragung neu in das WebM-Format kodiert. Ich habe nicht weiter gegraben, sondern angefangen, WebRTC zu studieren. Ich denke, ich werde einen separaten Artikel über die Ergebnisse meiner Forschung schreiben, wenn ich sie für die Community interessant finde.

Source: habr.com

Kommentar hinzufügen