(Për nearly) streaming të pa dobishëm të kamerës prej shfletuesit. Media Stream dhe Websockets

Në këtë artikull, do të ndaj përpjekjet e mia për të realizuar streaming video përmes websockets pa përdorur plugins të jashtme për shfletuesin, si Adobe Flash Player. Çfarë kam arritur, lexoni më poshtë.

Adobe Flash — më parë Macromedia Flash, është një platformë për krijimin e aplikacioneve që funksionojnë në shfletuesin në internet. Para implementimit të Media Stream API, ishte praktikisht platforma e vetme për transmetimin e videos dhe zërit nga kamera e uebit, si dhe për krijimin e konferencave dhe bisedave të ndryshme në shfletues. Protokolli për transmetimin e informacionit mediatik, RTMP (Real Time Messaging Protocol), ishte faktikisht një protokoll i mbyllur për një kohë të gjatë, çka do të thoshte: nëse dëshiron të ngresh shërbimin tënd të transmetimit, duhej ta përdorje softuerin e vetë Adobe — Adobe Media Server (AMS).

Pas disa kohësh, në vitin 2012, Adobe "u dorëzua dhe e hodhi" në gjykimin e publikut specifikimi protokollin RTMP, i cili përmbante gabime dhe në thelb nuk ishte i plotë. Në atë kohë, zhvilluesit filluan të bënin implementimet e tyre të këtij protokolli, kështu u shfaq serveri Wowza. Në vitin 2011, Adobe paditi Wowza për përdorim të paligjshëm të patentave të lidhura me RTMP, dhe pas 4 vjetësh, konflikti u zgjidh paqësisht.

Platforma Adobe Flash tani është më shumë se 20 vjeçare, dhe gjatë kësaj kohe kanë dalë në pah shumë dobësi kritike, mbështetja u premtoi të kishte mbaruar deri në vitin 2020, kështu që alternativat për shërbimin e transmetimit nuk kanë shumë.

Për projektin tim, vendosa menjëherë të heq dorë nga përdorimi i Flash në shfletues. Arsyeja kryesore e kam përmendur më lart; gjithashtu, Flash nuk mbështetet fare në platformat mobile, dhe së pari nuk doja aspak të implementoja Adobe Flash për zhvillim në Windows (në emuluesin Wine). Prandaj, fillova të shkruaj një klient në JavaScript. Ky do të jetë vetëm një prototip, pasi më vonë mësova se transmetimi mund të realizohet shumë më efektivisht në bazë të p2p, vetëm se këtu do të jetë peer — server — peers, por për këtë do të flasim një herë tjetër, sepse akoma nuk është gati.

Për të filluar punën, na nevojitet një server websockets. Krijova një të thjeshtë në bazë të paketës Go, melody:

Kodi i pjesës server

paketa kryesore

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

         // Ne po dërgojmë një video stream 
	m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
		m.BroadcastBinary(msg)
	})

	log.Println("Duke nisur serverin...")

	http.ListenAndServe(":3000", r)
}

Në klient (anës transmetues) fillimisht duhet të sigurohemi që kemi qasje në kamerë. Kjo bëhet përmes MediaStream API.

Sigurojmë qasje (leje) në kamerë/ mikrofon përmes Media Devices API. Ky API ofron metodën MediaDevices.getUserMedia(), e cila shfaq një dritare tërheqëse, duke pyetur përdoruesin për lejen e qasjes në kamerë dhe/ose mikrofon. Do të doja të vërej se të gjitha eksperimentet i kam realizuar në Google Chrome, por mendoj se në Firefox do të funksionojë përafërsisht njësoj.

Më tej, getUserMedia() kthen një Promise, e cila dërgon objektin MediaStream — një rrjedhë të dhënash video-audio. Ky objekt e caktuar në pronën src të elementit video. Kodi:

Anës transmetues

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

Për të transmetuar një video stream përmes soketëve, ne duhet ta kodifikojmë, ta buferizojmë dhe ta dërgojmë në pjesë. Një video stream e papërcaktuar nuk mund të dërgohet përmes websockets. Këtu na ndihmon MediaRecorder API. Ky API lejon kodimin dhe ndarjen e rrjedhës në copëza. Kodimi e bëj për të kompresuar videon, në mënyrë që të dërgoj sa më pak byte përmes rrjetit. Pasi të ndarë në copëza, çdo copëza mund të dërgohet në websocket. Kodi:

E kodifikojmë videon, e ndajmë në pjesë

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

Tani e shtojmë dërgimin përmes websockets. Siç mund të duket e çuditshme, për këtë na nevojitet vetëm objekti WebSocket. Ka vetëm dy metoda send dhe close. Emrat flasin vetë për veten. Kodi i plotësuar:

Dërgojmë video stream në 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>

Anës transmetues është gati! Tani le të përpiqemi të marrim video stream dhe ta shfaqim në klient. Çfarë na nevojitet për këtë? Së pari, natyrisht, një lidhje në socket. Ne lidhim një "dëgjues" (listener) në objektin WebSocket, dhe regjistrohemi për ngjarjen 'message'. Pas marrjes së një copëze të dhënash binar, serveri jonë e transmeton atë për abonentët, domethënë për klientët. Në klient, funksioni callback i lidhur me "dëgjuesin" e ngjarjes 'message', merr si argument objektin — copëzë e video stream-it e koduar vp8.

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

Ka shumë kohë që përpiqesha të kuptoja pse nuk mund të dërgoja menjëherë copat e marra për t'u luajtur në elementin video, por rezultoi se nuk mund të bëhej kështu, së pari duhet ta vendosësh copën në një bufer të veçantë, të lidhur me elementin video, dhe vetëm atëherë do të fillojë të luajë video-streamin. Për këtë do të nevojitet MediaSource API dhe FileReader API.

MediaSource vepron si një ndërmjetës midis objektit që luan media dhe burimit të këtij streami mediatik. Objekti MediaSource përmban një bufer të lidhur për burimin e streamit video/audio. Një veçori është se buferi mund të përmbajë vetëm të dhëna nga lloji Uint8, prandaj për të krijuar një bufer të tillë do të nevojitet FileReader. Shikoni kodin, dhe do të bëhet më e qartë:

Po luajmë video-streamin

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

Prototipi i shërbimit të streaming është gati. Minus kryesor është se reproduktimi i videos do të mbetet prapa palës transmetuese me 100 ms, që ne e vendosëm vetë gjatë ndarjes së streamit të videos përpara se ta dërgonim në server. Për më tepër, kur kontrollova në laptopin tim, filloi të grumbullohej vonesa midis palës transmetuese dhe asaj njëpranuese; kjo ishte shumë e dukshme. Fillova të kërkoj mënyra për të kapërcyer këtë mangësi, dhe... ra në RTCPeerConnection API, i cili lejon dërgimin e video-streamit pa ndërlikime si ndarja e streamit në copa. Vonesa e grumbulluar, mendoj se ndodh për shkak se në shfletues përpara dërgimit ndodh ricodimi i çdo cope në formatin webm. Nuk u thella më tej, dhe fillova të mësoj për WebRTC, për rezultatet e kërkimeve të mia mendoj se do të shkruaj një artikull të veçantë, nëse e konsideroj të tillë interesante për komunitetin.

Burimi: habr.com

Blini hostim të besueshëm për faqe interneti me mbrojtje DDoS, serverë VPS VDS 🔥 Blini hostim të besueshëm për faqe interneti me mbrojtje DDoS, serverë VPS VDS - ProHoster