(Téměř) zbytečné streamování z webové kamery z prohlížeče. Stream médií a webové zásuvky

V tomto článku se chci podělit o své pokusy o streamování videa přes websockets bez použití pluginů prohlížeče třetích stran, jako je Adobe Flash Player. Čtěte dále a zjistěte, co z toho vzešlo.

Adobe Flash, dříve Macromedia Flash, je platforma pro vytváření aplikací, které běží ve webovém prohlížeči. Před zavedením Media Stream API to byla prakticky jediná platforma pro streamování videa a hlasu z webové kamery a také pro vytváření různých druhů konferencí a chatů v prohlížeči. Protokol pro přenos mediálních informací RTMP (Real Time Messaging Protocol) byl ve skutečnosti na dlouhou dobu uzavřen, což znamenalo: pokud chcete posílit svou streamovací službu, buďte tak laskaví a použijte software od samotné společnosti Adobe - Adobe Media Server (AMS).

Po nějaké době v roce 2012 to Adobe „vzdalo a vyplivlo to“ veřejnosti. Specifikace RTMP protokol, který obsahoval chyby a byl v podstatě neúplný. V té době začali vývojáři vytvářet vlastní implementace tohoto protokolu a objevil se server Wowza. V roce 2011 společnost Adobe podala žalobu na Wowzu za nezákonné používání patentů souvisejících s RTMP; po 4 letech byl konflikt vyřešen smírnou cestou.

Platforma Adobe Flash je stará více než 20 let a během této doby bylo objeveno mnoho kritických zranitelností. slíbil skončit do roku 2020, takže pro streamovací službu zůstane jen několik alternativ.

Pro svůj projekt jsem se okamžitě rozhodl úplně opustit používání Flashe v prohlížeči. Hlavní důvod jsem uvedl výše; Flash také není vůbec podporován na mobilních platformách a opravdu jsem nechtěl nasadit Adobe Flash pro vývoj na Windows (emulátor vína). Pustil jsem se tedy do psaní klienta v JavaScriptu. Toto bude jen prototyp, protože později jsem se dozvěděl, že streamování lze dělat mnohem efektivněji na základě p2p, jen pro mě to bude peer - server - peers, ale o tom jindy, protože to ještě není připraveno.

Abychom mohli začít, potřebujeme skutečný server websockets. Udělal jsem ten nejjednodušší na základě balíčku melodie go:

Kód serveru

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 straně klienta (strana streamování) musíte nejprve získat přístup ke kameře. To se provádí skrz MediaStream API.

Získáme přístup (oprávnění) ke kameře/mikrofonu prostřednictvím Media Devices API. Toto API poskytuje metodu MediaDevices.getUserMedia(), která zobrazí vyskakovací okno. okno s žádostí o povolení přístupu ke kameře a/nebo mikrofonu. Rád bych poznamenal, že jsem všechny experimenty provedl v prohlížeči Google Chrome, ale myslím, že ve Firefoxu bude vše fungovat přibližně stejně.

Dále getUserMedia() vrátí Promise, kterému předá objekt MediaStream – tok video-audio dat. Tento objekt přiřadíme vlastnosti src prvku video. Kód:

Strana vysílání

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

Chcete-li vysílat video stream přes zásuvky, musíte jej někde zakódovat, uložit do vyrovnávací paměti a přenést po částech. Nezpracovaný video stream nelze přenášet prostřednictvím webových zásuvek. Tady nám přichází na pomoc MediaRecorder API. Toto API vám umožňuje kódovat a rozdělit stream na kousky. Provádím kódování pro kompresi video streamu, abych posílal méně bajtů přes síť. Po rozbití na kusy můžete každý kus poslat do webového soketu. Kód:

Video stream zakódujeme, rozdělíme na části

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

Nyní přidáme přenos přes webové zásuvky. Kupodivu vám k tomu stačí jen předmět WebSocket. Má pouze dvě metody odeslání a uzavření. Jména mluví sama za sebe. Přidán kód:

Přenášíme 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>

Vysílací strana je připravena! Nyní se pokusíme přijmout video stream a zobrazit jej na klientovi. Co k tomu potřebujeme? Za prvé, samozřejmě, připojení zásuvky. K objektu WebSocket připojíme „posluchač“ a přihlásíme se k události „zpráva“. Po přijetí části binárních dat je náš server vysílá předplatitelům, tedy klientům. V tomto případě je na klientovi spuštěna funkce zpětného volání spojená s „posluchačem“ události „zpráva“; samotný objekt je předán do argumentu funkce - část video streamu kódovaného vp8.

Přijímáme 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>

Dlouho jsem se snažil pochopit, proč není možné okamžitě odeslat přijaté kousky do video elementu k přehrání, ale ukázalo se, že to samozřejmě nejde, musíte skladbu nejprve vložit do speciální vyrovnávací paměti vázané na prvek videa a teprve poté začne přehrávat video stream. K tomu budete potřebovat MediaSource API и FileReader API.

MediaSource funguje jako jakýsi prostředník mezi objektem přehrávání médií a zdrojem tohoto mediálního toku. Objekt MediaSource obsahuje připojitelnou vyrovnávací paměť pro zdroj video/audio streamu. Jednou funkcí je, že vyrovnávací paměť může obsahovat pouze data Uint8, takže k vytvoření takové vyrovnávací paměti budete potřebovat FileReader. Podívejte se na kód a bude to jasnější:

Přehrávání video streamu

<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 streamovací služby je připraven. Hlavní nevýhodou je zpoždění přehrávání videa za vysílací stranou o 100 ms, to si nastavujeme sami při rozdělování video streamu před přenosem na server. Navíc, když jsem kontroloval svůj notebook, zpoždění mezi vysílací a přijímací stranou se postupně hromadilo, bylo to jasně vidět. Začal jsem hledat způsoby, jak tuto nevýhodu překonat, a... narazil jsem RTCPeerConnection API, který umožňuje přenášet video stream bez triků, jako je rozdělení streamu na kousky. Akumulující se prodleva je, myslím, způsobena tím, že prohlížeč před přenosem každý kus znovu zakóduje do formátu webm. Nehrabal jsem dál, ale začal jsem studovat WebRTC. Myslím, že o výsledcích svého výzkumu napíšu samostatný článek, pokud to bude pro komunitu zajímavé.

Zdroj: www.habr.com

Přidat komentář