Transmisión de cámara web (casi) inútil desde un navegador. Transmisión de medios y Websockets

En este artículo quiero compartir mis intentos de transmitir vídeo a través de websockets sin utilizar complementos de navegador de terceros, como Adobe Flash Player. Siga leyendo para descubrir qué resultó de esto.

Adobe Flash, anteriormente Macromedia Flash, es una plataforma para crear aplicaciones que se ejecutan en un navegador web. Antes de la introducción de la API Media Stream, era prácticamente la única plataforma para transmitir vídeo y voz desde una cámara web, así como para crear varios tipos de conferencias y chats en el navegador. El protocolo para transmitir información multimedia RTMP (Protocolo de mensajería en tiempo real) estuvo cerrado durante mucho tiempo, lo que significa que si desea mejorar su servicio de transmisión, tenga la amabilidad de utilizar el software de Adobe: Adobe Media Server (AMS).

Después de algún tiempo en 2012, Adobe “se rindió y lo escupió” al público. especificación Protocolo RTMP, que contenía errores y estaba esencialmente incompleto. En ese momento, los desarrolladores comenzaron a realizar sus propias implementaciones de este protocolo y apareció el servidor Wowza. En 2011, Adobe presentó una demanda contra Wowza por uso ilegal de patentes relacionadas con RTMP; después de 4 años, el conflicto se resolvió de manera amistosa.

La plataforma Adobe Flash tiene más de 20 años, tiempo durante el cual se han descubierto muchas vulnerabilidades críticas, soporte prometido finalizará en 2020, dejando pocas alternativas para el servicio de streaming.

Para mi proyecto, inmediatamente decidí abandonar por completo el uso de Flash en el navegador. Indiqué la razón principal anteriormente; Flash tampoco es compatible en absoluto con plataformas móviles y realmente no quería implementar Adobe Flash para el desarrollo en Windows (emulador de Wine). Entonces me propuse escribir un cliente en JavaScript. Esto será solo un prototipo, ya que luego supe que el streaming se puede hacer mucho más eficientemente basado en p2p, solo que para mí será peer - server - peers, pero hablaremos de eso en otro momento, porque aún no está listo.

Para comenzar, necesitamos el servidor websockets real. Hice el más simple basado en el paquete Melody Go:

código de 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)
}

En el cliente (lado de transmisión), primero debe acceder a la cámara. Esto se hace a través de API de transmisión de medios.

Obtenemos acceso (permiso) a la cámara/micrófono a través de API de dispositivos multimedia. Esta API proporciona un método Dispositivos multimedia.getUserMedia(), que muestra una ventana emergente. una ventana que solicita permiso al usuario para acceder a la cámara y/o micrófono. Me gustaría señalar que realicé todos los experimentos en Google Chrome, pero creo que todo funcionará igual en Firefox.

A continuación, getUserMedia() devuelve una Promesa, a la que le pasa un objeto MediaStream: un flujo de datos de vídeo y audio. Asignamos este objeto a la propiedad src del elemento de video. Código:

Lado de radiodifusió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 una transmisión de video a través de sockets, debe codificarla en algún lugar, almacenarla en un buffer y transmitirla en partes. La transmisión de video sin procesar no se puede transmitir a través de websockets. Aquí es donde viene en nuestra ayuda. API MediaRecorder. Esta API le permite codificar y dividir la transmisión en pedazos. Codifico para comprimir la transmisión de video y enviar menos bytes a través de la red. Una vez dividido en pedazos, puede enviar cada pieza a un websocket. Código:

Codificamos la transmisión de video, la dividimos en pedazos.

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

Ahora agreguemos la transmisión a través de websockets. Sorprendentemente, todo lo que necesitas para esto es un objeto. WebSocket. Tiene sólo dos métodos enviar y cerrar. Los nombres hablan por sí solos. Código añadido:

Transmitimos la transmisión de video al 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>

¡La parte de transmisión está lista! Ahora intentemos recibir una transmisión de video y mostrarla en el cliente. ¿Qué necesitamos para esto? En primer lugar, por supuesto, la conexión del enchufe. Adjuntamos un "escucha" al objeto WebSocket y nos suscribimos al evento 'mensaje'. Habiendo recibido un dato binario, nuestro servidor lo transmite a los suscriptores, es decir, a los clientes. En este caso, la función de devolución de llamada asociada con el "escucha" del evento 'mensaje' se activa en el cliente; el objeto en sí se pasa al argumento de la función, una parte de la transmisión de video codificada por vp8.

Aceptamos transmisión de video.

<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 mucho tiempo traté de entender por qué es imposible enviar inmediatamente las piezas recibidas al elemento de video para su reproducción, pero resultó que esto no se puede hacer, por supuesto, primero debes colocar la pieza en un búfer especial vinculado a el elemento de vídeo, y sólo entonces comenzará a reproducir la secuencia de vídeo. Para esto necesitarás API de fuente de medios и API FileReader.

MediaSource actúa como una especie de intermediario entre el objeto de reproducción multimedia y la fuente de este flujo multimedia. El objeto MediaSource contiene un búfer conectable para la fuente de la transmisión de video/audio. Una característica es que el búfer solo puede contener datos Uint8, por lo que necesitará un FileReader para crear dicho búfer. Mire el código y quedará más claro:

Reproduciendo la transmisión de video

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

El prototipo del servicio de streaming está listo. La principal desventaja es que la reproducción de video se retrasará con respecto al lado de transmisión en 100 ms; esto lo configuramos nosotros mismos al dividir la transmisión de video antes de transmitirla al servidor. Además, cuando revisé en mi computadora portátil, el retraso entre los lados transmisor y receptor se acumuló gradualmente, esto era claramente visible. Empecé a buscar formas de superar esta desventaja y... encontré API de conexión RTCPeer, que le permite transmitir una transmisión de video sin trucos como dividir la transmisión en partes. Creo que el retraso acumulado se debe al hecho de que el navegador vuelve a codificar cada pieza en el formato webm antes de la transmisión. No investigué más, pero comencé a estudiar WebRTC. Creo que escribiré un artículo aparte sobre los resultados de mi investigación si lo encuentro interesante para la comunidad.

Fuente: habr.com

Añadir un comentario