(Gotovo) beskorisno strujanje web kamere iz pretraživača. Media Stream i Websockets

U ovom članku želim podijeliti svoje pokušaje streamanja videa putem websocketa bez korištenja dodataka za pretraživače treće strane kao što je Adobe Flash Player. Čitajte dalje da saznate šta je iz toga proizašlo.

Adobe Flash, ranije Macromedia Flash, je platforma za kreiranje aplikacija koje se pokreću u web pretraživaču. Prije uvođenja Media Stream API-ja, to je bila praktički jedina platforma za streaming videa i glasa sa web kamere, kao i za kreiranje raznih vrsta konferencija i ćaskanja u pretraživaču. Protokol za prijenos medijskih informacija RTMP (Real Time Messaging Protocol) je zapravo dugo bio zatvoren, što je značilo: ako želite da poboljšate svoj streaming servis, budite ljubazni da koristite softver od samog Adobe-a - Adobe Media Server (AMS).

Nakon nekog vremena 2012. godine, Adobe je “odustao i ispljunuo ga” javnosti. specifikacija RTMP protokol, koji je sadržavao greške i bio je u suštini nepotpun. Do tada su programeri počeli da prave sopstvene implementacije ovog protokola, a pojavio se i Wowza server. Adobe je 2011. godine podnio tužbu protiv Wowze zbog nezakonitog korištenja RTMP patenata; nakon 4 godine, sukob je riješen sporazumno.

Adobe Flash platforma je stara više od 20 godina, tokom kojih su otkrivene mnoge kritične ranjivosti, podrška obećao završiti do 2020. godine, ostavljajući nekoliko alternativa za streaming servis.

Za svoj projekat, odmah sam odlučio da potpuno odustanem od upotrebe Flash-a u pretraživaču. Naveo sam glavni razlog gore; Flash također uopće nije podržan na mobilnim platformama i zaista nisam želio implementirati Adobe Flash za razvoj na Windows (wine emulator). Tako sam krenuo da napišem klijenta u JavaScript-u. Ovo će biti samo prototip, pošto sam kasnije saznao da se streaming može mnogo efikasnije raditi na bazi p2p-a, samo za mene će to biti peer - server - peers, ali o tome drugi put, jer još nije spreman.

Za početak, potreban nam je pravi websockets server. Napravio sam najjednostavniji na osnovu melody go paketa:

Serverski kod

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 klijentu (strani strimovanja), prvo morate pristupiti kameri. Ovo se radi kroz MediaStream API.

Dobijamo pristup (dozvolu) kameri/mikrofonu putem API medijskih uređaja. 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. Napominjem da sam sve eksperimente izvršio u Google Chrome-u, ali mislim da će sve raditi otprilike isto u Firefoxu.

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

Strana emitovanja

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

Da biste emitovali video stream preko utičnica, morate ga negdje kodirati, baferovati i prenijeti u dijelovima. Sirovi video stream se ne može prenijeti putem websocketa. Tu nam dolazi u pomoć MediaRecorder API. Ovaj API vam omogućava da kodirate i razbijete stream na komade. Radim kodiranje da komprimujem video stream kako bih poslao manje bajtova preko mreže. Nakon što ga razbijete na komade, svaki komad možete poslati u websocket. kod:

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>

Sada dodajmo prijenos preko websocketa. Iznenađujuće, sve što vam treba za ovo je objekat WebSockets. Ima samo dva načina slanja i zatvaranja. Imena govore sama za sebe. Dodan kod:

Prenosimo video stream 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>

Strana za emitovanje je spremna! Pokušajmo sada primiti video stream i prikazati ga na klijentu. Šta nam treba za ovo? Prvo, naravno, priključak utičnice. Prilažemo “slušatelja” na WebSocket objekt i pretplaćujemo se na događaj 'poruka'. Nakon što primi dio binarnog podatka, naš server ga emituje pretplatnicima, odnosno klijentima. U ovom slučaju, funkcija povratnog poziva povezana sa "slušateljem" događaja "poruka" se pokreće na klijentu; sam objekt se prosljeđuje u argument funkcije - dio video toka koji je kodiran pomoću vp8.

Prihvatamo 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 da shvatim zašto je nemoguće odmah poslati primljene komade u video element na reprodukciju, ali se pokazalo da se to ne može učiniti, naravno, prvo morate staviti komad u poseban međuspremnik vezan za video element, i tek tada će početi reproducirati video stream. Za ovo će vam trebati MediaSource API и FileReader API.

MediaSource djeluje kao neka vrsta posrednika između objekta za reprodukciju medija i izvora ovog medijskog toka. Objekt MediaSource sadrži bafer koji se može priključiti za izvor video/audio toka. Jedna od karakteristika je da bafer može zadržati samo podatke Uint8, tako da će vam trebati FileReader da kreirate takav bafer. Pogledajte kod i bit će vam jasnije:

Reprodukcija 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 streaming servisa je spreman. Glavni nedostatak je što će reprodukcija videa zaostajati za stranom odašiljanja za 100 ms; to sami postavljamo kada dijelimo video stream prije nego što ga prenesemo na server. Štaviše, kada sam provjerio na svom laptopu, zaostajanje između odašiljačke i prijemne strane se postepeno akumuliralo, to je bilo jasno vidljivo. Počeo sam da tražim načine da prevaziđem ovaj nedostatak i... naišao sam RTCPeerConnection API, koji vam omogućava da prenosite video stream bez trikova kao što je dijeljenje toka na dijelove. Mislim da je zaostajanje nagomilano zbog činjenice da pretraživač ponovo 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 mog istraživanja ako budem bio zanimljiv zajednici.

izvor: www.habr.com

Dodajte komentar