En este artículo, quiero compartir mis intentos de transmitir video a través de websockets sin usar complementos de navegador de terceros como Adobe Flash Player. Lo que salió de esto sigue leyendo.
Adobe Flash, anteriormente Macromedia Flash, es una plataforma para crear aplicaciones que se ejecutan en un navegador web. Antes de la implementación de Media Stream API, era prácticamente la única plataforma para transmitir video y voz desde una cámara web, así como para crear varias conferencias y chats en un navegador. El protocolo para transmitir información de medios RTMP (Protocolo de mensajería en tiempo real) en realidad estuvo cerrado durante mucho tiempo, lo que significaba: si desea aumentar su servicio de transmisión, utilice el software de Adobe - Adobe Media Server (AMS).
Después de algún tiempo en 2012, Adobe "se rindió y escupió" la
especificación del protocolo RTMP, que contenía errores y, de hecho, no estaba completa. En ese momento, los desarrolladores comenzaron a hacer sus implementaciones de este protocolo, por lo que apareció el servidor Wowza. En 2011, Adobe presentó una demanda contra Wowza por el uso ilegal de patentes relacionadas con RTMP, después de 4 años el conflicto fue resuelto por el mundo.
La plataforma Adobe Flash ha existido durante más de 20 años, durante este tiempo se descubrieron muchas vulnerabilidades críticas,
prometieron dejar de admitir para 2020, por lo que no hay tantas alternativas para el servicio de transmisión.
Para mi proyecto, inmediatamente decidí abandonar por completo el uso de Flash en el navegador. La razón principal que indiqué anteriormente, Flash no es compatible en absoluto en plataformas móviles, y realmente no quería implementar Adobe Flash para el desarrollo en Windows (Wine Emulator). Entonces comencé a escribir un cliente en JavaScript. Esto será solo un prototipo, ya que más tarde aprendí que la transmisión se puede hacer de manera mucho más eficiente en función de p2p, solo que tendré pares - servidor - pares, pero más sobre eso en otro momento, porque aún no está listo.
Para comenzar, necesitamos el servidor websockets. Hice el más simple basado en el paquete melody go:
Código de servidorpackage 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) })
En el cliente (lado de transmisión), primero debe acceder a la cámara. Esto se hace a través de la
API MediaStream .
Tenemos acceso (resolución) a la cámara / micrófono a través de la
API de dispositivos multimedia . Esta API proporciona el método
MediaDevices.getUserMedia () , que muestra una ventana emergente. una ventana que le pide permiso al usuario para acceder a la cámara y / o al micrófono. Me gustaría señalar que realicé todos los experimentos en Google Chrome, pero creo que en Firefox todo funcionará aproximadamente de la misma manera.
A continuación, getUserMedia () devuelve una Promesa, en la que se pasa un objeto MediaStream: una secuencia de datos de video y audio. Asignamos este objeto a la propiedad del elemento video en src. Código:
Lado de la transmisió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'); </script>
Para transmitir una transmisión de video a través de sockets, debe codificarla de alguna manera en algún lugar, almacenarla en búfer y transmitirla en partes. La transmisión de video sin procesar no se puede transmitir a través de websockets. Aquí es
donde la API MediaRecorder viene al
rescate . Esta API le permite codificar y dividir una secuencia en pedazos. Hago codificación para comprimir la transmisión de video, de modo que pueda conducir menos bytes a través de la red. Habiendo roto en pedazos, es posible enviar cada pieza a websocket. Código:
Codificamos la transmisión de video, la hacemos 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'); </script>
Ahora agregue la transferencia en websockets. Sorprendentemente, esto requiere solo un objeto
WebSocket . Tiene solo dos métodos de envío y cierre. Los nombres hablan por sí mismos. Código extendido:
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'); </script>
¡El lado de la transmisión está listo! Ahora intentemos tomar una transmisión de video y mostrarla en el cliente. ¿Qué necesitamos para esto? En primer lugar, por supuesto, una conexión de socket. Colgamos un "oyente" en el objeto WebSocket, suscribimos al evento 'mensaje'. Después de recibir un dato binario, nuestro servidor lo transmite a los suscriptores, es decir, a los clientes. Al mismo tiempo, la función de devolución de llamada conectada con el "oyente" del evento 'mensaje' se activa en el cliente, el objeto mismo se pasa al argumento de la función, una parte del flujo de video codificado por vp8.
Aceptamos una 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) { </script>
Durante mucho tiempo intenté entender por qué es imposible enviar de inmediato las piezas recibidas al elemento de video para su reproducción, pero resultó ser imposible, por supuesto, primero debe colocar la pieza en un búfer especial conectado al elemento de video, y solo entonces comenzará a reproducir la transmisión de video. Para hacer esto, necesita la
API MediaSource y la
API FileReader .
MediaSource actúa como una especie de intermediario entre el objeto de reproducción de medios y la fuente de este flujo de medios. El objeto MediaSource contiene un búfer conectable para la fuente de transmisión de video / audio. Una característica es que el búfer solo puede contener datos de Uint8, por lo que se requiere FileReader para crear dicho búfer. Eche un vistazo al código y quedará más claro:
Reproduce 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(), </script>
El prototipo del servicio de transmisión está listo. La principal desventaja es que la reproducción de video estará a 100 ms del lado de transmisión, nosotros mismos lo configuramos al dividir la transmisión de video antes de enviarla al servidor. Además, cuando revisé mi computadora portátil, gradualmente acumulé un retraso entre los lados de transmisión y recepción, esto fue claramente visible. Comencé a buscar formas de superar esta deficiencia, y ... encontré la
API RTCPeerConnection , que le permite transferir una transmisión de video sin trucos como dividir una transmisión en pedazos. Creo que el retraso acumulado se debe al hecho de que en el navegador, antes de la transferencia, cada pieza se transcodifica al formato webm. No busqué más, pero comencé a estudiar WebRTC, pienso en los resultados de mi investigación, escribiré un artículo separado si encuentro esta comunidad interesante.