(Gotovo) beskoristan streaming web kamere iz preglednika. Media Stream i Websockets

U ovom članku želim podijeliti svoje pokušaje strujanja videa putem web-utičnica bez korištenja dodataka za preglednike trećih strana kao što je Adobe Flash Player. Čitajte dalje kako biste saznali što je iz toga proizašlo.

Adobe Flash, bivši Macromedia Flash, platforma je za izradu aplikacija koje se izvode u web pregledniku. Prije uvođenja Media Stream API-ja, to je bila praktički jedina platforma za streaming videa i glasa s web kamere, kao i za kreiranje raznih vrsta konferencija i chatova u pregledniku. Protokol za prijenos medijskih informacija RTMP (Real Time Messaging Protocol) zapravo je dugo bio zatvoren, što je značilo: ako želite poboljšati svoju uslugu streaminga, budite ljubazni i koristite Adobeov softver - Adobe Media Server (AMS).

Nakon nekog vremena 2012. Adobe je "odustao i ispljunuo" u javnost. specifikacija RTMP protokol, koji je sadržavao pogreške i bio je u biti nepotpun. Do tada su programeri počeli izrađivati ​​vlastite implementacije ovog protokola i pojavio se Wowza poslužitelj. Godine 2011. Adobe je podnio tužbu protiv Wowze zbog nezakonitog korištenja patenata povezanih s RTMP-om; nakon 4 godine sukob je riješen sporazumno.

Adobe Flash platforma stara je više od 20 godina, a tijekom tog vremena otkrivene su mnoge kritične ranjivosti, podrška obećao završiti do 2020., ostavljajući nekoliko alternativa za uslugu strujanja.

Za svoj sam projekt odmah odlučio potpuno napustiti korištenje Flasha u pregledniku. Gore sam naveo glavni razlog; Flash također uopće nije podržan na mobilnim platformama i stvarno nisam želio implementirati Adobe Flash za razvoj na Windows (wine emulator). Stoga sam krenuo pisati klijenta u JavaScriptu. Ovo će biti samo prototip, budući da sam kasnije saznao da se streaming može raditi puno učinkovitije na temelju p2p-a, samo što će za mene biti peer - server - peers, ali o tome drugi put, jer još nije gotovo.

Za početak trebamo stvarni websockets poslužitelj. Napravio sam najjednostavniji na temelju paketa melody go:

Kod poslužitelja

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 strani klijenta (streaming), najprije morate pristupiti kameri. Ovo se radi putem MediaStream API.

Dobivamo pristup (dopuštenje) kameri/mikrofonu putem API za medijske uređaje. Ovaj API pruža metodu MediaDevices.getUserMedia(), koji prikazuje skočni prozor. prozor koji od korisnika traži dozvolu za pristup kameri i/ili mikrofonu. Želio bih napomenuti da sam sve eksperimente proveo u Google Chromeu, ali mislim da će sve raditi otprilike isto u Firefoxu.

Zatim, getUserMedia() vraća Promise, kojem prosljeđuje MediaStream objekt - tok video-audio podataka. Ovaj objekt dodjeljujemo svojstvu src video elementa. Kodirati:

Strana emitiranja

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

Za emitiranje video streama preko soketa, trebate ga negdje kodirati, spremiti u međuspremnik i prenijeti u dijelovima. Neobrađeni video stream ne može se prenositi putem websocketa. Ovdje nam dolazi u pomoć MediaRecorder API. Ovaj API omogućuje kodiranje i rastavljanje toka na dijelove. Radim kodiranje za komprimiranje video streama kako bih poslao manje bajtova preko mreže. Nakon što ga razbijete na dijelove, svaki dio možete poslati u websocket. Kodirati:

Kodiramo video stream, razbijamo ga na dijelove

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

Dodajmo sada prijenos putem websocketa. Začudo, sve što vam za ovo treba je predmet WebSocket. Ima samo dva načina slanja i zatvaranja. Imena govore sama za sebe. Dodani kod:

Video stream prenosimo na poslužitelj

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

Strana emitiranja je spremna! Pokušajmo sada primiti video stream i prikazati ga na klijentu. Što nam treba za ovo? Prvo, naravno, priključak utičnice. WebSocket objektu prilažemo "slušača" i pretplaćujemo se na događaj "message". Nakon što primi dio binarnog podatka, naš poslužitelj ga emitira pretplatnicima, odnosno klijentima. U ovom slučaju, funkcija povratnog poziva povezana sa "slušačem" događaja 'poruke' pokreće se na klijentu; sam objekt se prosljeđuje u argument funkcije - dio video streama kodiran vp8.

Prihvaćamo 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>

Dugo sam pokušavao shvatiti zašto je nemoguće odmah poslati primljene dijelove u video element za reprodukciju, ali pokazalo se da to nije moguće učiniti, naravno, prvo morate staviti komad u poseban međuspremnik vezan za video element, i tek tada će započeti reprodukciju video streama. Za ovo će vam trebati MediaSource API и FileReader API.

MediaSource djeluje kao neka vrsta posrednika između objekta reprodukcije medija i izvora ovog medija. Objekt MediaSource sadrži međuspremnik koji se može priključiti za izvor video/audio streama. Jedna značajka je da međuspremnik može držati samo Uint8 podatke, pa će vam trebati FileReader za stvaranje takvog međuspremnika. Pogledajte kod i bit će vam jasnije:

Reprodukcija video streama

<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 streaming usluge je spreman. Glavni nedostatak je što će video reprodukcija kasniti za odašiljačkom stranom za 100 ms; to sami postavljamo prilikom dijeljenja video streama prije prijenosa na poslužitelj. Štoviše, kad sam provjerio na svom prijenosnom računalu, zaostatak između odašiljačke i prijemne strane postupno se nakupljao, to je bilo jasno vidljivo. Počeo sam tražiti načine da prevladam ovaj nedostatak i... naišao sam RTCPeerConnection API, koji vam omogućuje prijenos video streama bez trikova kao što je dijeljenje streama na dijelove. Mislim da je gomilanje kašnjenja posljedica činjenice da preglednik ponovno kodira svaki dio u webm format prije prijenosa. Nisam dalje kopao, već sam počeo proučavati WebRTC. Mislim da ću napisati poseban članak o rezultatima svog istraživanja ako budem bio zanimljiv zajednici.

Izvor: www.habr.com

Dodajte komentar