(Hast) nuttelose webcam streaming fan in browser. Media Stream en Websockets

Yn dit artikel wol ik myn besykjen diele om fideo te streamen fia websockets sûnder browser-plugins fan tredden te brûken lykas Adobe Flash Player. Lês fierder om út te finen wat der fan kaam.

Adobe Flash, earder Macromedia Flash, is in platfoarm foar it meitsjen fan applikaasjes dy't rinne yn in webblêder. Foar de yntroduksje fan de Media Stream API wie it praktysk it ienige platfoarm foar streaming fan fideo en stim fan in webcam, lykas ek foar it meitsjen fan ferskate soarten konferinsjes en petearen yn 'e browser. It protokol foar it ferstjoeren fan media-ynformaasje RTMP (Real Time Messaging Protocol) wie feitlik foar in lange tiid sluten, wat betsjutte: as jo jo streamingtsjinst wolle stimulearje, wês freonlik genôch om software fan Adobe sels te brûken - Adobe Media Server (AMS).

Nei in skoft yn 2012 joech Adobe "op en spuide it út" oan it publyk. spesifikaasje RTMP-protokol, dat flaters befette en yn wêzen net kompleet wie. Tsjin dy tiid begûnen ûntwikkelders har eigen ymplemintaasjes fan dit protokol te meitsjen, en de Wowza-tsjinner ferskynde. Yn 2011 hat Adobe in rjochtsaak tsjin Wowza yntsjinne foar yllegaal gebrûk fan RTMP-relatearre oktroaien; nei 4 jier waard it konflikt yn freonskip oplost.

It Adobe Flash-platfoarm is mear as 20 jier âld, yn hokker tiid binne in protte krityske kwetsberens ûntdutsen, stipe tasein om 2020 te einigjen, wat in pear alternativen oerlitte foar de streamingtsjinst.

Foar myn projekt besleat ik fuortendaliks it gebrûk fan Flash yn 'e browser folslein te ferlitten. Ik haw hjirboppe de wichtichste reden oanjûn; Flash wurdt ek hielendal net stipe op mobile platfoarms, en ik woe echt Adobe Flash net ynsette foar ûntwikkeling op Windows (wynemulator). Dat ik sette út om in klant te skriuwen yn JavaScript. Dit sil gewoan in prototype wêze, om't ik letter learde dat streaming folle effisjinter kin wurde op basis fan p2p, allinich foar my sil it peer - server - peers wêze, mar mear oer dat in oare kear, om't it noch net klear is.

Om te begjinnen, hawwe wy de eigentlike websockets-tsjinner nedich. Ik makke de ienfâldichste basearre op it melody go-pakket:

Tsjinner koade

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

Op 'e kliïnt (streamingkant) moatte jo earst tagong krije ta de kamera. Dit wurdt dien troch MediaStream API.

Wy krije tagong (tastimming) ta de kamera / mikrofoan troch Media Apparaten API. Dizze API jout in metoade MediaDevices.getUserMedia(), dy't popup toant. in finster dat de brûker om tastimming freget om tagong te krijen ta de kamera en/of mikrofoan. Ik wol opmerke dat ik alle eksperiminten yn Google Chrome útfierde, mar ik tink dat alles sawat itselde sil wurkje yn Firefox.

Folgjende, getUserMedia() jout in belofte, wêrnei't it in MediaStream-objekt trochjûn - in stream fan fideo-audiogegevens. Wy jouwe dit objekt oan it src-eigenskip fan it fideo-elemint. Koade:

Omrop kant

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

Om in fideostream oer sockets út te stjoeren, moatte jo it earne kodearje, bufferje en it yn dielen ferstjoere. De rauwe fideostream kin net oerdroegen wurde fia websockets. Dit is wêr't it komt foar ús help MediaRecorder API. Dizze API lit jo de stream yn stikken kodearje en brekke. Ik doch kodearring om de fideostream te komprimearjen om minder bytes oer it netwurk te stjoeren. Nei't jo it yn stikken hawwe brutsen, kinne jo elk stik nei in websocket stjoere. Koade:

Wy kodearje de fideostream, brekke it yn dielen

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

Litte wy no oerdracht tafoegje fia websockets. Ferrassend, alles wat jo nedich hawwe foar dit is in foarwerp WebSockets. It hat mar twa metoaden ferstjoere en slute. De nammen sprekke foar harsels. Koade tafoege:

Wy stjoere de fideostream nei de tsjinner

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

De útstjoerkant is klear! Litte wy no besykje in fideostream te ûntfangen en it op 'e kliïnt werjaan. Wat hawwe wy hjirfoar nedich? As earste, fansels, de socket ferbining. Wy hechtsje in "harker" oan it WebSocket-objekt en abonnearje op it 'berjocht'-evenemint. Nei it ûntfangen fan in stik binêre gegevens, stjoert ús server it út nei abonnees, dat is kliïnten. Yn dit gefal wurdt de werombelfunksje ferbûn mei de "harker" fan it 'berjocht'-evenemint op 'e kliïnt trigger; it objekt sels wurdt trochjûn yn it funksjeargumint - in stik fan 'e fideostream kodearre troch vp8.

Wy akseptearje video stream

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

Ik haw in lange tiid besocht te begripen wêrom't it ûnmooglik is om de ûntfongen stikken fuortendaliks nei it fideo-elemint te stjoeren foar ôfspieljen, mar it die bliken dat dit net kin, fansels, jo moatte it stik earst yn in spesjale buffer pleatse bûn oan it fideo-elemint, en allinich dan sil it de fideostream begjinne te spyljen. Hjirfoar sille jo nedich hawwe MediaSource API и FileReader API.

MediaSource fungearret as in soarte fan tuskenpersoan tusken it media-ôfspielobjekt en de boarne fan dizze mediastream. It MediaSource-objekt befettet in pluggable buffer foar de boarne fan 'e fideo-/audiostream. Ien funksje is dat de buffer allinich Uint8-gegevens kin hâlde, dus jo sille in FileReader nedich wêze om sa'n buffer te meitsjen. Sjoch nei de koade en it sil dúdliker wurde:

It spieljen fan de fideostream

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

It prototype fan 'e streamingtsjinst is klear. It wichtichste neidiel is dat it ôfspieljen fan fideo's 100 ms efter de útstjoerkant sil bliuwe; wy stelle dit sels yn by it splitsen fan de fideostream foardat it nei de tsjinner ferstjoerd wurdt. Boppedat, doe't ik kontrolearre op myn laptop, de efterstân tusken de stjoerende en ûntfangende kanten stadichoan sammele, dit wie dúdlik sichtber. Ik begon te sykjen nei manieren om dit neidiel te oerwinnen, en ... kaam oer RTCPeerConnection API, wêrmei jo in fideostream kinne ferstjoere sûnder trúkjes lykas it splitsen fan de stream yn stikken. De akkumulearjende efterstân, tink ik, komt troch it feit dat de browser elk stik opnij kodearret yn it webm-formaat foar oerdracht. Ik haw net groeven fierder, mar begûn te studearjen WebRTC. Ik tink dat ik sil skriuwe in apart artikel oer de resultaten fan myn ûndersyk as ik fyn it nijsgjirrich foar de mienskip.

Boarne: www.habr.com

Add a comment