(Gandrīz) bezjēdzīga tīmekļa kameras straumēšana no pārlūkprogrammas. Media Stream un Websockets

Šajā rakstā es vēlos dalīties ar saviem mēģinājumiem straumēt video, izmantojot tīmekļa ligzdas, neizmantojot trešās puses pārlūkprogrammas spraudņus, piemēram, Adobe Flash Player. Lasiet tālāk, lai uzzinātu, kas no tā izriet.

Adobe Flash, agrāk Macromedia Flash, ir platforma lietojumprogrammu izveidei, kas darbojas tīmekļa pārlūkprogrammā. Pirms Media Stream API ieviešanas tā bija praktiski vienīgā platforma video un balss straumēšanai no tīmekļa kameras, kā arī dažādu konferenču un tērzēšanas veidu veidošanai pārlūkprogrammā. Multivides informācijas pārsūtīšanas protokols RTMP (Real Time Messaging Protocol) faktiski bija slēgts uz ilgu laiku, kas nozīmēja: ja vēlaties uzlabot savu straumēšanas pakalpojumu, esiet laipni un izmantojiet pašu Adobe programmatūru - Adobe Media Server (AMS).

Pēc kāda laika 2012. gadā Adobe “padevās un to izspļāva” sabiedrībai. specifikācija RTMP protokols, kas saturēja kļūdas un būtībā bija nepilnīgs. Līdz tam laikam izstrādātāji sāka paši ieviest šo protokolu, un parādījās Wowza serveris. 2011. gadā Adobe iesniedza tiesā prasību pret Wowza par ar RTMP saistīto patentu nelikumīgu izmantošanu; pēc 4 gadiem konflikts tika atrisināts draudzīgā ceļā.

Adobe Flash platforma ir vairāk nekā 20 gadus veca, un šajā laikā ir atklātas daudzas kritiskas ievainojamības, atbalsts apsolīja beigties līdz 2020. gadam, atstājot dažas straumēšanas pakalpojuma alternatīvas.

Savam projektam es nekavējoties nolēmu pilnībā atteikties no Flash izmantošanas pārlūkprogrammā. Iepriekš norādīju galveno iemeslu; Flash arī vispār netiek atbalstīts mobilajās platformās, un es patiešām negribēju izvietot Adobe Flash izstrādei operētājsistēmā Windows (vīna emulators). Tāpēc es nolēmu rakstīt klientu JavaScript. Šis būs tikai prototips, jo vēlāk uzzināju, ka uz p2p straumēšanu var veikt daudz efektīvāk, tikai man tas būs peer - server - peers, bet par to citreiz, jo tas vēl nav gatavs.

Lai sāktu, mums ir nepieciešams faktiskais Websockets serveris. Es izveidoju vienkāršāko, pamatojoties uz melody go pakotni:

Servera kods

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

Klientā (straumēšanas pusē) vispirms ir jāpiekļūst kamerai. Tas tiek darīts cauri MediaStream API.

Mēs iegūstam piekļuvi (atļauju) kamerai/mikrofonam caur Multivides ierīču API. Šī API nodrošina metodi MediaDevices.getUserMedia(), kurā tiek parādīts uznirstošais logs. logs, kurā lietotājam tiek lūgta atļauja piekļūt kamerai un/vai mikrofonam. Es vēlos atzīmēt, ka es veicu visus eksperimentus pārlūkprogrammā Google Chrome, bet es domāju, ka pārlūkprogrammā Firefox viss darbosies tāpat.

Pēc tam getUserMedia() atgriež solījumu, kuram tas nodod MediaStream objektu — video-audio datu straumi. Mēs piešķiram šo objektu video elementa rekvizītam src. Kods:

Apraides puse

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

Lai pārraidītu video straumi pa ligzdām, tā kaut kur ir jākodē, buferē un jāpārraida pa daļām. Neapstrādātu video straumi nevar pārsūtīt, izmantojot tīmekļa ligzdas. Šeit tas nāk mums palīgā MediaRecorder API. Šī API ļauj iekodēt un sadalīt straumi gabalos. Es veicu kodējumu, lai saspiestu video straumi, lai tīklā nosūtītu mazāk baitu. Sadalot to gabalos, jūs varat nosūtīt katru gabalu uz tīmekļa ligzdu. Kods:

Mēs iekodējam video straumi, sadalām to daļās

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

Tagad pievienosim pārraidi, izmantojot tīmekļa ligzdas. Pārsteidzoši, viss, kas jums nepieciešams, ir objekts WebSocket. Tam ir tikai divas sūtīšanas un aizvēršanas metodes. Nosaukumi runā paši par sevi. Pievienots kods:

Mēs pārraidām video straumi uz serveri

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

Raidījuma puse ir gatava! Tagad mēģināsim saņemt video straumi un parādīt to klientam. Kas mums tam vajadzīgs? Pirmkārt, protams, kontaktligzdas savienojums. Mēs pievienojam WebSocket objektam "klausītāju" un abonējam notikumu "ziņojums". Saņemot bināro datu daļu, mūsu serveris tos pārraida abonentiem, tas ir, klientiem. Šajā gadījumā klientam tiek aktivizēta atzvanīšanas funkcija, kas saistīta ar ziņojuma notikuma “klausītāju”; pats objekts tiek nodots funkcijas argumentam — vp8 kodētā video straumes daļai.

Mēs pieņemam video straumi

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

Ilgi mēģināju saprast, kāpēc saņemtos gabalus nav iespējams uzreiz nosūtīt uz video elementu atskaņošanai, bet izrādījās, ka to nevar izdarīt, protams, vispirms jāieliek gabals speciālā buferī, kas saistīts ar video elementu, un tikai tad tas sāks atskaņot video straumi. Šim nolūkam jums būs nepieciešams MediaSource API и FileReader API.

MediaSource darbojas kā sava veida starpnieks starp multivides atskaņošanas objektu un šīs multivides straumes avotu. MediaSource objekts satur pievienojamu buferi video/audio straumes avotam. Viena iezīme ir tāda, ka buferis var saturēt tikai Uint8 datus, tāpēc jums būs nepieciešams FileReader, lai izveidotu šādu buferi. Apskatiet kodu, un tas kļūs skaidrāks:

Tiek atskaņota video straume

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

Straumēšanas pakalpojuma prototips ir gatavs. Galvenais trūkums ir tas, ka video atskaņošana atpaliks no raidošās puses par 100 ms; mēs to iestatām paši, sadalot video straumi pirms tās pārsūtīšanas uz serveri. Turklāt, pārbaudot klēpjdatoru, pakāpeniski uzkrājās nobīde starp raidīšanas un uztveršanas pusēm, tas bija skaidri redzams. Es sāku meklēt veidus, kā pārvarēt šo trūkumu, un... sanāca RTCPeerConnection API, kas ļauj pārraidīt video straumi bez trikiem, piemēram, straumes sadalīšanas gabalos. Uzkrājošā nobīde, manuprāt, ir saistīta ar to, ka pārlūkprogramma pirms pārraides katru gabalu atkārtoti kodē webm formātā. Es nedziļinājos, bet sāku studēt WebRTC. Domāju, ka es uzrakstīšu atsevišķu rakstu par sava pētījuma rezultātiem, ja man tas būs interesanti sabiedrībai.

Avots: www.habr.com

Pievieno komentāru