Wie ich (fast) nutzloses Javascript-Webcam-Streaming gemacht habe

In diesem Artikel möchte ich meine Versuche zum Streamen von Videos über Websockets ohne Verwendung von Browser-Plugins von Drittanbietern wie Adobe Flash Player teilen. Was dabei herauskam, lesen Sie weiter.

Adobe Flash - früher Macromedia Flash - ist eine Plattform zum Erstellen von Anwendungen, die in einem Webbrowser ausgeführt werden. Vor der Implementierung der Media Stream-API war dies praktisch die einzige Plattform zum Streamen von Video und Sprache von einer Webcam sowie zum Erstellen verschiedener Konferenzen und Chats in einem Browser. Das Protokoll zur Übertragung von Medieninformationen RTMP (Real Time Messaging Protocol) war tatsächlich lange Zeit geschlossen, was bedeutete: Wenn Sie Ihren Streaming-Dienst erhöhen möchten, verwenden Sie bitte die Software von Adobe selbst - Adobe Media Server (AMS).

Nach einiger Zeit im Jahr 2012 hat Adobe die Spezifikation des RTMP-Protokolls „aufgegeben und ausgespuckt“, das Fehler enthielt und tatsächlich nicht vollständig war. Zu diesem Zeitpunkt begannen die Entwickler mit der Implementierung dieses Protokolls, sodass der Wowza-Server angezeigt wurde. Im Jahr 2011 reichte Adobe eine Klage gegen Wowza wegen illegaler Nutzung von Patenten im Zusammenhang mit RTMP ein. Nach vier Jahren wurde der Konflikt von der Welt gelöst.

Die Adobe Flash-Plattform gibt es seit mehr als 20 Jahren. In dieser Zeit wurden viele kritische Sicherheitslücken entdeckt. Sie versprachen , die Unterstützung bis 2020 einzustellen, sodass es nicht so viele Alternativen für den Streaming-Dienst gibt.

Für mein Projekt habe ich mich sofort entschieden, die Verwendung von Flash im Browser vollständig aufzugeben. Der Hauptgrund, den ich oben angegeben habe, ist, dass Flash auf mobilen Plattformen überhaupt nicht unterstützt wird, und ich wollte Adobe Flash wirklich nicht für die Entwicklung unter Windows (Wine Emulator) bereitstellen. Also fing ich an, einen Client in JavaScript zu schreiben. Dies wird nur ein Prototyp sein, da ich später erfahren habe, dass Streaming auf der Basis von P2P viel effizienter durchgeführt werden kann. Nur ich werde Peer-Server-Peers haben, aber ein anderes Mal mehr, da es noch nicht fertig ist.

Um loszulegen, benötigen wir den Websockets-Server selbst. Ich habe das einfachste basierend auf dem Melodie-Go-Paket gemacht:

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 (Broadcast-Seite) müssen Sie zuerst auf die Kamera zugreifen. Dies erfolgt über die MediaStream-API .

Über die Media Devices API erhalten wir Zugriff (Auflösung) auf die Kamera / das Mikrofon. Diese API stellt die MediaDevices.getUserMedia () -Methode bereit , die 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 darauf hinweisen, dass ich alle Experimente in Google Chrome durchgeführt habe, aber ich denke, in Firefox wird alles ungefähr auf die gleiche Weise funktionieren.

Als nächstes gibt getUserMedia () ein Versprechen zurück, an das ein MediaStream-Objekt übergeben wird - ein Strom von Video- und Audiodaten. Wir weisen dieses Objekt der Videoelement-Eigenschaft in src zu. Code:

Broadcast-Seite
 <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 senden, müssen Sie ihn irgendwo codieren, puffern und in Teilen übertragen. Der Rohvideostream kann nicht über Websockets übertragen werden. Hier hilft die MediaRecorder-API . Mit dieser API können Sie einen Stream codieren und in Teile zerlegen. Ich codiere, um den Videostream zu komprimieren, damit ich weniger Bytes über das Netzwerk fahren kann. Nach dem Aufbrechen ist es möglich, jedes Stück an den Websocket zu senden. Code:

Wir kodieren den Videostream und schlagen ihn in Stücke
 <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 Sie nun die Übertragung auf Websockets hinzu. Überraschenderweise erfordert dies nur ein WebSocket- Objekt. Es gibt nur zwei Sende- und Schließmethoden. Die Namen sprechen für sich. Erweiterter Code:

Wir übertragen 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 Rundfunkseite ist fertig! Versuchen wir nun, einen Videostream aufzunehmen und auf dem Client anzuzeigen. Was brauchen wir dafür? Zuallererst natürlich eine Steckdosenverbindung. Wir legen einen "Listener" für das WebSocket-Objekt auf und abonnieren das Ereignis "message". Nachdem unser Server Binärdaten empfangen hat, sendet er diese an Abonnenten, dh Clients. Gleichzeitig wird auf dem Client die Rückruffunktion ausgelöst, die mit dem „Listener“ des 'message'-Ereignisses verbunden ist. Das Objekt selbst wird an das Funktionsargument übergeben - ein Teil des von vp8 codierten Videostreams.

Wir akzeptieren einen Videostream
 <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 hat sich als unmöglich herausgestellt. Natürlich müssen Sie das Stück zuerst in einen speziellen Puffer legen, der an das Videoelement angehängt ist, und erst dann beginnt es mit der Wiedergabe des Videostreams. Dazu benötigen Sie die MediaSource-API und die FileReader-API .

MediaSource fungiert als eine Art Vermittler zwischen dem Medienwiedergabeobjekt und der Quelle dieses Medienstroms. Das MediaSource-Objekt enthält einen steckbaren Puffer für die Video- / Audio-Stream-Quelle. Eine Funktion ist, dass der Puffer nur Uint8-Daten enthalten kann, sodass FileReader erforderlich ist, um einen solchen Puffer zu erstellen. Werfen Sie einen Blick auf den Code und es wird klarer:

Spielen Sie den Videostream ab
 <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 ist, dass die Videowiedergabe 100 ms hinter der Sendeseite liegt. Wir stellen dies selbst ein, wenn wir den Videostream aufteilen, bevor wir ihn an den Server senden. Als ich meinen Laptop überprüfte, sammelte ich außerdem allmählich eine Verzögerung zwischen der Sende- und der Empfangsseite, die deutlich sichtbar war. Ich suchte nach Möglichkeiten, um dieses Manko zu überwinden, und ... stieß auf die RTCPeerConnection-API , mit der Sie einen Videostream ohne Tricks wie das Aufteilen eines Streams in Teile übertragen können. Die akkumulierte Verzögerung ist meiner Meinung nach auf die Tatsache zurückzuführen, dass im Browser vor der Übertragung jedes Stück in das WebM-Format transkodiert wird. Ich habe nicht mehr weiter gegraben, sondern angefangen, WebRTC zu studieren. Ich denke über die Ergebnisse meiner Forschung nach. Ich werde einen separaten Artikel schreiben, wenn ich diese Community interessant finde.

Source: https://habr.com/ru/post/de461769/


All Articles