(Nästan) värdelös webbkameraströmning från en webbläsare. Media Stream och Websockets

I den här artikeln vill jag dela mina försök att strömma video via websockets utan att använda tredjeparts webbläsarplugin som Adobe Flash Player. Läs vidare för att ta reda på vad det blev.

Adobe Flash, tidigare Macromedia Flash, är en plattform för att skapa applikationer som körs i en webbläsare. Innan introduktionen av Media Stream API var det praktiskt taget den enda plattformen för att streama video och röst från en webbkamera, samt för att skapa olika typer av konferenser och chattar i webbläsaren. Protokollet för att överföra medieinformation RTMP (Real Time Messaging Protocol) var faktiskt stängt under en lång tid, vilket innebar: om du vill boosta din streamingtjänst, var vänlig nog att använda programvara från Adobe själva - Adobe Media Server (AMS).

Efter en tid 2012 "gav Adobe upp och spottade ut det" till allmänheten. Specifikation RTMP-protokollet, som innehöll fel och var i huvudsak ofullständigt. Vid den tiden började utvecklare göra sina egna implementeringar av detta protokoll, och Wowza-servern dök upp. 2011 lämnade Adobe in en stämningsansökan mot Wowza för olaglig användning av RTMP-relaterade patent; efter fyra år löstes konflikten i godo.

Adobe Flash-plattformen är mer än 20 år gammal, under vilken tid många kritiska sårbarheter har upptäckts, support lovat att avslutas till 2020, vilket lämnar få alternativ för streamingtjänsten.

För mitt projekt bestämde jag mig omedelbart för att helt överge användningen av Flash i webbläsaren. Jag angav huvudorsaken ovan; Flash stöds inte alls på mobila plattformar, och jag ville verkligen inte distribuera Adobe Flash för utveckling på Windows (vinemulator). Så jag gav mig i kast med att skriva en klient i JavaScript. Det här kommer bara att vara en prototyp, eftersom jag senare lärde mig att streaming kan göras mycket mer effektivt baserat på p2p, bara för mig kommer det att vara peer - server - peers, men mer om det en annan gång, eftersom det inte är klart än.

För att komma igång behöver vi själva websockets-servern. Jag gjorde den enklaste baserad på melodi go-paketet:

Serverkod

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ömningssidan) måste du först komma åt kameran. Detta görs genom MediaStream API.

Vi får tillgång (tillstånd) till kameran/mikrofonen genom Media Devices API. Detta API tillhandahåller en metod MediaDevices.getUserMedia(), som visar popup. ett fönster som ber användaren om tillåtelse att komma åt kameran och/eller mikrofonen. Jag skulle vilja notera att jag utförde alla experiment i Google Chrome, men jag tror att allt kommer att fungera ungefär likadant i Firefox.

Därefter returnerar getUserMedia() ett löfte, till vilket det skickar ett MediaStream-objekt - en ström av video-ljuddata. Vi tilldelar detta objekt till egenskapen src för videoelementet. Koda:

Sändningssidan

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

För att sända en videoström över uttag måste du koda den någonstans, buffra den och överföra den i delar. Råvideoströmmen kan inte överföras via websockets. Det är här det kommer till vår hjälp MediaRecorder API. Detta API låter dig koda och bryta strömmen i bitar. Jag kodar för att komprimera videoströmmen för att skicka färre byte över nätverket. Efter att ha brutit upp den i bitar kan du skicka varje del till en websocket. Koda:

Vi kodar videoströmmen, delar upp den i delar

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

Låt oss nu lägga till överföring via websockets. Överraskande nog är allt du behöver för detta ett föremål WebSocket. Den har bara två metoder för att skicka och stänga. Namnen talar för sig själva. Tillagd kod:

Vi överför videoströmmen till servern

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

Sändningssidan är klar! Låt oss nu försöka ta emot en videoström och visa den på klienten. Vad behöver vi för detta? För det första, naturligtvis, uttagsanslutningen. Vi kopplar en "lyssnare" till WebSocket-objektet och prenumererar på "meddelande"-händelsen. Efter att ha fått en bit binär data sänder vår server den till abonnenter, det vill säga klienter. I det här fallet utlöses återuppringningsfunktionen associerad med "avlyssnaren" av "meddelande"-händelsen på klienten; själva objektet skickas till funktionsargumentet - en del av videoströmmen kodad av vp8.

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

Under en lång tid försökte jag förstå varför det är omöjligt att omedelbart skicka de mottagna bitarna till videoelementet för uppspelning, men det visade sig att detta inte kan göras, naturligtvis måste du först lägga biten i en speciell buffert bunden till videoelementet, och först då börjar det spela upp videoströmmen. För detta behöver du MediaSource API и FileReader API.

MediaSource fungerar som en slags mellanhand mellan mediauppspelningsobjektet och källan till denna mediaström. MediaSource-objektet innehåller en pluggbar buffert för källan till video-/ljudströmmen. En funktion är att bufferten bara kan innehålla Uint8-data, så du behöver en FileReader för att skapa en sådan buffert. Titta på koden så blir den tydligare:

Spelar upp 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 av streamingtjänsten är klar. Den största nackdelen är att videouppspelningen släpar efter sändningssidan med 100 ms; vi ställer in detta själva när vi delar upp videoströmmen innan den överförs till servern. Dessutom, när jag kollade på min bärbara dator, ackumulerades fördröjningen mellan sändnings- och mottagningssidan gradvis, detta var tydligt synligt. Jag började leta efter sätt att övervinna denna nackdel, och... kom över RTCPeerConnection API, som låter dig överföra en videoström utan knep som att dela upp strömmen i bitar. Den ackumulerande fördröjningen tror jag beror på det faktum att webbläsaren kodar om varje del till webm-formatet innan sändning. Jag grävde inte längre utan började studera WebRTC. Jag tror att jag kommer att skriva en separat artikel om resultaten av min forskning om jag finner det intressant för samhället.

Källa: will.com

Lägg en kommentar