Dans cet article, je souhaite partager mes tentatives d'effectuer un streaming vidéo via WebSockets sans utiliser de plug-ins tiers pour le navigateur comme Adobe Flash Player. Ce qui en résulte, lisez la suite.
Adobe Flash — anciennement Macromedia Flash, est une plateforme de création d'applications fonctionnant dans un navigateur web. Avant l'introduction de l'API Media Stream, c'était pratiquement la seule plateforme pour le streaming vidéo et audio à partir d'une webcam, ainsi que pour créer divers types de conférences et de discussions dans un navigateur. Le protocole de transmission des informations multimédias RTMP (Real Time Messaging Protocol) a longtemps été fermé, ce qui signifiait : si vous souhaitez lancer votre service de streaming, merci d'utiliser le logiciel d'Adobe — Adobe Media Server (AMS).
Après un certain temps, en 2012, Adobe a "cédé et a craché" au public le protocole RTMP, qui contenait des erreurs et était en réalité incomplet. À ce moment-là, les développeurs ont commencé à réaliser leurs propres implémentations de ce protocole, ainsi est né le serveur Wowza. En 2011, Adobe a poursuivi Wowza pour utilisation illégale de brevets liés à RTMP, et après 4 ans, le conflit a été résolu à l'amiable.
La plateforme Adobe Flash existe depuis plus de 20 ans, et au fil du temps, de nombreuses vulnérabilités critiques ont été découvertes, le support devait être arrêté d'ici 2020, donc les alternatives pour les services de streaming ne sont pas si nombreuses.
Pour mon projet, j'ai immédiatement décidé de me passer complètement de Flash dans le navigateur. La raison principale est mentionnée ci-dessus, en outre, Flash n'est pas du tout pris en charge sur les plateformes mobiles, et je ne voulais absolument pas déployer Adobe Flash pour le développement sur Windows (avec un émulateur Wine). J'ai donc commencé à écrire un client en JavaScript. Ce ne sera qu'un prototype, car j'ai ensuite appris que le streaming peut être rendu beaucoup plus efficace sur la base du P2P, mais ce sera un peer — server — peers, mais nous en parlerons une autre fois, car ce n'est pas encore prêt.
Pour commencer, nous avons besoin d'un serveur WebSockets. J'en ai créé un très simple basé sur le package Go Melody :
Code de la partie serveur
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)
})
// Diffuser le flux vidéo
m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
m.BroadcastBinary(msg)
})
log.Println("Démarrage du serveur...")
http.ListenAndServe(":3000", r)
}
Du côté du client (la partie qui diffuse), il est d'abord nécessaire d'accéder à la caméra. Cela se fait via .
Nous obtenons l'accès (autorisation) à la caméra/microphone via . Cette API fournit la méthode , qui affiche une fenêtre contextuelle demandant à l'utilisateur l'autorisation d'accéder à la caméra et/ou au microphone. Je tiens à préciser que tous les tests que j'ai effectués l'ont été dans Google Chrome, mais je pense que cela fonctionnera de manière similaire dans Firefox.
Ensuite, getUserMedia() renvoie une Promise, dans laquelle il transmet un objet MediaStream — un flux de données audio-vidéo. Cet objet est assigné à la propriété src de l'élément vidéo. Code :
La partie diffuseuse
<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>
Pour diffuser un flux vidéo via des sockets, il faut l'encoder, le tamponner et le transmettre par morceaux. Un flux vidéo brut ne peut pas être envoyé par websockets. Ici nous introduisons . Cette API permet d'encoder et de découper le flux en morceaux. L'encodage que je fais vise à compresser le flux vidéo afin de réduire le volume de données circulant sur le réseau. Une fois découpés en morceaux, chaque morceau peut être envoyé par websocket. Code :
Nous encodons le flux vidéo et le découpons
<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>
Ajoutons maintenant l'envoi via websockets. Étonnamment, il suffit d'un objet . Il n'a que deux méthodes : send et close. Les noms parlent d'eux-mêmes. Code complété :
Transmettons le flux vidéo au serveur
<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>
La partie diffuseuse est prête ! Essayons maintenant de recevoir le flux vidéo et de l'afficher sur le client. De quoi avons-nous besoin pour cela ? Tout d'abord, bien sûr, d'une connexion socket. Nous attachons un « auditeur » à l'objet WebSocket, nous nous abonnissons à l'événement 'message'. Lorsqu'un morceau de données binaires est reçu, notre serveur le diffuse à ses abonnés, c'est-à-dire aux clients. Sur le client, cela déclenche une fonction de rappel liée à l'auditeur de l'événement 'message', où l'argument de la fonction est le morceau de flux vidéo, encodé en vp8.
Recevons le flux vidéo
<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>
J'ai longtemps essayé de comprendre pourquoi on ne pouvait pas immédiatement envoyer les morceaux obtenus à l'élément video pour les reproduire, mais il s'avère qu'en fait, ce n'est pas possible. Il faut d'abord placer le morceau dans un tampon spécial, lié à l'élément video, et ce n'est qu'ensuite qu'il commencera à reproduire le flux vidéo. Pour cela, il faudra et .
MediaSource agit comme un intermédiaire entre l'objet de lecture multimédia et la source de ce flux média. L'objet MediaSource contient un tampon connecté pour la source du flux vidéo/audio. Une particularité est que le tampon ne peut contenir que des données de type Uint8, donc pour créer un tel tampon, un FileReader sera nécessaire. Regardez le code, et cela deviendra plus clair :
Reproduction du flux vidéo
<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>
Le prototype du service de streaming est prêt. Le principal inconvénient est que la lecture vidéo sera décalée par rapport à la source de transmission de 100 ms, ce que nous avons réglé nous-mêmes lors de la segmentation du flux vidéo avant son envoi au serveur. De plus, lorsque j'ai vérifié sur mon ordinateur portable, un lag s'accumulait progressivement entre le côté transmetteur et le côté récepteur, ce qui était très visible. J'ai commencé à chercher des moyens de surmonter ce défaut et... je suis tombé sur , qui permet de transmettre le flux vidéo sans la nécessité de segmenter le flux. Je pense que le lag accumulé est dû au fait qu'une ré-encodage de chaque morceau en format webm se produit dans le navigateur avant la transmission. Je n'ai pas cherché plus loin et j'ai commencé à étudier WebRTC. Je pense que je rédigerai un article séparé sur les résultats de mes recherches si je les juge intéressants pour la communauté.
Source : habr.com
