(Peaaegu) kasutu veebikaamera voogesitus brauserist. Media Stream ja Websockets

Selles artiklis tahan jagada oma katseid voogesitada videot veebipistikupesade kaudu ilma kolmanda osapoole brauseri pistikprogramme, nagu Adobe Flash Player, kasutamata. Loe edasi, et teada saada, mis sellest välja tuli.

Adobe Flash, varem Macromedia Flash, on platvorm veebibrauseris töötavate rakenduste loomiseks. Enne Media Stream API kasutuselevõttu oli see praktiliselt ainus platvorm video ja hääle voogesitamiseks veebikaamerast, samuti mitmesuguste konverentside ja vestluste loomiseks brauseris. Meediainfo edastamise protokoll RTMP (Real Time Messaging Protocol) oli tegelikult pikka aega suletud, mis tähendas: kui soovite oma voogedastusteenust turgutada, olge lahked ja kasutage Adobe enda tarkvara - Adobe Media Server (AMS).

Mõne aja pärast 2012. aastal andis Adobe alla ja sülitas selle avalikkuse ette. spetsifikatsioon RTMP protokoll, mis sisaldas vigu ja oli sisuliselt puudulik. Selleks ajaks hakkasid arendajad seda protokolli ise juurutama ja ilmus Wowza server. 2011. aastal esitas Adobe Wowza vastu hagi RTMP-ga seotud patentide ebaseadusliku kasutamise pärast, 4 aasta pärast lahendati konflikt sõbralikult.

Adobe Flashi platvorm on rohkem kui 20 aastat vana, selle aja jooksul on avastatud palju kriitilisi turvaauke, tugi lubas lõpetada 2020. aastaks, jättes voogedastusteenusele vähe alternatiive.

Oma projekti jaoks otsustasin kohe brauseris Flashi kasutamisest täielikult loobuda. Mainisin ülaltoodud peamise põhjuse; Flashi ei toetata ka mobiilsetel platvormidel ja ma ei tahtnud Adobe Flashi Windowsi (veini emulaator) arendamiseks juurutada. Nii otsustasin kirjutada JavaScriptis kliendi. Sellest saab alles prototüüp, sest hiljem sain teada, et voogesitust saab p2p baasil teha palju tõhusamalt, ainult minu jaoks on see peer - server - peer, aga sellest lähemalt mõni teine ​​kord, sest see pole veel valmis.

Alustamiseks vajame tegelikku Websocketsi serverit. Kõige lihtsama tegin melody go paketi põhjal:

Serveri kood

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

Kliendil (voogesituse poolel) peate esmalt pääsema juurde kaamerale. Seda tehakse läbi MediaStream API.

Saame ligipääsu (loa) kaamerale/mikrofonile läbi Media Devices API. See API pakub meetodit MediaDevices.getUserMedia(), mis näitab hüpikakent. aken, mis küsib kasutajalt luba kaamerale ja/või mikrofonile juurdepääsuks. Tahaksin märkida, et tegin kõik katsed Google Chrome'is, kuid arvan, et Firefoxis töötab kõik samamoodi.

Järgmisena tagastab getUserMedia() lubaduse, millele edastab MediaStreami objekti – video-heliandmete voo. Määrame selle objekti videoelemendi atribuudile src. Kood:

Ringhäälingu pool

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

Videovoo edastamiseks pistikupesade kaudu peate selle kuhugi kodeerima, puhverdama ja osade kaupa edastama. Toores videovoogu ei saa veebipistikupesade kaudu edastada. Siin tuleb see meile appi MediaRecorder API. See API võimaldab teil voogu kodeerida ja tükkideks jagada. Kodeerin videovoo tihendamiseks, et saata võrgu kaudu vähem baite. Kui olete selle tükkideks purustanud, saate iga tüki võrgupistikusse saata. Kood:

Kodeerime videovoo, jagame selle osadeks

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

Lisame nüüd edastuse veebipistikupesade kaudu. Üllataval kombel on selleks vaja ainult objekti WebSocket. Sellel on ainult kaks saatmis- ja sulgemisviisi. Nimed räägivad enda eest. Lisatud kood:

Edastame videovoo serverisse

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

Saate pool on valmis! Proovime nüüd videovoogu vastu võtta ja kliendil kuvada. Mida me selleks vajame? Esiteks muidugi pistikupesa ühendus. Lisame WebSocketi objektile "kuulaja" ja tellime sündmuse "sõnum". Pärast binaarandmete osa vastuvõtmist edastab meie server selle abonentidele, see tähendab klientidele. Sel juhul käivitatakse kliendil tagasihelistamise funktsioon, mis on seotud sõnumi sündmuse "kuulajaga"; objekt ise edastatakse funktsiooni argumendisse - vp8 poolt kodeeritud videovoo osa.

Aktsepteerime videovoogu

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

Pikka aega püüdsin aru saada, miks ei ole võimalik saabunud tükke kohe videoelemendile taasesitamiseks saata, kuid selgus, et seda ei saa teha, muidugi tuleb esmalt tükk spetsiaalsesse puhvrisse, mis on seotud videoelementi ja alles siis hakkab see videovoogu esitama. Selleks vajate MediaSource API и FileReader API.

MediaSource toimib omamoodi vahendajana meediumi taasesitusobjekti ja selle meediumivoo allika vahel. MediaSource objekt sisaldab ühendatavat puhvrit video/helivoo allika jaoks. Üks omadus on see, et puhver mahutab ainult Uint8 andmeid, seega vajate sellise puhvri loomiseks FileReaderit. Vaadake koodi ja see muutub selgemaks:

Videovoo esitamine

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

Voogedastusteenuse prototüüp on valmis. Peamine puudus on see, et video taasesitus jääb edastavast poolest 100 ms võrra maha, selle määrame ise videovoo jagamisel enne serverisse edastamist. Veelgi enam, kui ma oma sülearvutit kontrollisin, kogunes järk-järgult vahe saatva ja vastuvõtva poole vahel, see oli selgelt näha. Hakkasin otsima viise, kuidas sellest puudusest üle saada ja... jõudsin kohale RTCPeerConnection API, mis võimaldab edastada videovoogu ilma nippideta, näiteks voogu tükkideks jagamiseta. Kogunev viivitus on minu arvates tingitud sellest, et brauser kodeerib enne edastamist iga tüki uuesti webm-vormingusse. Ma ei süvenenud edasi, vaid hakkasin WebRTC-d õppima. Arvan, et kirjutan oma uurimistöö tulemustest eraldi artikli, kui see on kogukonnale huvitav.

Allikas: www.habr.com

Lisa kommentaar