(Quasi) inutilità webcam streaming da un navigatore. Media Stream è Websockets

In questu articulu vogliu sparte i mo tentativi di streaming video via websockets senza utilizà plugins di navigatore di terzu partitu cum'è Adobe Flash Player. Leghjite per sapè ciò chì hè vinutu.

Adobe Flash, prima Macromedia Flash, hè una piattaforma per creà applicazioni chì funzionanu in un navigatore web. Prima di l'intruduzioni di l'API Media Stream, era praticamente l'unica piattaforma per streaming video è voce da una webcam, è ancu per creà diversi tipi di cunferenze è chats in u navigatore. U protokollu per a trasmissione di l'infurmazioni media RTMP (Protocolu di Messaging in Tempu Reale) hè statu in realtà chjusu per un bellu pezzu, chì significava: se vulete rinfurzà u vostru serviziu di streaming, sia abbastanza gentile per aduprà software da Adobe stessi - Adobe Media Server (AMS).

Dopu qualchì tempu in u 2012, Adobe "abbandunò è sputò" à u publicu. specificazione Protokollu RTMP, chì cuntene errori è era essenzialmente incomplete. À quellu tempu, i sviluppatori cuminciaru à fà e so propiu implementazioni di stu protokollu, è u servitore Wowza apparsu. In u 2011, Adobe hà presentatu una demanda contr'à Wowza per l'usu illegale di patenti in relazione à RTMP; dopu à 4 anni, u cunflittu hè statu risoltu amichevule.

A piattaforma Adobe Flash hè più di 20 anni d'età, durante quale tempu sò scuperti assai vulnerabili critichi, supportu prumessu per finisce da u 2020, lascendu uni pochi di alternative per u serviziu di streaming.

Per u mo prughjettu, aghju subitu decisu di abbandunà completamente l'usu di Flash in u navigatore. Aghju indicatu u mutivu principale sopra; Flash hè ancu micca supportatu in tuttu in e plataforme mobili, è ùn vulia micca implementà Adobe Flash per u sviluppu in Windows (emulatore di vinu). Allora aghju stabilitu per scrive un cliente in JavaScript. Questu serà solu un prototipu, postu chì dopu aghju amparatu chì u streaming pò esse fattu assai più efficacemente basatu nantu à p2p, solu per mè serà peer - server - peers, ma più nantu à questu un'altra volta, perchè ùn hè micca pronta.

Per cumincià, avemu bisognu di u servitore websockets attuale. Aghju fattu u più simplice basatu annantu à u pacchettu di melodia:

U codice di u servitore

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)
}

Nantu à u cliente (latu streaming), avete prima bisognu di accede à a camera. Questu hè fattu attraversu API MediaStream.

Avemu accessu (permissione) à a camera / microfonu attraversu API Media Devices. Questa API furnisce un metudu MediaDevices.getUserMedia(), chì mostra popup. una finestra chì dumanda à l'utilizatori l'autorizazione per accede à a camera è / o microfonu. Vogliu nutà chì aghju realizatu tutti l'esperimenti in Google Chrome, ma pensu chì tuttu hà da travaglià u listessu in Firefox.

Dopu, getUserMedia() torna una Promessa, à quale passa un oggettu MediaStream - un flussu di dati video-audio. Assignemu stu oggettu à a pruprietà src di l'elementu video. Codice:

Latu di trasmissione

<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>

Per trasmette un flussu di video nantu à sockets, avete bisognu di codificà in un locu, buffer, è trasmette in parte. U flussu di video crudu ùn pò micca esse trasmessu via websockets. Hè quì chì vene à u nostru aiutu API MediaRecorder. Questa API permette di codificà è rompe u flussu in pezzi. Facciu codificazione per cumpressà u flussu di video per mandà menu byte nantu à a reta. Dopu avè rottu in pezzi, pudete mandà ogni pezzu à un websocket. Codice:

Codificamu u flussu di video, rompemu in parti

<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>

Avà aghjunghje a trasmissione via websockets. Sorprendentemente, tuttu ciò chì avete bisognu per questu hè un ughjettu WebSockets. Hè solu dui metudi di mandà è chjude. I nomi parlanu per elli stessi. Codice aghjuntu:

Trasmettimu u flussu di video à u servitore

<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>

U latu di trasmissione hè pronta! Avà pruvemu à riceve un flussu di video è vede nantu à u cliente. Chì avemu bisognu per questu? Prima, sicuru, a cunnessione socket. Attachemu un "ascoltatore" à l'ughjettu WebSocket è abbonate à l'avvenimentu "messagiu". Dopu avè ricivutu un pezzu di dati binari, u nostru servitore trasmette à l'abbonati, vale à dì i clienti. In questu casu, a funzione di callback assuciata à l'"ascoltatore" di l'avvenimentu di "messagiu" hè attivata nantu à u cliente; l'ughjettu stessu hè passatu in l'argumentu di a funzione - un pezzu di u flussu video codificatu da vp8.

Accettamu u flussu di video

<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>

Per un bellu pezzu aghju pruvatu à capisce perchè hè impussibile di mandà immediatamente i pezzi ricivuti à l'elementu video per a riproduzione, ma hè risultatu chì questu ùn pò micca esse fattu, sicuru, prima deve mette u pezzu in un buffer speciale ligatu à l'elementu video, è solu allora accuminciarà à ghjucà u flussu di video. Per questu avete bisognu API MediaSource и FileReader API.

MediaSource agisce cum'è un tipu d'intermediariu trà l'ughjettu di riproduzione media è a fonte di stu flussu media. L'ughjettu MediaSource cuntene un buffer pluggable per a fonte di u flussu video / audio. Una funziunalità hè chì u buffer pò cuntene solu dati Uint8, cusì avete bisognu di un FileReader per creà un tali buffer. Fighjate u codice è diventerà più chjaru:

Riproduce u flussu di video

<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>

U prototipu di u serviziu di streaming hè prontu. U principale svantaghju hè chì a riproduzione di video ritardarà da u latu di trasmissione di 100 ms; l'avemu stabilitu noi stessi quandu si divide u flussu di video prima di trasmette à u servitore. Inoltre, quandu aghju verificatu in u mo laptop, u lag trà i lati di trasmissione è di ricezione s'hè accumulatu gradualmente, questu era chjaramente visibile. Aghju cuminciatu à circà modi per superà stu svantaghju, è... ghjuntu RTCPeerConnection API, chì permette di trasmette un flussu di video senza trucchi cum'è spliting the stream in pezzi. U lag accumulating, pensu, hè duvuta à u fattu chì u navigatore re-encode ogni pezzu in u formatu webm prima di trasmissione. Ùn aghju micca scavatu più, ma hà cuminciatu à studià WebRTC. Pensu chì scriveraghju un articulu separatu nantu à i risultati di a mo ricerca se mi pare interessante per a cumunità.

Source: www.habr.com

Add a comment