(Melkein) turha verkkokameran suoratoisto selaimesta. Media Stream ja Websockets

Tässä artikkelissa haluan jakaa yritykseni suoratoistaa videota verkkoliitäntöjen kautta ilman kolmannen osapuolen selainlaajennuksia, kuten Adobe Flash Playeria. Lue eteenpäin saadaksesi selville, mitä siitä tuli.

Adobe Flash, entinen Macromedia Flash, on alusta verkkoselaimessa toimivien sovellusten luomiseen. Ennen Media Stream API:n käyttöönottoa se oli käytännössä ainoa alusta videon ja äänen suoratoistoon web-kamerasta sekä erilaisten konferenssien ja chattien luomiseen selaimessa. Mediatiedon välitysprotokolla RTMP (Real Time Messaging Protocol) oli itse asiassa suljettuna pitkään, mikä tarkoitti: jos haluat tehostaa suoratoistopalveluasi, ole ystävällinen ja käytä Adoben itsensä ohjelmistoja - Adobe Media Server (AMS).

Jonkin ajan kuluttua vuonna 2012 Adobe "luopui ja sylki sen" yleisölle. erittely RTMP-protokolla, joka sisälsi virheitä ja oli olennaisesti epätäydellinen. Siihen mennessä kehittäjät alkoivat tehdä omia toteutuksiaan tästä protokollasta, ja Wowza-palvelin ilmestyi. Vuonna 2011 Adobe nosti kanteen Wowzaa vastaan ​​RTMP:hen liittyvien patenttien laittomasta käytöstä; 4 vuoden kuluttua konflikti ratkaistiin sovinnollisesti.

Adobe Flash -alusta on yli 20 vuotta vanha, jonka aikana on löydetty monia kriittisiä haavoittuvuuksia, tuki luvattu päättyy vuoteen 2020 mennessä, jolloin suoratoistopalvelulle jää vain vähän vaihtoehtoja.

Projektissani päätin välittömästi luopua Flashin käytöstä selaimessa. Ilmoitin yllä pääsyyn; Flashia ei myöskään tueta ollenkaan mobiilialustoilla, enkä todellakaan halunnut ottaa Adobe Flashia käyttöön Windows-kehitykseen (viini-emulaattori). Joten päätin kirjoittaa asiakkaan JavaScriptillä. Tämä tulee olemaan vain prototyyppi, koska myöhemmin opin, että suoratoisto voidaan tehdä paljon tehokkaammin p2p:n pohjalta, vain minulle se tulee olemaan peer - server - peers, mutta siitä lisää toisella kerralla, koska se ei ole vielä valmis.

Aloittaaksemme tarvitsemme todellisen websockets-palvelimen. Tein yksinkertaisimman melody go -paketin perusteella:

Palvelimen koodi

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

Asiakkaalla (suoratoistopuolella) sinun on ensin käytettävä kameraa. Tämä tehdään läpi MediaStream API.

Saamme pääsyn (luvan) kameraan/mikrofoniin kautta Media Devices API. Tämä API tarjoaa menetelmän MediaDevices.getUserMedia(), joka näyttää ponnahdusikkunan. ikkuna, jossa pyydetään käyttäjältä lupaa käyttää kameraa ja/tai mikrofonia. Haluaisin huomauttaa, että tein kaikki kokeet Google Chromessa, mutta uskon, että kaikki toimii suunnilleen samalla tavalla Firefoxissa.

Seuraavaksi getUserMedia() palauttaa Promisen, jolle se välittää MediaStream-objektin - video-audiodatavirran. Määritämme tämän objektin videoelementin src-ominaisuuteen. Koodi:

Lähetyspuoli

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

Jos haluat lähettää videovirran liittimien kautta, sinun on koodattava se jonnekin, puskuroitava ja lähetettävä osissa. Raakavideovirtaa ei voi siirtää verkkoliitäntöjen kautta. Tässä se tulee avuksemme MediaRecorder API. Tämän API:n avulla voit koodata ja jakaa virran osiin. Käytän koodausta videovirran pakkaamiseksi, jotta voin lähettää vähemmän tavuja verkon yli. Kun olet rikkonut sen paloiksi, voit lähettää jokaisen kappaleen verkkopistorasiaan. Koodi:

Koodaamme videovirran, jaamme sen paloiksi

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

Lisätään nyt lähetys websockettien kautta. Yllättäen tähän tarvitset vain esineen WebSocket. Sillä on vain kaksi tapaa lähettää ja sulkea. Nimet puhuvat puolestaan. Lisätty koodi:

Välitämme videovirran palvelimelle

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

Lähetyspuoli on valmis! Yritetään nyt vastaanottaa videovirta ja näyttää se asiakkaalla. Mitä me tarvitsemme tähän? Ensinnäkin tietysti pistorasialiitäntä. Liitämme WebSocket-objektiin "kuuntelijan" ja tilaamme "viesti"-tapahtuman. Vastaanotettuaan osan binaaridataa palvelimemme lähettää sen tilaajille eli asiakkaille. Tässä tapauksessa 'viesti'-tapahtuman "kuuntelijaan" liittyvä takaisinsoittotoiminto laukeaa asiakkaassa; itse objekti välitetään funktion argumenttiin - osaan vp8:n koodaamaa videovirtaa.

Hyväksymme videostreamin

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

Yritin pitkään ymmärtää, miksi vastaanotettuja kappaleita on mahdotonta lähettää heti videoelementtiin toistoa varten, mutta kävi ilmi, että näin ei voi tehdä, tietysti sinun on ensin asetettava kappale erityiseen puskuriin, joka on sidottu videoelementin, ja vasta sitten se alkaa toistaa videovirtaa. Tätä varten tarvitset MediaSource API и FileReader API.

MediaSource toimii eräänlaisena välittäjänä mediatoistoobjektin ja tämän mediavirran lähteen välillä. MediaSource-objekti sisältää kytkettävän puskurin video-/äänivirran lähteelle. Yksi ominaisuus on, että puskuri voi sisältää vain Uint8-tietoja, joten tarvitset FileReaderin sellaisen puskurin luomiseen. Katso koodia ja se tulee selkeämmäksi:

Toistaa videovirtaa

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

Suoratoistopalvelun prototyyppi on valmis. Suurin haittapuoli on, että videon toisto jää 100 ms jälkeen lähettävästä puolelta; asetamme tämän itse jakaessamme videovirran ennen sen lähettämistä palvelimelle. Lisäksi kun tarkistin kannettavaa tietokonettani, lähetys- ja vastaanottopuolen välinen viive kertyi vähitellen, tämä oli selvästi nähtävissä. Aloin etsiä tapoja voittaa tämä epäkohta, ja... törmäsin RTCPeerConnection API, jonka avulla voit lähettää videovirran ilman temppuja, kuten virran jakamista osiin. Kertyvä viive johtuu mielestäni siitä, että selain koodaa jokaisen kappaleen uudelleen webm-muotoon ennen lähetystä. En kaivannut enempää, vaan aloin opiskelemaan WebRTC:tä. Taidan kirjoittaa erillisen artikkelin tutkimukseni tuloksista, jos se kiinnostaa yhteisöä.

Lähde: will.com

Lisää kommentti