(Preskaŭ) senutila retkamerao fluanta de retumilo. Media Stream kaj Websockets

En ĉi tiu artikolo mi volas kunhavigi miajn provojn flui video per retkonektoj sen uzi aldonaĵojn de triaj retumiloj kiel Adobe Flash Player. Legu plu por ekscii, kio rezultis el ĝi.

Adobe Flash, antaŭe Macromedia Flash, estas platformo por krei aplikojn, kiuj funkcias en TTT-legilo. Antaŭ la enkonduko de la Media Stream API, ĝi estis praktike la sola platformo por flui video kaj voĉo de retkamerao, kaj ankaŭ por krei diversajn konferencojn kaj babilojn en la retumilo. La protokolo por transdoni amaskomunikilajn informojn RTMP (Real Time Messaging Protocol) estis efektive fermita dum longa tempo, kio signifis: se vi volas akceli vian streaming-servon, bonvolu uzi programaron de Adobe mem - Adobe Media Server (AMS).

Post iom da tempo en 2012, Adobe "rezignis kaj kraĉis ĝin" al publiko. specifo RTMP-protokolo, kiu enhavis erarojn kaj estis esence nekompleta. Antaŭ tiu tempo, programistoj komencis fari siajn proprajn efektivigojn de ĉi tiu protokolo, kaj la Wowza-servilo aperis. En 2011, Adobe arkivis proceson kontraŭ Wowza por kontraŭleĝa uzo de RTMP-rilataj patentoj; post 4 jaroj, la konflikto estis solvita amikece.

La platformo Adobe Flash havas pli ol 20 jarojn, dum kiu tempo multaj kritikaj vundeblecoj estis malkovritaj, subteno promesis finiĝos antaŭ 2020, lasante malmultajn alternativojn por la streaming-servo.

Por mia projekto, mi tuj decidis tute forlasi la uzon de Flash en la retumilo. Mi indikis la ĉefan kialon supre; Flash ankaŭ tute ne estas subtenata en moveblaj platformoj, kaj mi vere ne volis disfaldi Adobe Flash por disvolviĝo en Vindozo (winemulator). Do mi komencis skribi klienton en JavaScript. Ĉi tio estos nur prototipo, ĉar poste mi eksciis, ke streaming povas esti farita multe pli efike surbaze de p2p, nur por mi ĝi estos samulo - servilo - samuloj, sed pli pri tio alian fojon, ĉar ĝi ankoraŭ ne estas preta.

Por komenci, ni bezonas la realan websockets-servilon. Mi faris la plej simplan surbaze de la melodia go-pakaĵo:

Servila kodo

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

Ĉe la kliento (flua flanko), vi unue devas aliri la fotilon. Ĉi tio estas farita tra MediaStream API.

Ni akiras aliron (permeson) al la fotilo/mikrofono tra Media Devices API. Ĉi tiu API provizas metodon MediaDevices.getUserMedia(), kiu montras ŝprucfenestron. fenestro petante la uzanton permeson aliri la fotilon kaj/aŭ mikrofonon. Mi ŝatus rimarki, ke mi faris ĉiujn eksperimentojn en Google Chrome, sed mi pensas, ke ĉio funkcios proksimume same en Firefox.

Poste, getUserMedia() resendas Promeson, al kiu ĝi pasas MediaStream-objekton - fluon de video-aŭdaj datumoj. Ni asignas ĉi tiun objekton al la src-posedaĵo de la videoelemento. Kodo:

Elsenda flanko

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

Por dissendi videofluon tra ingoj, vi devas kodi ĝin ie, bufri ĝin kaj transdoni ĝin en partoj. La kruda videofluo ne povas esti elsendita per retejsockets. Jen kie ĝi venas al nia helpo MediaRecorder API. Ĉi tiu API permesas vin kodi kaj rompi la rivereton en pecojn. Mi faras kodigon por kunpremi la videofluon por sendi malpli da bajtoj tra la reto. Rompinte ĝin en pecojn, vi povas sendi ĉiun pecon al retkonekto. Kodo:

Ni kodas la videofluon, rompas ĝin en pecojn

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

Nun ni aldonu dissendon per retaj sockets. Surprize, ĉio, kion vi bezonas por ĉi tio, estas objekto Retejo. Ĝi havas nur du metodojn sendi kaj fermi. La nomoj parolas por si mem. Aldonita kodo:

Ni transdonas la videofluon al la servilo

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

La elsenda flanko estas preta! Nun ni provu ricevi videofluon kaj montri ĝin sur la kliento. Kion ni bezonas por ĉi tio? Unue, kompreneble, la ingo-konekto. Ni kunligas "aŭskultanton" al la objekto WebSocket kaj abonas la eventon "mesaĝo". Ricevinte pecon da binaraj datumoj, nia servilo elsendas ĝin al abonantoj, tio estas, klientoj. En ĉi tiu kazo, la revokfunkcio asociita kun la "aŭskultanto" de la "mesaĝo" okazaĵo estas ekigita sur la kliento; la objekto mem estas transdonita en la funkcioargumenton - peco de la videofluo kodita de vp8.

Ni akceptas videofluon

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

Dum longa tempo mi provis kompreni, kial estas neeble tuj sendi la ricevitajn pecojn al la videoelemento por reludi, sed montriĝis, ke tio ne povas esti farita, kompreneble, vi unue devas meti la pecon en specialan bufron ligitan al. la videoelemento, kaj nur tiam ĝi komencos ludi la videofluon. Por ĉi tio vi bezonos MediaSource API и FileReader API.

MediaSource funkcias kiel speco de peranto inter la amaskomunikila reproduktadobjekto kaj la fonto de ĉi tiu amaskomunikila fluo. La MediaSource-objekto enhavas ŝtopeblan bufron por la fonto de la video/audiofluo. Unu trajto estas, ke la bufro povas nur teni Uint8-datumojn, do vi bezonos FileReader por krei tian bufron. Rigardu la kodon kaj ĝi fariĝos pli klara:

Ludante la videofluon

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

La prototipo de la streaming-servo estas preta. La ĉefa malavantaĝo estas, ke videoludado postrestas 100 ms malantaŭ la elsenda flanko; ni mem fiksas tion disigante la videofluon antaŭ ol transdoni ĝin al la servilo. Krome, kiam mi kontrolis sur mia tekkomputilo, la malfruo inter la elsenda kaj ricevanta flankoj iom post iom akumuliĝis, tio estis klare videbla. Mi komencis serĉi manierojn venki ĉi tiun malavantaĝon, kaj... renkontis RTCPeerConnection API, kiu ebligas al vi elsendi videofluon sen lertaĵoj kiel disigi la rivereton en pecojn. La akumulanta malfruo, mi pensas, ŝuldiĝas al la fakto ke la retumilo rekodas ĉiun pecon en la webm-formaton antaŭ transdono. Mi ne fosis plu, sed komencis studi WebRTC.Mi pensas, ke mi skribos apartan artikolon pri la rezultoj de mia esplorado se mi trovas ĝin interesa por la komunumo.

fonto: www.habr.com

Aldoni komenton