(Demək olar ki) brauzerdən axın edən yararsız veb-kamera. Media Stream və Websockets

Bu yazıda mən Adobe Flash Player kimi üçüncü tərəf brauzer plaginlərindən istifadə etmədən veb-sockets vasitəsilə video yayımlamaq cəhdlərimi bölüşmək istəyirəm. Bunun nədən gəldiyini öyrənmək üçün oxuyun.

Adobe Flash, əvvəllər Macromedia Flash, veb brauzerdə işləyən proqramlar yaratmaq üçün platformadır. Media Stream API-nin tətbiqindən əvvəl o, veb-kameradan video və səs axını, həmçinin brauzerdə müxtəlif növ konfranslar və söhbətlər yaratmaq üçün praktiki olaraq yeganə platforma idi. Media məlumatlarının ötürülməsi protokolu RTMP (Real Time Messaging Protocol) əslində uzun müddət bağlandı, bu o demək idi: axın xidmətinizi artırmaq istəyirsinizsə, Adobe-nin proqramlarından istifadə etmək üçün kifayət qədər xeyirxah olun - Adobe Media Server (AMS).

2012-ci ildə bir müddət sonra Adobe ictimaiyyətə "təslim oldu və tükürdü". spesifikasiya Səhvləri ehtiva edən və mahiyyətcə natamam olan RTMP protokolu. O vaxta qədər tərtibatçılar bu protokolun öz tətbiqlərini etməyə başladılar və Wowza serveri meydana çıxdı. 2011-ci ildə Adobe RTMP ilə əlaqəli patentlərdən qeyri-qanuni istifadəyə görə Wowza-ya qarşı iddia qaldırdı; 4 ildən sonra münaqişə sülh yolu ilə həll edildi.

Adobe Flash platformasının 20 ildən çox yaşı var və bu müddət ərzində bir çox kritik boşluqlar aşkar edilib, dəstək söz verdi axın xidməti üçün bir neçə alternativ buraxaraq 2020-ci ilə qədər sona çatacaq.

Layihəm üçün dərhal brauzerdə Flash istifadəsindən tamamilə imtina etmək qərarına gəldim. Yuxarıda əsas səbəbi qeyd etdim; Flash da mobil platformalarda ümumiyyətlə dəstəklənmir və mən həqiqətən Adobe Flash-ı Windows-da (şərab emulyatoru) inkişaf etdirmək üçün yerləşdirmək istəmirdim. Beləliklə, mən JavaScript-də müştəri yazmağa başladım. Bu, sadəcə bir prototip olacaq, çünki sonradan öyrəndim ki, axın p2p əsasında daha səmərəli həyata keçirilə bilər, yalnız mənim üçün bu, həmyaşıd - server - həmyaşıdlar olacaq, lakin başqa vaxt daha çox olacaq, çünki hələ hazır deyil.

Başlamaq üçün bizə faktiki websockets server lazımdır. Melody go paketi əsasında ən sadəini hazırladım:

Server kodu

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

Müştəridə (axın tərəfində) əvvəlcə kameraya daxil olmalısınız. Bu vasitəsilə həyata keçirilir MediaStream API.

Biz vasitəsilə kameraya/mikrofona giriş (icazə) əldə edirik Media Cihazları API. Bu API bir üsul təqdim edir MediaDevices.getUserMedia(), açılan pəncərəni göstərir. istifadəçidən kameraya və/yaxud mikrofona daxil olmaq üçün icazə tələb edən pəncərə. Qeyd etmək istərdim ki, mən bütün təcrübələri Google Chrome-da aparmışam, amma düşünürəm ki, Firefox-da hər şey təxminən eyni olacaq.

Sonra, getUserMedia() MediaStream obyektini - video-audio məlumat axınını ötürdüyü Promise qaytarır. Bu obyekti video elementin src xassəsinə təyin edirik. Kod:

Yayım tərəfi

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

Video axınını rozetkalar üzərindən yayımlamaq üçün onu haradasa kodlaşdırmaq, bufer etmək və hissə-hissə ötürmək lazımdır. Xam video axını veb yuvalar vasitəsilə ötürülə bilməz. Bu, bizim köməyimizə çatır MediaRecorder API. Bu API sizə axını kodlamağa və parçalara ayırmağa imkan verir. Şəbəkə üzərindən daha az bayt göndərmək üçün video axını sıxışdırmaq üçün kodlaşdırmanı edirəm. Onu parçalara ayıraraq, hər bir parçanı veb yuvasına göndərə bilərsiniz. Kod:

Video axını kodlayırıq, onu parçalara ayırırıq

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

İndi veb-soketlər vasitəsilə ötürülmə əlavə edək. Təəccüblüdür ki, bunun üçün sizə lazım olan hər şey bir obyektdir VebSocket. Onun göndərmə və bağlama yalnız iki üsulu var. Adlar özləri üçün danışır. Əlavə edilmiş kod:

Video axınını serverə ötürürük

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

Yayım tərəfi hazırdır! İndi video axını qəbul etməyə və onu müştəridə göstərməyə çalışaq. Bunun üçün bizə nə lazımdır? Birincisi, əlbəttə ki, rozetka bağlantısı. Biz WebSocket obyektinə “dinləyici” əlavə edirik və “mesaj” hadisəsinə abunə oluruq. İkili məlumatların bir hissəsini aldıqdan sonra serverimiz onu abunəçilərə, yəni müştərilərə yayımlayır. Bu halda, "mesaj" hadisəsinin "dinləyicisi" ilə əlaqəli geri çağırış funksiyası müştəridə işə salınır; obyektin özü funksiya arqumentinə - vp8 ilə kodlanmış video axınının bir hissəsinə ötürülür.

Video axını qəbul edirik

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

Uzun müddətdir ki, qəbul edilmiş parçaları oxutmaq üçün dərhal video elementə göndərməyin niyə qeyri-mümkün olduğunu anlamağa çalışdım, amma məlum oldu ki, bunu etmək mümkün deyil, əlbəttə ki, əvvəlcə parçanı xüsusi bir buferə qoymalısınız. video elementi və yalnız bundan sonra video axını oynatmağa başlayacaq. Bunun üçün sizə lazım olacaq MediaSource API и FileReader API.

MediaSource media oxutma obyekti ilə bu media axınının mənbəyi arasında bir növ vasitəçi kimi çıxış edir. MediaSource obyekti video/audio axınının mənbəyi üçün qoşula bilən buferi ehtiva edir. Xüsusiyyətlərdən biri budur ki, bufer yalnız Uint8 məlumatlarını saxlaya bilər, ona görə də belə bir bufer yaratmaq üçün sizə FileReader lazımdır. Koda baxın və daha aydın olacaq:

Video axını oynayır

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

Striminq xidmətinin prototipi hazırdır. Əsas çatışmazlıq odur ki, videonun oxudulması ötürücü tərəfdən 100 ms geri qalacaq; biz bunu video axınını serverə ötürməzdən əvvəl bölərkən özümüz təyin edirik. Üstəlik, dizüstü kompüterimi yoxlayanda, ötürücü və qəbuledici tərəflər arasındakı gecikmə tədricən yığıldı, bu aydın görünürdü. Bu dezavantajı aradan qaldırmağın yollarını axtarmağa başladım və... qarşıma çıxdı RTCPeerConnection API, video axınını parçalara bölmək kimi hiylələr olmadan ötürməyə imkan verir. Yığılan gecikmə, məncə, brauzerin ötürülməzdən əvvəl hər bir parçanı webm formatına yenidən kodlaşdırması ilə əlaqədardır. Mən daha çox araşdırmadım, amma WebRTC-ni öyrənməyə başladım. Düşünürəm ki, cəmiyyət üçün maraqlı olsa, tədqiqatımın nəticələri haqqında ayrıca məqalə yazacam.

Mənbə: www.habr.com

Добавить комментарий