In diesem Artikel möchte ich meine Versuche teilen, Video-Streaming über Websockets ohne die Verwendung von Drittanbieter-Browser-Plugins wie Adobe Flash Player zu ermöglichen. Was dabei herausgekommen ist, lesen Sie weiter.
Adobe Flash – früher Macromedia Flash – ist eine Plattform zur Erstellung von Anwendungen, die im Webbrowser funktionieren. Bis zur Einführung der Media Stream API war dies praktisch die einzige Plattform für Webcam-Streaming von Video und Ton sowie für die Erstellung von Konferenzen und Chats im Browser. Protokoll für die Übertragung von Medieninformationen ist RTMP (Real Time Messaging Protocol), das lange Zeit faktisch geschlossen war, was bedeutete: Wenn Sie Ihren Streaming-Service aufbauen wollten, mussten Sie die Software von Adobe selbst verwenden – Adobe Media Server (AMS).
Nach einiger Zeit, im Jahr 2012, gab Adobe „auf und präsentierte“ der Öffentlichkeit des RTMP-Protokolls, die Fehler enthielt und im Grunde genommen unvollständig war. Zu diesem Zeitpunkt begannen Entwickler, ihre eigenen Implementierungen dieses Protokolls zu erstellen, so entstand der Wowza-Server. Im Jahr 2011 reichte Adobe eine Klage gegen Wowza wegen unrechtmäßiger Nutzung von mit RTMP verbundenen Patenten ein, vier Jahre später wurde der Konflikt friedlich gelöst.
Adobe Flash ist seit über 20 Jahren auf dem Markt und hat in dieser Zeit zahlreiche kritische Sicherheitsanfälligkeiten aufgezeigt. Die Unterstützung versprochen, bis 2020 eingestellt zu werden, sodass es nicht viele Alternativen für Streaming-Dienste gibt.
Für mein Projekt habe ich mich sofort entschieden, komplett auf Flash im Browser zu verzichten. Die Hauptgründe habe ich oben bereits genannt; zudem wird Flash auf mobilen Plattformen gar nicht unterstützt und es war mir auch nicht recht, Adobe Flash unter Windows (im Wine-Emulator) einzusetzen. Daher habe ich damit begonnen, 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 gestaltet werden kann. Dabei werde ich ein Peer-Server-Peers-Modell verwenden, aber dazu später mehr, da dies noch nicht fertig ist.
Zunächst benötigen wir einen eigenen WebSockets-Server. Ich habe einen sehr einfachen auf Basis des Go-Pakets melody erstellt:
Server-Code
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)
})
// Wir senden den Video-Stream aus
m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
m.BroadcastBinary(msg)
})
log.Println("Server startet...")
http.ListenAndServe(":3000", r)
}
Auf der Client-Seite (Sender-Seite) muss zunächst auf die Kamera zugegriffen werden. Dies geschieht über .
Wir erhalten Zugriff (Erlaubnis) auf die Kamera/den Mikrofon über . Diese API bietet die Methode , die ein Popup-Fenster anzeigt und den Benutzer um Erlaubnis für den Zugriff auf die Kamera und/oder das Mikrofon fragt. Ich möchte bemerken, dass ich alle Experimente in Google Chrome durchgeführt habe, aber ich denke, dass es in Firefox ähnlich funktionieren wird.
Anschließend gibt getUserMedia() ein Promise zurück, das ein MediaStream-Objekt — einen Video-Audio-Datenstrom — überträgt. Dieses Objekt weisen wir der src-Eigenschaft des Video-Elements zu. Der Code:
Die Sender-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 Video-Stream über Sockets zu übertragen, muss er in irgendeiner Weise kodiert, zwischengespeichert und in Teilen gesendet werden. Ein roher Video-Stream kann nicht über Websockets übertragen werden. Hier kommt uns zur Hilfe. Dieses API ermöglicht es, den Stream zu kodieren und in Stücke zu zerlegen. Das Kodieren erfolgt zur Kompression des Video-Streams, um weniger Bytes über das Netzwerk zu senden. Indem wir ihn in Stücke zerlegen, kann jedes Stück in ein Websocket gesendet werden. Code:
Wir kodieren den Video-Stream und zerteilen ihn
<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>
Jetzt fügen wir die Übertragung über Websockets hinzu. Überraschenderweise benötigen wir dafür nur ein Objekt . Es hat nur zwei Methoden: send und close. Die Namen sprechen für sich. Ergänzter Code:
Wir senden den Video-Stream 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 Streaming-Seite ist bereit! Jetzt probieren wir, den Video-Stream zu empfangen und ihn auf dem Client anzuzeigen. Was benötigen wir dafür? Zunächst einmal eine Socket-Verbindung. Wir fügen dem WebSocket-Objekt einen "Listener" hinzu und abonnieren das 'message'-Ereignis. Sobald unser Server ein Stück binärer Daten erhält, sendet er es an seine Abonnenten, also an die Clients. Auf dem Client wird dann die Callback-Funktion ausgelöst, die mit dem 'message'-Ereignis verbunden ist, und das passende Objekt — das Stück Video-Stream, codiert mit vp8 — wird als Argument an die Funktion übergeben.
Video-Stream empfangen
<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>
Lange Zeit habe ich versucht zu verstehen, warum man die empfangenen Stücke nicht sofort an das video-Element zur Wiedergabe senden kann, aber es stellte sich heraus, dass man das nicht so machen kann. Zuerst muss das Stück in einen speziellen Puffer gelegt werden, der mit dem video-Element verbunden ist, und erst dann wird der Video-Stream wiedergegeben. Dafür benötigen wir und .
MediaSource fungiert als Vermittler zwischen dem Wiedergabeobjekt und der Quelle des Medienstreams. Das MediaSource-Objekt enthält einen Puffer, der mit der Video-/Audioquelle verbunden ist. Eine Besonderheit ist, dass der Puffer nur Daten vom Typ Uint8 enthalten kann, weshalb zur Erstellung eines solchen Puffers ein FileReader erforderlich ist. Schauen Sie sich den Code an, dann wird es klarer:
Videostream wird wiedergegeben
<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 Videoausgabe gegenüber der sendenden Seite um 100 ms verzögert ist, was wir selbst festgelegt haben, als wir den Videostream vor der Übertragung an den Server aufgeteilt haben. Darüber hinaus habe ich bemerkt, dass sich auf meinem Laptop im Laufe der Zeit ein Lag zwischen sendender und empfangender Seite aufgebaut hat, was deutlich zu sehen war. Ich begann, nach Möglichkeiten zu suchen, um dieses Problem zu beheben, und... stieß auf , das eine nahtlose Übertragung von Videostreams ermöglicht, ohne die stream in kleinere Teile aufzuteilen. Der angesammelte Lag entsteht, weil im Browser vor der Übertragung jeder Teil in das Format webm umkodiert wird. Ich wollte nicht tiefer eingreifen, also begann ich, mich mit WebRTC zu beschäftigen. Ich werde über die Ergebnisse meiner Recherchen einen separaten Artikel schreiben, wenn ich denke, dass es für die Community interessant ist.
Quelle: habr.com
