(Prawie) bezużyteczne przesyłanie strumieniowe z kamery internetowej z przeglądarki. Strumień multimediów i gniazda internetowe

W tym artykule chcę podzielić się moimi próbami przesyłania strumieniowego wideo za pośrednictwem gniazd sieciowych bez korzystania z wtyczek do przeglądarek innych firm, takich jak Adobe Flash Player. Czytaj dalej, aby dowiedzieć się, co z tego wynikło.

Adobe Flash, dawniej Macromedia Flash, to platforma służąca do tworzenia aplikacji uruchamianych w przeglądarce internetowej. Przed wprowadzeniem API Media Stream była to praktycznie jedyna platforma do strumieniowego przesyłania obrazu i głosu z kamery internetowej, a także do tworzenia różnego rodzaju konferencji i czatów w przeglądarce. Protokół przesyłania informacji medialnych RTMP (Real Time Messaging Protocol) był właściwie już dawno zamknięty, co oznaczało, że jeśli chcesz usprawnić swoją usługę przesyłania strumieniowego, bądź na tyle uprzejmy, aby skorzystać z oprogramowania firmy Adobe - Adobe Media Server (AMS).

Po pewnym czasie w 2012 roku Adobe „poddało się i wypluło to” opinii publicznej. specyfikacja Protokół RTMP, który zawierał błędy i był w zasadzie niekompletny. W tym czasie programiści zaczęli tworzyć własne implementacje tego protokołu i pojawił się serwer Wowza. W 2011 roku Adobe złożyło pozew przeciwko firmie Wowza za nielegalne wykorzystanie patentów związanych z RTMP, po 4 latach konflikt został rozwiązany polubownie.

Platforma Adobe Flash ma ponad 20 lat i w tym czasie odkryto wiele krytycznych luk w zabezpieczeniach, wsparcie obiecał zakończyć do 2020 r., pozostawiając niewiele alternatyw dla usługi przesyłania strumieniowego.

W przypadku mojego projektu od razu zdecydowałem się całkowicie zrezygnować z używania Flasha w przeglądarce. Główny powód wskazałem powyżej; Flash również nie jest w ogóle obsługiwany na platformach mobilnych i naprawdę nie chciałem wdrażać Adobe Flash do programowania w systemie Windows (emulator wina). Postanowiłem więc napisać klienta w JavaScript. To będzie tylko prototyp, bo później dowiedziałem się, że streaming można zrobić znacznie wydajniej w oparciu o p2p, tylko u mnie będzie to peer - serwer - peery, ale o tym innym razem, bo to jeszcze nie jest gotowe.

Aby rozpocząć, potrzebujemy rzeczywistego serwera websockets. Zrobiłem najprostszy w oparciu o pakiet Melody Go:

Kod serwera

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

Najpierw musisz uzyskać dostęp do kamery po stronie klienta (po stronie przesyłania strumieniowego). Odbywa się to poprzez API MediaStream.

Dostęp (pozwolenie) do kamery/mikrofonu uzyskujemy poprzez Interfejs API urządzeń multimedialnych. Ten interfejs API udostępnia metodę MediaDevices.getUserMedia(), który wyświetla wyskakujące okienko. okno z prośbą o pozwolenie na dostęp do kamery i/lub mikrofonu. Pragnę zaznaczyć, że wszystkie eksperymenty przeprowadziłem w przeglądarce Google Chrome, ale myślę, że w przeglądarce Firefox wszystko będzie działać mniej więcej tak samo.

Następnie getUserMedia() zwraca Promise, do którego przekazuje obiekt MediaStream – strumień danych wideo-audio. Przypisujemy ten obiekt do właściwości src elementu wideo. Kod:

Strona nadawcza

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

Aby transmitować strumień wideo przez gniazda, należy go gdzieś zakodować, buforować i przesyłać w częściach. Surowego strumienia wideo nie można przesyłać za pośrednictwem gniazd sieciowych. I tu właśnie przychodzi nam z pomocą API MediaRecordera. Ten interfejs API umożliwia kodowanie i dzielenie strumienia na kawałki. Koduję, aby skompresować strumień wideo w celu przesłania mniejszej liczby bajtów przez sieć. Po podzieleniu go na kawałki możesz wysłać każdy element do websocketa. Kod:

Kodujemy strumień wideo, dzielimy go na części

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

Dodajmy teraz transmisję poprzez websockety. Co zaskakujące, wszystko, czego potrzebujesz do tego, to przedmiot WebSocket. Ma tylko dwie metody wysyłania i zamykania. Nazwy mówią same za siebie. Dodany kod:

Przesyłamy strumień wideo na serwer

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

Strona nadawcza jest gotowa! Spróbujmy teraz odebrać strumień wideo i wyświetlić go na kliencie. Czego potrzebujemy do tego? Po pierwsze oczywiście podłączenie do gniazdka. Dołączamy „słuchacza” do obiektu WebSocket i subskrybujemy zdarzenie „message”. Po otrzymaniu fragmentu danych binarnych nasz serwer rozsyła go do abonentów, czyli klientów. W tym przypadku na kliencie uruchamiana jest funkcja wywołania zwrotnego powiązana ze „słuchaczem” zdarzenia „message”, a do argumentu funkcji przekazywany jest sam obiekt – fragment strumienia wideo zakodowany przez vp8.

Akceptujemy strumień wideo

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

Długo próbowałem zrozumieć, dlaczego nie można od razu przesłać odebranych fragmentów do elementu wideo w celu odtworzenia, ale okazało się, że nie da się tego zrobić, oczywiście trzeba najpierw umieścić utwór w specjalnym buforze powiązanym z element wideo i dopiero wtedy rozpocznie odtwarzanie strumienia wideo. Do tego będziesz potrzebować API MediaSource и API FileReadera.

MediaSource działa jako swego rodzaju pośrednik pomiędzy obiektem odtwarzania multimediów a źródłem tego strumienia multimediów. Obiekt MediaSource zawiera podłączany bufor dla źródła strumienia wideo/audio. Jedną z cech jest to, że bufor może przechowywać tylko dane Uint8, więc do utworzenia takiego bufora będziesz potrzebować FileReader. Spójrz na kod, a stanie się jaśniejszy:

Odtwarzanie strumienia wideo

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

Prototyp usługi streamingowej jest gotowy. Główną wadą jest to, że odtwarzanie wideo będzie opóźnione w stosunku do strony nadawczej o 100 ms; sami ustawiamy to podczas dzielenia strumienia wideo przed przesłaniem go na serwer. Co więcej, kiedy sprawdziłem na swoim laptopie, opóźnienie między stroną nadawczą i odbiorczą stopniowo narastało, było to wyraźnie widoczne. Zacząłem szukać sposobów na pokonanie tej wady i... natknąłem się API RTCPeerConnection, co pozwala na przesyłanie strumienia wideo bez sztuczek, takich jak dzielenie strumienia na kawałki. Myślę, że narastające opóźnienie wynika z faktu, że przeglądarka ponownie koduje każdy fragment do formatu webm przed transmisją. Nie drążyłem dalej, tylko zacząłem studiować WebRTC.Myślę, że napiszę osobny artykuł o wynikach moich badań, jeśli uznam to za interesujące dla społeczności.

Źródło: www.habr.com

Dodaj komentarz