(Majdnem) haszontalan webkamerás streamelés böngészőből. Media Stream és Websockets

Ebben a cikkben szeretném megosztani azokat a próbálkozásaimat, amelyekkel a websocketeken keresztül próbáltam streamelni videókat harmadik féltől származó böngészőbővítmények, például az Adobe Flash Player használata nélkül. Olvasson tovább, hogy megtudja, mi lett belőle.

Az Adobe Flash, korábbi nevén Macromedia Flash egy webböngészőben futó alkalmazások létrehozására szolgáló platform. A Media Stream API bevezetése előtt gyakorlatilag ez volt az egyetlen platform a webkamerás videó és hang streamelésére, valamint különféle konferenciák és chatek böngészőben történő létrehozására. A médiainformációk továbbítására szolgáló RTMP (Real Time Messaging Protocol) protokoll hosszú időre le volt zárva, ami azt jelentette: ha szeretné fellendíteni a streaming szolgáltatását, legyen szíves magától az Adobe szoftverétől - Adobe Media Server (AMS) - használni.

2012-ben egy idő után az Adobe „feladta és kiköpte” a nyilvánosság elé. leírás RTMP protokoll, amely hibákat tartalmazott, és lényegében hiányos volt. Addigra a fejlesztők elkezdték saját maguk implementálni ezt a protokollt, és megjelent a Wowza szerver. 2011-ben az Adobe pert indított Wowza ellen RTMP-vel kapcsolatos szabadalmak illegális felhasználása miatt, 4 év után a konfliktus békés úton megoldódott.

Az Adobe Flash platform több mint 20 éves, ez idő alatt számos kritikus biztonsági rést fedeztek fel, támogatás igért 2020-ig véget ér, így kevés alternatíva marad a streaming szolgáltatás számára.

A projektemnél azonnal úgy döntöttem, hogy teljesen elhagyom a Flash használatát a böngészőben. A fő okot fentebb jeleztem; a Flash szintén egyáltalán nem támogatott mobil platformokon, és nagyon nem akartam telepíteni az Adobe Flash-t Windows-on (wine emulator) való fejlesztéshez. Ezért elhatároztam, hogy írok egy klienst JavaScriptben. Ez csak egy prototípus lesz, mert később megtudtam, hogy a streamelést sokkal hatékonyabban lehet csinálni p2p alapján, csak nekem peer - server - peer lesz, de erről majd máskor, mert még nincs kész.

A kezdéshez szükségünk van a tényleges websockets szerverre. A melody go csomag alapján elkészítettem a legegyszerűbbet:

Szerver kódja

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

Az ügyfélen (streaming oldalon) először el kell érnie a kamerát. Ez keresztül történik MediaStream API.

Ezen keresztül kapunk hozzáférést (engedélyt) a kamerához/mikrofonhoz Media Devices API. Ez az API egy módszert biztosít MediaDevices.getUserMedia(), amely előugró ablakot jelenít meg. egy ablak, amely engedélyt kér a felhasználótól a kamera és/vagy mikrofon eléréséhez. Szeretném megjegyezni, hogy az összes kísérletet a Google Chrome-ban végeztem, de úgy gondolom, hogy minden nagyjából ugyanúgy fog működni a Firefoxban.

Ezután a getUserMedia() Promise-t ad vissza, amelynek átad egy MediaStream objektumot – egy video-audio adatfolyamot. Ezt az objektumot hozzárendeljük a videoelem src tulajdonságához. Kód:

Műsorszórási oldal

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

Ahhoz, hogy videofolyamot socketeken keresztül sugározzon, valahol kódolnia kell, pufferelnie kell, és részenként továbbítania kell. A nyers videofolyam nem továbbítható websocket-en keresztül. Itt jön a segítségünkre MediaRecorder API. Ez az API lehetővé teszi az adatfolyam kódolását és darabokra bontását. Kódolást végzek a videofolyam tömörítésére, hogy kevesebb bájtot küldjek a hálózaton. Miután darabokra bontotta, mindegyik darabot egy webaljzatba küldheti. Kód:

A videofolyamot kódoljuk, részekre bontjuk

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

Most adjuk hozzá a websocketeken keresztüli átvitelt. Meglepő módon ehhez csak egy tárgy kell WebSocket. Csak két küldési és bezárási módja van. A nevek magukért beszélnek. Hozzáadott kód:

A video streamet továbbítjuk a szervernek

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

Elkészült a közvetítési oldal! Most próbáljunk meg egy videofolyamot fogadni és megjeleníteni az ügyfélen. Mi kell ehhez? Először is természetesen az aljzatcsatlakozás. A WebSocket objektumhoz egy „hallgatót” csatolunk, és feliratkozunk az „üzenet” eseményre. Miután megkapta a bináris adatot, szerverünk továbbítja az előfizetőknek, azaz az ügyfeleknek. Ebben az esetben az „üzenet” esemény „hallgatójához” társított visszahívási funkció aktiválódik a kliensen, maga az objektum pedig átkerül a függvény argumentumába – a vp8 által kódolt videofolyam egy darabjába.

Videó streamet elfogadunk

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

Sokáig próbáltam megérteni, hogy miért nem lehet a beérkezett darabokat azonnal visszaküldeni a videóelemre, de kiderült, hogy ezt nem lehet megtenni, természetesen először egy speciális pufferbe kell helyezni a darabot. a videó elemet, és csak ezután kezdi el lejátszani a videofolyamot. Ehhez szüksége lesz MediaSource API и FileReader API.

A MediaSource egyfajta közvetítőként működik a médialejátszó objektum és a médiafolyam forrása között. A MediaSource objektum egy csatlakoztatható puffert tartalmaz a videó/audio adatfolyam forrásához. Az egyik jellemző, hogy a puffer csak Uint8 adatokat tud tárolni, így egy ilyen puffer létrehozásához FileReaderre lesz szüksége. Nézd meg a kódot, és világosabb lesz:

A videofolyam lejátszása

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

Elkészült a streaming szolgáltatás prototípusa. Legfőbb hátránya, hogy a videó lejátszás 100 ms-al elmarad az átviteli oldaltól, ezt mi magunk állítjuk be a videofolyam felosztásánál, mielőtt továbbítanánk a szerverre. Sőt, amikor megnéztem a laptopomon, fokozatosan felgyülemlett a késés az adó és a vevő oldal között, ez jól látható volt. Elkezdtem keresni a módokat, hogy leküzdjem ezt a hátrányt, és... rájöttem RTCPeerConnection API, amely lehetővé teszi a videofolyam továbbítását olyan trükkök nélkül, mint a folyam darabokra osztása. A felgyülemlett lemaradás szerintem annak tudható be, hogy a böngésző minden darabot átkódol a webm formátumba átvitel előtt. Nem ástam tovább, hanem elkezdtem tanulni a WebRTC-t, azt hiszem, írok egy külön cikket a kutatásom eredményeiről, ha érdekesnek találom a közösséget.

Forrás: will.com

Hozzászólás