Dans cet article, je souhaite partager mes tentatives de diffusion de vidéo via des websockets sans utiliser de plugins de navigateur tiers tels qu'Adobe Flash Player. Lisez la suite pour découvrir ce qui en est arrivé.
Adobe Flash, anciennement Macromedia Flash, est une plateforme permettant de créer des applications qui s'exécutent dans un navigateur Web. Avant l'introduction de l'API Media Stream, c'était pratiquement la seule plate-forme permettant de diffuser de la vidéo et de la voix à partir d'une webcam, ainsi que de créer divers types de conférences et de discussions dans le navigateur. Le protocole de transmission d'informations multimédias RTMP (Real Time Messaging Protocol) était en fait fermé depuis longtemps, ce qui signifiait : si vous souhaitez booster votre service de streaming, ayez la gentillesse d'utiliser le logiciel d'Adobe lui-même - Adobe Media Server (AMS).
Après un certain temps en 2012, Adobe « a abandonné et l'a craché » au public.
La plateforme Adobe Flash a plus de 20 ans, période au cours de laquelle de nombreuses vulnérabilités critiques ont été découvertes, supporte
Pour mon projet, j'ai immédiatement décidé d'abandonner complètement l'utilisation de Flash dans le navigateur. J'ai indiqué la raison principale ci-dessus : Flash n'est pas non plus du tout pris en charge sur les plates-formes mobiles et je ne voulais vraiment pas déployer Adobe Flash pour le développement sous Windows (émulateur Wine). J'ai donc décidé d'écrire un client en JavaScript. Ce ne sera qu'un prototype, car plus tard j'ai appris que le streaming peut être fait beaucoup plus efficacement sur la base du p2p, seulement pour moi ce sera peer - server - peers, mais nous en reparlerons une autre fois, car ce n'est pas encore prêt.
Pour commencer, nous avons besoin du serveur Websockets actuel. J'ai fait le plus simple basé sur le package melody go :
Code du 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)
})
// Бродкастим видео поток
m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
m.BroadcastBinary(msg)
})
log.Println("Starting server...")
http.ListenAndServe(":3000", r)
}
Côté client (côté streaming), vous devez d’abord accéder à la caméra. Cela se fait à travers
Nous obtenons l'accès (autorisation) à la caméra/au microphone via
Ensuite, getUserMedia() renvoie une promesse, à laquelle il transmet un objet MediaStream - un flux de données vidéo-audio. Nous attribuons cet objet à la propriété src de l'élément vidéo. Code:
Côté diffusion
<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 sur des sockets, vous devez l'encoder quelque part, le mettre en mémoire tampon et le transmettre en plusieurs parties. Le flux vidéo brut ne peut pas être transmis via des websockets. C'est là qu'il vient à notre aide
Nous encodons le flux vidéo, le divisons en parties
<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 la transmission via les websockets. Étonnamment, tout ce dont vous avez besoin est un objet
Nous 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>
Le côté diffusion est prêt ! Essayons maintenant de recevoir un flux vidéo et de l'afficher sur le client. De quoi avons-nous besoin pour cela ? Tout d’abord, bien sûr, la connexion par prise. Nous attachons un « écouteur » à l'objet WebSocket et nous abonnons à l'événement « message ». Après avoir reçu une donnée binaire, notre serveur la diffuse aux abonnés, c'est-à-dire aux clients. Dans ce cas, la fonction de rappel associée à « l'auditeur » de l'événement « message » est déclenchée sur le client ; l'objet lui-même est passé dans l'argument de la fonction - un morceau du flux vidéo codé par vp8.
Nous acceptons 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>
Pendant longtemps, j'ai essayé de comprendre pourquoi il est impossible d'envoyer immédiatement les morceaux reçus à l'élément vidéo pour les lire, mais il s'est avéré que cela ne peut pas être fait. Bien sûr, vous devez d'abord placer le morceau dans un tampon spécial lié à l'élément vidéo, et alors seulement il commencera à lire le flux vidéo. Pour cela vous aurez besoin
MediaSource agit comme une sorte d'intermédiaire entre l'objet de lecture multimédia et la source de ce flux multimédia. L'objet MediaSource contient un tampon enfichable pour la source du flux vidéo/audio. Une caractéristique est que le tampon ne peut contenir que des données Uint8, vous aurez donc besoin d'un FileReader pour créer un tel tampon. Regardez le code et cela deviendra plus clair :
Lecture 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 est en retard de 100 ms par rapport au côté émetteur ; nous l'avons réglé nous-mêmes lors de la division du flux vidéo avant de le transmettre au serveur. De plus, lorsque j'ai vérifié sur mon ordinateur portable, le décalage entre les côtés émetteur et récepteur s'est progressivement accumulé, cela était clairement visible. J'ai commencé à chercher des moyens de surmonter cet inconvénient et... je suis tombé sur
Source: habr.com