Streaming webcam (presque) inutile depuis un navigateur. Flux multimédia et Websockets

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. spécification Protocole RTMP, qui contenait des erreurs et était essentiellement incomplet. À cette époque, les développeurs ont commencé à créer leurs propres implémentations de ce protocole et le serveur Wowza est apparu. En 2011, Adobe a intenté une action en justice contre Wowza pour utilisation illégale des brevets liés au RTMP ; après 4 ans, le conflit a été résolu à l'amiable.

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 promis se terminera d’ici 2020, laissant peu d’alternatives au service de streaming.

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 API MediaStream.

Nous obtenons l'accès (autorisation) à la caméra/au microphone via API des appareils multimédias. Cette API fournit une méthode MediaDevices.getUserMedia(), qui affiche une fenêtre contextuelle. une fenêtre demandant à l'utilisateur l'autorisation d'accéder à la caméra et/ou au microphone. Je tiens à souligner que j'ai effectué toutes les expériences dans Google Chrome, mais je pense que tout fonctionnera à peu près de la même manière dans Firefox.

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 API MediaRecorder. Cette API vous permet d'encoder et de diviser le flux en morceaux. Je fais de l'encodage pour compresser le flux vidéo afin d'envoyer moins d'octets sur le réseau. Après l'avoir divisé en morceaux, vous pouvez envoyer chaque morceau vers une websocket. Code:

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 WebSocket. Il n'a que deux méthodes d'envoi et de fermeture. Les noms parlent d'eux-mêmes. Code ajouté :

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 API MediaSource и API FileReader.

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 API RTCPeerConnection, qui vous permet de transmettre un flux vidéo sans astuces telles que diviser le flux en morceaux. Le décalage accumulé, je pense, est dû au fait que le navigateur réencode chaque élément au format webm avant la transmission. Je n'ai pas creusé plus loin, mais j'ai commencé à étudier WebRTC. Je pense que j'écrirai un article séparé sur les résultats de mes recherches si je trouve cela intéressant pour la communauté.

Source: habr.com

Ajouter un commentaire