(Case) inútil transmisión de webcam desde un navegador. Media Stream e Websockets

Neste artigo quero compartir os meus intentos de transmitir vídeo a través de websockets sen usar complementos de navegador de terceiros como Adobe Flash Player. Sigue lendo para descubrir o que saíu.

Adobe Flash, antes Macromedia Flash, é unha plataforma para crear aplicacións que se executan nun navegador web. Antes da introdución da API Media Stream, era practicamente a única plataforma para transmitir vídeo e voz desde unha cámara web, así como para crear varios tipos de conferencias e chats no navegador. O protocolo para transmitir información multimedia RTMP (Protocolo de mensaxería en tempo real) estivo realmente pechado durante moito tempo, o que significaba: se queres aumentar o teu servizo de transmisión, ten a amabilidade de utilizar o software propio de Adobe: Adobe Media Server (AMS).

Despois dun tempo en 2012, Adobe "renunciou e cuspiro" ao público. especificación Protocolo RTMP, que contiña erros e estaba esencialmente incompleto. Nese momento, os desenvolvedores comezaron a facer as súas propias implementacións deste protocolo e apareceu o servidor Wowza. En 2011, Adobe presentou unha demanda contra Wowza por uso ilegal de patentes relacionadas con RTMP; despois de 4 anos, o conflito resolveuse amigablemente.

A plataforma Adobe Flash ten máis de 20 anos, tempo durante o que se descubriron moitas vulnerabilidades críticas, soporte prometido para rematar en 2020, deixando poucas alternativas para o servizo de streaming.

Para o meu proxecto, inmediatamente decidín abandonar completamente o uso de Flash no navegador. Indiquei o motivo principal anteriormente; Flash tampouco é compatible en absoluto nas plataformas móbiles e realmente non quería implementar Adobe Flash para o desenvolvemento en Windows (emulador de viño). Entón me dexen a escribir un cliente en JavaScript. Este será só un prototipo, xa que máis tarde souben que o streaming pódese facer moito máis eficiente baseándose en p2p, só para min será peer - server - peers, pero máis sobre iso noutra ocasión, porque aínda non está listo.

Para comezar, necesitamos o servidor websockets real. Fixen o máis sinxelo baseado no paquete melody go:

Código do servidor

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

No cliente (lado da transmisión), primeiro debes acceder á cámara. Isto faise a través API MediaStream.

Conseguimos acceso (permiso) á cámara/micrófono a través API de dispositivos multimedia. Esta API proporciona un método MediaDevices.getUserMedia(), que mostra unha ventá emerxente. unha xanela na que se solicita permiso ao usuario para acceder á cámara e/ou ao micrófono. Gustaríame sinalar que fixen todos os experimentos en Google Chrome, pero creo que todo funcionará igual en Firefox.

A continuación, getUserMedia() devolve unha Promesa, á que pasa un obxecto MediaStream: un fluxo de datos de vídeo e audio. Asignamos este obxecto á propiedade src do elemento video. Código:

Lado de emisión

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

Para transmitir un fluxo de vídeo a través de sockets, cómpre codificalo nalgún lugar, almacenalo e transmitilo por partes. O fluxo de vídeo en bruto non se pode transmitir a través de websockets. Aquí é onde chega a nosa axuda API MediaRecorder. Esta API permítelle codificar e dividir o fluxo en anacos. Fago codificación para comprimir o fluxo de vídeo para enviar menos bytes pola rede. Despois de rompelo en anacos, pode enviar cada peza a un websocket. Código:

Codificamos o fluxo de vídeo, dividilo en partes

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

Agora imos engadir transmisión a través de websockets. Sorprendentemente, todo o que necesitas para iso é un obxecto WebSockets. Só ten dous métodos para enviar e pechar. Os nomes falan por si sós. Código engadido:

Transmitimos o fluxo de vídeo ao servidor

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

O lado da emisión está listo! Agora imos tentar recibir un fluxo de vídeo e mostralo no cliente. Que necesitamos para isto? En primeiro lugar, por suposto, a conexión do socket. Anexamos un "escuitador" ao obxecto WebSocket e subscribimos ao evento "mensaxe". Despois de recibir un anaco de datos binarios, o noso servidor emíteo aos subscritores, é dicir, aos clientes. Neste caso, a función de devolución de chamada asociada ao "oínte" do evento "mensaxe" desenvólvese no cliente; o propio obxecto pásase ao argumento da función: unha parte do fluxo de vídeo codificado por vp8.

Aceptamos transmisión de vídeo

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

Durante moito tempo tentei entender por que é imposible enviar inmediatamente as pezas recibidas ao elemento de vídeo para a súa reprodución, pero resultou que isto non se pode facer, por suposto, primeiro debes poñer a peza nun búfer especial vinculado a o elemento de vídeo e só entón comezará a reproducir o fluxo de vídeo. Para iso necesitarás API MediaSource и FileReader API.

MediaSource actúa como unha especie de intermediario entre o obxecto de reprodución multimedia e a fonte deste fluxo multimedia. O obxecto MediaSource contén un búfer conectable para a fonte do fluxo de vídeo/audio. Unha característica é que o búfer só pode conter datos Uint8, polo que necesitarás un FileReader para crear tal búfer. Mira o código e quedará máis claro:

Reproducir o fluxo de vídeo

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

O prototipo do servizo de transmisión está listo. A principal desvantaxe é que a reprodución de vídeo quedará 100 ms por detrás do lado de transmisión; establecimos isto nós mesmos ao dividir o fluxo de vídeo antes de transmitilo ao servidor. Ademais, cando comprobei no meu portátil, o desfase entre os lados transmisor e receptor acumulouse gradualmente, isto era claramente visible. Comecei a buscar formas de superar esta desvantaxe, e... atopeime API de RTCPeerConnection, que permite transmitir un fluxo de vídeo sen trucos como dividir o fluxo en anacos. O atraso acumulado, creo, débese ao feito de que o navegador volve codificar cada peza no formato webm antes da transmisión. Non cavei máis, pero comecei a estudar WebRTC. Creo que escribirei un artigo aparte sobre os resultados da miña investigación se me parece interesante para a comunidade.

Fonte: www.habr.com

Engadir un comentario