Webcam (aproape) inutil streaming dintr-un browser. Media Stream și Websockets

În acest articol, vreau să împărtășesc încercările mele de a reda videoclipuri prin intermediul websocket-urilor fără a utiliza pluginuri de browser terță parte, cum ar fi Adobe Flash Player. Citiți mai departe pentru a afla ce a rezultat.

Adobe Flash, anterior Macromedia Flash, este o platformă pentru crearea de aplicații care rulează într-un browser web. Înainte de introducerea API-ului Media Stream, era practic singura platformă pentru streaming video și voce dintr-o cameră web, precum și pentru crearea diferitelor tipuri de conferințe și chat-uri în browser. Protocolul de transmitere a informațiilor media RTMP (Real Time Messaging Protocol) a fost de fapt închis pentru o lungă perioadă de timp, ceea ce însemna: dacă doriți să vă îmbunătățiți serviciul de streaming, fiți amabil să utilizați software de la Adobe înșiși - Adobe Media Server (AMS).

După ceva timp în 2012, Adobe „a renunțat și a scuipat” publicului. specificație Protocolul RTMP, care conținea erori și era în esență incomplet. În acel moment, dezvoltatorii au început să facă propriile implementări ale acestui protocol și a apărut serverul Wowza. În 2011, Adobe a intentat un proces împotriva Wowza pentru utilizarea ilegală a brevetelor legate de RTMP; după 4 ani, conflictul a fost rezolvat pe cale amiabilă.

Platforma Adobe Flash are mai bine de 20 de ani, timp în care au fost descoperite multe vulnerabilități critice, suport promis să se încheie până în 2020, lăsând puține alternative pentru serviciul de streaming.

Pentru proiectul meu, am decis imediat să renunț complet la utilizarea Flash în browser. Am indicat motivul principal mai sus; nici Flash nu este acceptat deloc pe platformele mobile și chiar nu am vrut să implementez Adobe Flash pentru dezvoltare pe Windows (emulator de vin). Așa că mi-am propus să scriu un client în JavaScript. Acesta va fi doar un prototip, deoarece mai târziu am aflat că streamingul se poate face mult mai eficient pe baza p2p, doar pentru mine va fi peer - server - peers, dar mai multe despre asta altă dată, pentru că nu este încă gata.

Pentru a începe, avem nevoie de serverul websockets real. Am făcut-o pe cea mai simplă pe baza pachetului melody go:

Cod server

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

Pe client (partea de streaming), mai întâi trebuie să accesați camera. Acest lucru se realizează prin MediaStream API.

Obținem acces (permisiune) la cameră/microfon prin intermediul API-ul Media Devices. Acest API oferă o metodă MediaDevices.getUserMedia(), care afișează pop-up. o fereastră care cere utilizatorului permisiunea de a accesa camera și/sau microfonul. Aș dori să remarc că am efectuat toate experimentele în Google Chrome, dar cred că totul va funcționa cam la fel în Firefox.

Apoi, getUserMedia() returnează o Promisiune, căreia îi transmite un obiect MediaStream - un flux de date video-audio. Atribuim acest obiect proprietății src a elementului video. Cod:

Partea de difuzare

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

Pentru a difuza un flux video prin socluri, trebuie să îl codificați undeva, să îl salvați și să îl transmiteți în părți. Fluxul video brut nu poate fi transmis prin intermediul websocket-urilor. Aici ne vine în ajutor MediaRecorder API. Acest API vă permite să codificați și să spargeți fluxul în bucăți. Fac codificare pentru a comprima fluxul video pentru a trimite mai puțini octeți prin rețea. După ce l-a rupt în bucăți, puteți trimite fiecare bucată la un websocket. Cod:

Codăm fluxul video, îl rupem în bucăți

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

Acum să adăugăm transmisia prin websocket-uri. În mod surprinzător, tot ce ai nevoie pentru asta este un obiect WebSockets. Are doar două metode de trimitere și închidere. Numele vorbesc de la sine. Cod adăugat:

Transmitem fluxul video către server

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

Partea de difuzare este gata! Acum să încercăm să primim un flux video și să-l afișăm pe client. De ce avem nevoie pentru asta? În primul rând, desigur, conexiunea priză. Atașăm un „ascultător” obiectului WebSocket și ne abonăm la evenimentul „mesaj”. După ce a primit o bucată de date binare, serverul nostru o transmite abonaților, adică clienților. În acest caz, funcția de apel invers asociată cu „ascultătorul” evenimentului „mesaj” este declanșată pe client; obiectul însuși este trecut în argumentul funcției - o parte din fluxul video codificat de vp8.

Acceptăm fluxul video

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

Multă vreme am încercat să înțeleg de ce este imposibil să trimiți imediat piesele primite la elementul video pentru redare, dar s-a dovedit că acest lucru nu se poate face, desigur, trebuie mai întâi să pui piesa într-un buffer special legat de elementul video și numai atunci va începe redarea fluxului video. Pentru asta vei avea nevoie MediaSource API и FileReader API.

MediaSource acționează ca un fel de intermediar între obiectul de redare media și sursa acestui flux media. Obiectul MediaSource conține un buffer conectabil pentru sursa fluxului video/audio. O caracteristică este că bufferul poate conține doar date Uint8, așa că veți avea nevoie de un FileReader pentru a crea un astfel de buffer. Uită-te la cod și va deveni mai clar:

Redarea fluxului video

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

Prototipul serviciului de streaming este gata. Principalul dezavantaj este că redarea video va rămâne cu 100 ms în urmă față de partea de transmisie; acest lucru îl setăm noi înșine când împărțim fluxul video înainte de a-l transmite către server. Mai mult, atunci când am verificat pe laptop, decalajul dintre partea de transmisie și cea de recepție s-a acumulat treptat, acest lucru a fost clar vizibil. Am început să caut modalități de a depăși acest dezavantaj și... am găsit API-ul RTCPeerConnection, care vă permite să transmiteți un flux video fără trucuri, cum ar fi împărțirea fluxului în bucăți. Decalajul care se acumulează, cred, se datorează faptului că browser-ul re-codifică fiecare piesă în format webm înainte de transmitere. Nu am săpat mai departe, dar am început să studiez WebRTC. Cred că voi scrie un articol separat despre rezultatele cercetării mele dacă găsesc că este interesant pentru comunitate.

Sursa: www.habr.com

Adauga un comentariu