(Takmer) zbytočné streamovanie webkamery z prehliadača. Stream médií a webové zásuvky

V tomto článku sa chcem podeliť o svoje pokusy o streamovanie videa cez websockets bez použitia doplnkov prehliadača tretích strán, ako je Adobe Flash Player. Čítajte ďalej a zistite, čo z toho vzniklo.

Adobe Flash, predtým Macromedia Flash, je platforma na vytváranie aplikácií, ktoré bežia vo webovom prehliadači. Pred zavedením Media Stream API to bola prakticky jediná platforma na streamovanie videa a hlasu z webovej kamery, ako aj na vytváranie rôznych druhov konferencií a chatov v prehliadači. Protokol na prenos mediálnych informácií RTMP (Real Time Messaging Protocol) bol v skutočnosti na dlhú dobu uzavretý, čo znamenalo: ak chcete zlepšiť svoju streamovaciu službu, buďte tak láskaví a použite softvér od samotnej spoločnosti Adobe - Adobe Media Server (AMS).

Po nejakom čase v roku 2012 to Adobe „vzdalo a vypľulo to“ verejnosti. špecifikácia RTMP protokol, ktorý obsahoval chyby a bol v podstate neúplný. V tom čase vývojári začali vytvárať svoje vlastné implementácie tohto protokolu a objavil sa server Wowza. V roku 2011 spoločnosť Adobe podala žalobu na spoločnosť Wowza za nezákonné používanie patentov súvisiacich s RTMP; po 4 rokoch sa konflikt vyriešil priateľsky.

Platforma Adobe Flash je stará viac ako 20 rokov a počas tejto doby bolo objavených mnoho kritických zraniteľností, podpora sľúbil skončiť do roku 2020, pričom pre streamovaciu službu zostane len málo alternatív.

Pre svoj projekt som sa okamžite rozhodol úplne opustiť používanie Flash v prehliadači. Hlavný dôvod som uviedol vyššie; Flash tiež nie je vôbec podporovaný na mobilných platformách a naozaj som nechcel nasadiť Adobe Flash na vývoj v systéme Windows (emulátor vína). Tak som sa rozhodol napísať klienta v JavaScripte. Toto bude len prototyp, keďže neskôr som sa dozvedel, že streamovanie sa dá robiť oveľa efektívnejšie na základe p2p, len pre mňa to bude peer - server - peers, ale o tom inokedy, pretože to ešte nie je pripravené.

Aby sme mohli začať, potrebujeme skutočný server websockets. Urobil som ten najjednoduchší na základe balíčka melódie go:

Kód servera

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

Na strane klienta (strana streamovania) musíte najprv získať prístup ku kamere. Toto sa robí cez MediaStream API.

Získame prístup (povolenie) ku kamere/mikrofónu prostredníctvom Media Devices API. Toto API poskytuje metódu MediaDevices.getUserMedia(), ktorá zobrazuje vyskakovacie okno. okno so žiadosťou o povolenie prístupu ku kamere a/alebo mikrofónu. Chcel by som poznamenať, že som vykonal všetky experimenty v prehliadači Google Chrome, ale myslím si, že vo Firefoxe bude všetko fungovať približne rovnako.

Ďalej getUserMedia() vráti Promise, ktorému odovzdá objekt MediaStream – tok video-audio údajov. Tento objekt priradíme k vlastnosti src prvku video. kód:

Strana vysielania

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

Ak chcete vysielať video stream cez zásuvky, musíte ho niekde zakódovať, uložiť do vyrovnávacej pamäte a preniesť po častiach. Surový videostream nie je možné prenášať prostredníctvom webových zásuviek. Tu nám prichádza na pomoc MediaRecorder API. Toto API vám umožňuje kódovať a rozdeľovať stream na kúsky. Robím kódovanie na kompresiu video streamu, aby som cez sieť poslal menej bajtov. Po rozbití na kúsky môžete každý kus poslať do webovej zásuvky. kód:

Videostream zakódujeme, rozdelíme na kúsky

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

Teraz pridajme prenos cez websockety. Prekvapivo vám k tomu stačí len predmet WebSocket. Má iba dva spôsoby odoslania a zatvorenia. Názvy hovoria samé za seba. Pridaný kód:

Video stream prenášame na 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>

Vysielacia strana je pripravená! Teraz sa pokúsime prijať video stream a zobraziť ho na klientovi. Čo k tomu potrebujeme? Po prvé, samozrejme, pripojenie zásuvky. K objektu WebSocket pripojíme „poslucháča“ a prihlásime sa na odber udalosti „správa“. Po prijatí časti binárnych údajov ich náš server odošle predplatiteľom, teda klientom. V tomto prípade je na klientovi spustená funkcia spätného volania spojená s „poslucháčom“ udalosti „správa“; samotný objekt je odovzdaný do argumentu funkcie – časť video streamu kódovaného vp8.

Prijímame video stream

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

Dlho som sa snažil pochopiť, prečo nie je možné okamžite poslať prijaté kusy do video prvku na prehrávanie, ale ukázalo sa, že to nie je možné, samozrejme, musíte najprv vložiť kus do špeciálnej vyrovnávacej pamäte viazanej na prvok videa a až potom začne prehrávať video stream. Na to budete potrebovať MediaSource API и FileReader API.

MediaSource funguje ako akýsi prostredník medzi objektom prehrávania médií a zdrojom tohto mediálneho toku. Objekt MediaSource obsahuje pripojiteľnú vyrovnávaciu pamäť pre zdroj video/audio streamu. Jednou z funkcií je, že vyrovnávacia pamäť môže obsahovať iba údaje Uint8, takže na vytvorenie takejto vyrovnávacej pamäte budete potrebovať FileReader. Pozrite sa na kód a bude to jasnejšie:

Prehrávanie streamu videa

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

Prototyp streamovacej služby je pripravený. Hlavnou nevýhodou je oneskorenie prehrávania videa za vysielacou stranou o 100 ms, to si nastavujeme sami pri rozdeľovaní streamu videa pred jeho prenosom na server. Navyše, keď som skontroloval svoj notebook, oneskorenie medzi vysielacou a prijímacou stranou sa postupne nahromadilo, bolo to jasne viditeľné. Začal som hľadať spôsoby, ako prekonať túto nevýhodu a... narazil som RTCPeerConnection API, ktorý vám umožňuje prenášať video stream bez trikov, ako je rozdelenie streamu na kúsky. Akumulujúce sa oneskorenie je podľa mňa spôsobené tým, že prehliadač pred prenosom prekóduje každý kus do formátu webm. Nehrabal som ďalej, ale začal som študovať WebRTC. Myslím, že o výsledkoch môjho výskumu napíšem samostatný článok, ak to bude pre komunitu zaujímavé.

Zdroj: hab.com

Pridať komentár