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“.
Die Adobe Flash-Plattform ist mehr als 20 Jahre alt, in dieser Zeit wurden viele kritische Schwachstellen entdeckt, Support
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
Wir erhalten Zugriff (Erlaubnis) auf die Kamera/das Mikrofon durch
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
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
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 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
Source: habr.com