(Nesten) ubrukelig webkamerastrømming fra en nettleser. Media Stream og Websockets

I denne artikkelen vil jeg dele mine forsøk på å streame video via websockets uten å bruke tredjeparts nettleserplugins som Adobe Flash Player. Les videre for å finne ut hva som kom ut av det.

Adobe Flash, tidligere Macromedia Flash, er en plattform for å lage programmer som kjører i en nettleser. Før introduksjonen av Media Stream API var det praktisk talt den eneste plattformen for streaming av video og stemme fra et webkamera, samt for å lage ulike typer konferanser og chatter i nettleseren. Protokollen for overføring av medieinformasjon RTMP (Real Time Messaging Protocol) var faktisk stengt i lang tid, noe som betydde: hvis du ønsker å øke strømmetjenesten din, vær så snill å bruke programvare fra Adobe selv – Adobe Media Server (AMS).

Etter en tid i 2012 "ga Adobe opp og spyttet det ut" til publikum. spesifikasjon RTMP-protokollen, som inneholdt feil og var i hovedsak ufullstendig. På den tiden begynte utviklere å lage sine egne implementeringer av denne protokollen, og Wowza-serveren dukket opp. I 2011 anla Adobe et søksmål mot Wowza for ulovlig bruk av RTMP-relaterte patenter; etter 4 år ble konflikten løst i minnelighet.

Adobe Flash-plattformen er mer enn 20 år gammel, i løpet av denne tiden har mange kritiske sårbarheter blitt oppdaget, støtte lovet å avslutte innen 2020, og etterlate få alternativer for strømmetjenesten.

For prosjektet mitt bestemte jeg meg umiddelbart for å helt forlate bruken av Flash i nettleseren. Jeg indikerte hovedårsaken ovenfor; Flash støttes heller ikke i det hele tatt på mobile plattformer, og jeg ønsket virkelig ikke å distribuere Adobe Flash for utvikling på Windows (vinemulator). Så jeg satte meg for å skrive en klient i JavaScript. Dette vil bare være en prototype, siden jeg senere lærte at streaming kan gjøres mye mer effektivt basert på p2p, bare for meg vil det være peer - server - peers, men mer om det en annen gang, fordi det ikke er klart ennå.

For å komme i gang trenger vi selve websockets-serveren. Jeg laget den enkleste basert på melodi go-pakken:

Serverkode

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

På klienten (strømmesiden) må du først få tilgang til kameraet. Dette gjøres gjennom MediaStream API.

Vi får tilgang (tillatelse) til kamera/mikrofon gjennom Media Devices API. Dette API gir en metode MediaDevices.getUserMedia(), som viser popup. et vindu som ber brukeren om tillatelse til å få tilgang til kameraet og/eller mikrofonen. Jeg vil merke at jeg utførte alle eksperimentene i Google Chrome, men jeg tror alt vil fungere omtrent likt i Firefox.

Deretter returnerer getUserMedia() et løfte, som det sender et MediaStream-objekt til - en strøm av video-lyddata. Vi tildeler dette objektet til src-egenskapen til videoelementet. Kode:

Kringkastingssiden

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

For å kringkaste en videostrøm over stikkontakter, må du kode den et sted, bufre den og overføre den i deler. Råvideostrømmen kan ikke overføres via websockets. Det er her det kommer oss til hjelp MediaRecorder API. Denne API-en lar deg kode og bryte strømmen i biter. Jeg gjør koding for å komprimere videostrømmen for å sende færre byte over nettverket. Etter å ha brutt den i biter, kan du sende hver del til en websocket. Kode:

Vi koder videostrømmen, bryter den i biter

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

La oss nå legge til overføring via websockets. Overraskende nok er alt du trenger for dette et objekt WebSocket. Den har bare to metoder for å sende og lukke. Navnene taler for seg selv. Lagt til kode:

Vi overfører videostrømmen til serveren

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

Sendesiden er klar! La oss nå prøve å motta en videostrøm og vise den på klienten. Hva trenger vi til dette? For det første, selvfølgelig, stikkontakten. Vi knytter en "lytter" til WebSocket-objektet og abonnerer på "meldings"-hendelsen. Etter å ha mottatt et stykke binær data, kringkaster serveren vår den til abonnenter, det vil si klienter. I dette tilfellet utløses tilbakeringingsfunksjonen knyttet til "lytteren" av "meldingshendelsen" på klienten; selve objektet sendes inn i funksjonsargumentet - en del av videostrømmen kodet av vp8.

Vi aksepterer videostrøm

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

I lang tid prøvde jeg å forstå hvorfor det er umulig å umiddelbart sende de mottatte stykkene til videoelementet for avspilling, men det viste seg at dette ikke kan gjøres, selvfølgelig, du må først legge stykket i en spesiell buffer bundet til videoelementet, og først da vil det begynne å spille av videostrømmen. For dette trenger du MediaSource API и FileReader API.

MediaSource fungerer som en slags mellomledd mellom medieavspillingsobjektet og kilden til denne mediestrømmen. MediaSource-objektet inneholder en pluggbar buffer for kilden til video-/lydstrømmen. En funksjon er at bufferen bare kan inneholde Uint8-data, så du trenger en FileReader for å lage en slik buffer. Se på koden og den vil bli mer tydelig:

Spiller av videostrømmen

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

Prototypen til strømmetjenesten er klar. Den største ulempen er at videoavspilling vil ligge 100 ms bak overføringssiden; dette stiller vi selv når vi deler videostrømmen før vi overfører den til serveren. Dessuten, da jeg sjekket den bærbare datamaskinen min, akkumulerte etterslepet mellom sende- og mottakssiden gradvis, dette var tydelig synlig. Jeg begynte å lete etter måter å overvinne denne ulempen, og... kom over RTCPeerConnection API, som lar deg overføre en videostrøm uten triks som å dele strømmen i biter. Den akkumulerende forsinkelsen, tror jeg, skyldes det faktum at nettleseren omkoder hver del til webm-formatet før overføring. Jeg gravde ikke lenger, men begynte å studere WebRTC. Jeg tror jeg kommer til å skrive en egen artikkel om resultatene av forskningen min hvis jeg finner det interessant for samfunnet.

Kilde: www.habr.com

Legg til en kommentar