(Skoraj) neuporabno pretakanje spletne kamere iz brskalnika. Medijski tok in spletne vtičnice

V tem članku želim deliti svoje poskuse pretakanja videa prek spletnih vtičnic brez uporabe vtičnikov brskalnika tretjih oseb, kot je Adobe Flash Player. Preberite, če želite izvedeti, kaj je nastalo iz tega.

Adobe Flash, prej Macromedia Flash, je platforma za ustvarjanje aplikacij, ki se izvajajo v spletnem brskalniku. Pred uvedbo API-ja Media Stream je bil praktično edina platforma za pretakanje videa in glasu iz spletne kamere ter za ustvarjanje različnih vrst konferenc in klepetov v brskalniku. Protokol za prenos medijskih informacij RTMP (Real Time Messaging Protocol) je bil dolgo časa dejansko zaprt, kar je pomenilo: če želite izboljšati svojo storitev pretakanja, bodite dovolj prijazni in uporabite programsko opremo podjetja Adobe - Adobe Media Server (AMS).

Čez nekaj časa leta 2012 je Adobe "obupal in to izpljunil" javnosti. specifikacija Protokol RTMP, ki je vseboval napake in je bil v bistvu nepopoln. Do takrat so razvijalci začeli izdelovati lastne implementacije tega protokola in pojavil se je strežnik Wowza. Leta 2011 je Adobe vložil tožbo proti Wowzi zaradi nezakonite uporabe patentov, povezanih z RTMP; po 4 letih je bil spor rešen sporazumno.

Platforma Adobe Flash je stara več kot 20 let in v tem času je bilo odkritih veliko kritičnih ranljivosti, podpora obljubil končati do leta 2020, pri čemer bo ostalo le nekaj alternativ za storitev pretakanja.

Za svoj projekt sem se takoj odločil, da popolnoma opustim uporabo Flasha v brskalniku. Glavni razlog sem navedel zgoraj; Flash prav tako sploh ni podprt na mobilnih platformah in res nisem želel namestiti Adobe Flash za razvoj v sistemu Windows (wine emulator). Zato sem se odločil napisati stranko v JavaScriptu. To bo samo prototip, saj sem kasneje izvedel, da se lahko na podlagi p2p izvaja pretakanje veliko bolj učinkovito, samo zame bo peer - server - peers, ampak o tem drugič, ker še ni pripravljeno.

Za začetek potrebujemo dejanski strežnik websockets. Najenostavnejšega sem naredil na osnovi paketa melody go:

Koda strežnika

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 odjemalcu (na strani pretakanja) morate najprej dostopati do kamere. To se naredi prek MediaStream API.

Dostop (dovoljenje) do kamere/mikrofona pridobimo prek API za medijske naprave. Ta API ponuja metodo MediaDevices.getUserMedia(), ki prikaže pojavno okno. okno, ki uporabnika prosi za dovoljenje za dostop do kamere in/ali mikrofona. Rad bi omenil, da sem vse poskuse izvedel v Google Chromu, vendar mislim, da bo vse delovalo približno enako v Firefoxu.

Nato getUserMedia() vrne Promise, ki mu posreduje objekt MediaStream – tok video-zvočnih podatkov. Ta objekt dodelimo lastnosti src video elementa. Koda:

Stran oddajanja

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

Če želite oddajati video tok prek vtičnic, ga morate nekje kodirati, medpomniti in prenesti po delih. Neobdelanega video toka ni mogoče prenašati prek spletnih vtičnic. Tukaj nam priskoči na pomoč API MediaRecorder. Ta API vam omogoča kodiranje in razdelitev toka na dele. Kodiram za stiskanje video toka, da pošljem manj bajtov po omrežju. Ko ga razdelite na koščke, lahko vsak kos pošljete v spletno vtičnico. Koda:

Kodiramo video tok, ga razdelimo na koščke

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

Zdaj pa dodamo prenos prek spletnih vtičnic. Presenetljivo je, da za to potrebujete le predmet WebSocket. Ima samo dva načina pošiljanja in zapiranja. Imena govorijo zase. Dodana koda:

Video tok prenašamo na strežnik

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

Oddajna stran je pripravljena! Zdaj pa poskusimo prejeti video tok in ga prikazati na odjemalcu. Kaj potrebujemo za to? Najprej seveda priključek vtičnice. Objektu WebSocket priložimo »poslušalnik« in se naročimo na dogodek »sporočilo«. Ko prejme del binarnih podatkov, jih naš strežnik oddaja naročnikom, torej odjemalcem. V tem primeru se funkcija povratnega klica, povezana s »poslušalcem« dogodka 'sporočilo', sproži na odjemalcu; sam objekt se posreduje v argument funkcije - del video toka, ki ga kodira vp8.

Sprejemamo video tok

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

Dolgo časa sem poskušal razumeti, zakaj prejetih kosov ni mogoče takoj poslati v video element za predvajanje, vendar se je izkazalo, da tega ni mogoče storiti, seveda morate najprej dati del v poseben medpomnilnik, vezan na video element in šele nato bo začel predvajati video tok. Za to boste potrebovali MediaSource API и FileReader API.

MediaSource deluje kot nekakšen posrednik med predmetom predvajanja predstavnosti in virom tega medijskega toka. Objekt MediaSource vsebuje vtični medpomnilnik za vir video/zvočnega toka. Ena značilnost je, da lahko medpomnilnik vsebuje samo podatke Uint8, zato boste za ustvarjanje takega medpomnilnika potrebovali FileReader. Poglej kodo in postalo bo bolj jasno:

Predvajanje video toka

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

Prototip storitve pretakanja je pripravljen. Glavna pomanjkljivost je, da bo predvajanje videa zaostajalo za oddajno stranjo za 100 ms, kar sami nastavimo pri razdelitvi video toka pred prenosom na strežnik. Poleg tega, ko sem preveril na svojem prenosniku, se je zamik med oddajno in sprejemno stranjo postopoma kopičil, to je bilo jasno vidno. Začel sem iskati načine, kako premagati to pomanjkljivost, in ... naletel API RTCPeerConnection, ki vam omogoča prenos video toka brez trikov, kot je razdelitev toka na dele. Mislim, da je kopičenje zaostanka posledica dejstva, da brskalnik pred prenosom vsak kos znova kodira v format webm. Nisem več kopal, ampak sem začel preučevati WebRTC. Mislim, da bom o rezultatih svoje raziskave napisal ločen članek, če se mi bo zdel zanimiv za skupnost.

Vir: www.habr.com

Dodaj komentar