(Nästan) värdelös streaming av webbkamera från webbläsaren. Media Stream och Websockets

I denna artikel vill jag dela med mig av mina försök att streama video via websockets utan att använda tredjepartswebbläsartillägg som Adobe Flash Player. Vad jag fick till läsa vidare.

Adobe Flash — tidigare Macromedia Flash, är en plattform för att skapa applikationer som körs i webbläsaren. Innan Media Stream API infördes var detta nästan den enda plattformen för att streama video och ljud från webbkameran samt för att skapa olika typer av konferenser och chattar i webbläsaren. Protokollet för överföring av medieinformation, RTMP (Real Time Messaging Protocol), var faktiskt stängt under lång tid, vilket betydde: om du ville starta din streamingtjänst, var du tvungen att använda mjukvara från Adobe själva — Adobe Media Server (AMS).

Efter en tid, år 2012, "gav Adobe upp och spottade ut" en version av specifikationen protokollet RTMP, som innehöll buggar och var i huvudsak ofullständig. Vid den tiden började utvecklare skapa sina egna implementationer av detta protokoll, vilket ledde till att servern Wowza uppstod. År 2011 stämde Adobe Wowza för olaglig användning av patent relaterade till RTMP, och efter 4 år löstes konflikten fredligt.

Adobe Flash-plattformen har funnits i över 20 år, och under denna tid har många kritiska sårbarheter upptäckts, och stödet lovade upptäcktes skulle avslutas år 2020, så alternativen för streamingtjänster är inte särskilt många.

För mitt projekt bestämde jag mig direkt för att helt avstå från att använda Flash i webbläsaren. Anledningen till detta har jag angett ovan, och dessutom stöds Flash överhuvudtaget inte på mobila plattformar, och det var inte särskilt lockande att installera Adobe Flash för utveckling på Windows (med Wine-emulator). Därför började jag skriva en klient i JavaScript. Detta kommer bara att vara en prototyp, eftersom jag senare fick veta att streaming kan göras mycket mer effektivt baserat på p2p, men i mitt fall kommer det att vara peer — server — peers, men mer om detta senare, eftersom det ännu inte är klart.

För att komma igång behöver vi en egen websockets-server. Jag gjorde en mycket enkel baserad på go-paketet melody:

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

		// Sänder videoström
	m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
		m.BroadcastBinary(msg)
	})

	log.Println("Startar server...")

	http.ListenAndServe(":3000", r)
}

På klientsidan (sändande sidan) måste du först få åtkomst till kameran. Detta görs genom MediaStream API.

Att få åtkomst (tillstånd) till kamera/mikrofon genom Media Devices API. Detta API tillhandahåller metoden MediaDevices.getUserMedia(), som visar en popup som frågar användaren om tillstånd att få åtkomst till kamera och/eller mikrofon. Jag vill nämna att alla experiment jag genomfört har varit i Google Chrome, men jag tror att det fungerar ungefär likadant i Firefox.

Därefter returnerar getUserMedia() ett Promise, som skickar vidare ett MediaStream-objekt — en ström av video- och ljuddata. Detta objekt tilldelar vi src-egenskapen hos videoelementet. Koden:

Sändande sidan

<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 videoströmmen via sockets måste den på något sätt kodas, buffras och skickas i delar. En rå videoström kan inte skickas via websockets. Här kommer MediaRecorder API. Detta API låter oss koda och dela upp strömmen i bitar. Jag kodar för att komprimera videoströmmen, så att jag skickar färre byte över nätverket. Genom att dela upp den i bitar kan varje bit skickas via websocket. Koden:

Koda videoströmmen, dela upp den i bitar

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

Nu lägger vi till överföring via websockets. Oväntat nog krävs bara ett objekt WebSocket. Det har endast två metoder: send och close. Namnen talar för sig själva. Den kompletterade koden:

Ö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ändande sidan är redo! Nu låt oss försöka ta emot videoströmmen och visa den på klienten. Vad behöver vi för detta? För det första naturligtvis en socket-anslutning. Vi lägger till en 'lyssnare' (listener) på WebSocket-objektet och prenumererar på 'message'-händelsen. När vi får en bit binär data sänder vår server den till sina prenumeranter, det vill säga klienterna. På klienten aktiveras callback-funktionen kopplad till 'listener'-händelsen 'message', och funktionen får själva objektet — en bit av videoströmmen kodad som vp8.

Ta emot 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'), 
         arrayOfBlobs = [];

         socket.addEventListener('message', function (event) {
                // "кладем" полученный кусочек в массив 
                arrayOfBlobs.push(event.data);
                // здесь будем читать кусочки
                readChunk();
            });
</script>

Jag har länge försökt förstå varför man inte bara kan skicka de inhämtade bitarna direkt till videoelementet för uppspelning, men det visade sig att man först måste lägga biten i en speciell buffert kopplad till videoelementet, och först då börjar videoströmmen spelas upp. För detta behövs MediaSource API och FileReader API.

MediaSource fungerar som en medlare mellan uppspelningsobjektet för media och källan till den aktuella medieströmmen. MediaSource-objektet innehåller en ansluten buffert för källan till video-/ ljudströmmen. En funktion är att bufferten endast kan innehålla data av typen Uint8, så för att skapa en sådan buffert behövs en FileReader. Titta på koden, så blir det tydligare:

Uppspelar 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'),
        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 för streamingtjänsten är klar. Huvudnackdelen är att videouppspelningen kommer att vara försenad med 100 ms från sändarsidan, vilket vi bestämde själva när vi delade upp videoströmmen innan den skickades till servern. Dessutom, när jag testade på min bärbara dator, byggdes en fördröjning upp mellan sändande och mottagande sida, vilket var tydligt synligt. Jag började leta efter sätt att åtgärda denna nackdel och... snubblade över RTCPeerConnection API, som möjliggör överföring av videoström utan knep som att dela upp strömmen i bitar. Den uppbyggda fördröjningen, tror jag, beror på att varje bit får kodas om till webm-formatet i webbläsaren innan överföringen. Jag grävde inte djupare i det, utan började studera WebRTC, och resultatet av mina undersökningar ska jag nog skriva en separat artikel om, om jag anser att den skulle vara intressant för gemenskapen.

Källa: habr.com

Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar 🔥 Köp pålitlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster