El mejor enfoque para la transmisión http en tiempo real al cliente de video HTML5

213

Estoy realmente atrapado tratando de entender la mejor manera de transmitir la salida en tiempo real de ffmpeg a un cliente HTML5 usando node.js, ya que hay una serie de variables en juego y no tengo mucha experiencia en este espacio, Después de pasar muchas horas probando diferentes combinaciones.

Mi caso de uso es:

1) FFMPEG recoge el flujo RTSP H.264 de la cámara de video IP y lo remuxa a un contenedor mp4 usando la siguiente configuración de FFMPEG en el nodo, salida a STDOUT. Esto solo se ejecuta en la conexión inicial del cliente, de modo que las solicitudes de contenido parcial no intenten generar FFMPEG nuevamente.

liveFFMPEG = child_process.spawn("ffmpeg", [
                "-i", "rtsp://admin:[email protected]:554" , "-vcodec", "copy", "-f",
                "mp4", "-reset_timestamps", "1", "-movflags", "frag_keyframe+empty_moov", 
                "-"   // output to stdout
                ],  {detached: false});

2) Uso el servidor http del nodo para capturar el STDOUT y transmitirlo al cliente cuando lo solicite el cliente. Cuando el cliente se conecta por primera vez, engendro la línea de comando FFMPEG anterior y luego canalizo el flujo STDOUT a la respuesta HTTP.

liveFFMPEG.stdout.pipe(resp);

También he usado el evento stream para escribir los datos FFMPEG en la respuesta HTTP, pero no hay diferencia

xliveFFMPEG.stdout.on("data",function(data) {
        resp.write(data);
}

Uso el siguiente encabezado HTTP (que también se usa y funciona al transmitir archivos pregrabados)

var total = 999999999         // fake a large file
var partialstart = 0
var partialend = total - 1

if (range !== undefined) {
    var parts = range.replace(/bytes=/, "").split("-"); 
    var partialstart = parts[0]; 
    var partialend = parts[1];
} 

var start = parseInt(partialstart, 10); 
var end = partialend ? parseInt(partialend, 10) : total;   // fake a large file if no range reques 

var chunksize = (end-start)+1; 

resp.writeHead(206, {
                  'Transfer-Encoding': 'chunked'
                 , 'Content-Type': 'video/mp4'
                 , 'Content-Length': chunksize // large size to fake a file
                 , 'Accept-Ranges': 'bytes ' + start + "-" + end + "/" + total
});

3) El cliente tiene que usar etiquetas de video HTML5.

No tengo problemas con la reproducción de transmisión (usando fs.createReadStream con 206 contenido parcial HTTP) al cliente HTML5 un archivo de video previamente grabado con la línea de comando FFMPEG anterior (pero guardado en un archivo en lugar de STDOUT), así que sé la transmisión FFMPEG es correcto, e incluso puedo ver correctamente la transmisión de video en vivo en VLC cuando me conecto al servidor de nodo HTTP.

Sin embargo, tratar de transmitir en vivo desde FFMPEG a través del nodo HTTP parece ser mucho más difícil ya que el cliente mostrará un cuadro y luego se detendrá. Sospecho que el problema es que no estoy configurando la conexión HTTP para que sea compatible con el cliente de video HTML5. He intentado una variedad de cosas como usar HTTP 206 (contenido parcial) y 200 respuestas, poner los datos en un búfer y luego transmitir sin suerte, por lo que necesito volver a los primeros principios para asegurarme de que estoy configurando esto correctamente camino.

Aquí entiendo cómo debería funcionar esto, corríjame si me equivoco:

1) FFMPEG debe configurarse para fragmentar la salida y utilizar un moov vacío (FFMPEG frag_keyframe y empty_moov mov flags). Esto significa que el cliente no usa el átomo de moov, que generalmente se encuentra al final del archivo, que no es relevante cuando se transmite (sin final de archivo), pero significa que no es posible buscar, lo cual está bien para mi caso de uso.

2) Aunque uso fragmentos MP4 y MOOV vacío, todavía tengo que usar contenido parcial HTTP, ya que el reproductor HTML5 esperará hasta que se descargue toda la transmisión antes de reproducirla, lo que con una transmisión en vivo nunca termina, por lo que es inviable.

3) No entiendo por qué la canalización de la transmisión STDOUT a la respuesta HTTP no funciona cuando se transmite en vivo, pero si guardo en un archivo, puedo transmitir este archivo fácilmente a clientes HTML5 usando un código similar. Tal vez sea un problema de tiempo, ya que la generación de FFMPEG tarda un segundo en iniciarse, conectarse a la cámara IP y enviar fragmentos al nodo, y los eventos de datos del nodo también son irregulares. Sin embargo, bytestream debería ser exactamente lo mismo que guardar en un archivo, y HTTP debería poder atender las demoras.

4) Al verificar el registro de red desde el cliente HTTP al transmitir un archivo MP4 creado por FFMPEG desde la cámara, veo que hay 3 solicitudes de cliente: una solicitud GET general para el video, que el servidor HTTP devuelve aproximadamente 40Kb, luego un parcial solicitud de contenido con un rango de bytes para los últimos 10K del archivo, luego una solicitud final para los bits en el medio no cargados. ¿Quizás el cliente HTML5 una vez que recibe la primera respuesta está pidiendo la última parte del archivo para cargar el átomo MP4 MOOV? Si este es el caso, no funcionará para la transmisión ya que no hay un archivo MOOV ni un final del archivo.

5) Cuando reviso el registro de la red cuando intento transmitir en vivo, recibo una solicitud inicial cancelada con solo unos 200 bytes recibidos, luego una nueva solicitud abortada nuevamente con 200 bytes y una tercera solicitud que solo tiene 2K de longitud. No entiendo por qué el cliente HTML5 abortaría la solicitud, ya que bytestream es exactamente lo mismo que puedo usar con éxito cuando se transmite desde un archivo grabado. También parece que el nodo no está enviando el resto de la secuencia FFMPEG al cliente, pero puedo ver los datos FFMPEG en la rutina del evento .on, por lo que está llegando al servidor HTTP del nodo FFMPEG.

6) Aunque creo que la conexión de la secuencia STDOUT al búfer de respuesta HTTP debería funcionar, ¿tengo que crear un búfer intermedio y una secuencia que permita que las solicitudes de cliente de contenido parcial HTTP funcionen correctamente como lo hace cuando lee (con éxito) un archivo ? Creo que esta es la razón principal de mis problemas, sin embargo, no estoy exactamente seguro en Node sobre cómo configurarlo mejor. Y no sé cómo manejar una solicitud de cliente para los datos al final del archivo, ya que no hay un final de archivo.

7) ¿Estoy en el camino equivocado al tratar de manejar 206 solicitudes de contenido parcial, y esto debería funcionar con 200 respuestas HTTP normales? Las respuestas HTTP 200 funcionan bien para VLC, ¿así que sospecho que el cliente de video HTML5 solo funcionará con solicitudes de contenido parcial?

Como todavía estoy aprendiendo estas cosas, es difícil trabajar a través de las diversas capas de este problema (FFMPEG, nodo, transmisión, HTTP, video HTML5), por lo que cualquier puntero será muy apreciado. He pasado horas investigando en este sitio y en la red, y no he encontrado a nadie que haya podido hacer streaming en tiempo real en el nodo, pero no puedo ser el primero, y creo que esto debería funcionar (de alguna manera !).

deandob
fuente
44
Este es un tema complicado. Lo primero es lo primero. ¿Pusiste tu Content-Typeen tu cabeza? ¿Estás usando codificación de fragmentos? Ahí es donde comenzaría. Además, HTML5 no necesariamente proporciona la funcionalidad para transmitir, puede leer más sobre eso aquí . Lo más probable es que necesite implementar una forma de almacenar en búfer y reproducir la transmisión de video utilizando sus propios medios ( ver aquí ), aunque es probable que esto no sea compatible. También google en MediaSource API.
tsturzl
Gracias por la respuesta. Sí, el tipo de contenido es 'video / mp4' y este código funciona para la transmisión de archivos de video. Lamentablemente, MediaSource es solo Chrome, tengo que admitir otros navegadores. ¿Hay alguna especificación sobre cómo interactúa el cliente de video HTML5 con un servidor de transmisión HTTP? Estoy seguro de lo que quiero hacer, pero no estoy seguro exactamente cómo (con node.js pero podría usar C # o C ++ si es más fácil)
deandob
2
El problema no está en tu backend. Estás transmitiendo video muy bien. El problema está en su interfaz / cliente, debe implementar la transmisión usted mismo. HTML5 simplemente no maneja transmisiones. Tendrá que explorar las opciones por navegador más probablemente. La lectura de los estándares w3 para la etiqueta de video y las API de medios sería un buen lugar para comenzar.
tsturzl
Parece que debería ser posible hacer que esto funcione. No estoy ofreciendo una respuesta definitiva, pero sospecho que este problema se relaciona con el hecho de que el navegador espera el resto del encabezado / átomos del contenedor mp4 al principio y no el siguiente cuadro en la transmisión de video. Si envía un átomo MOOV para un video muy largo (para que el reproductor siga solicitando), así como los otros encabezados esperados y luego comience a copiar desde ffmpeg, esto podría funcionar. También debería ocultar la barra de fregado con js en el navegador para que no puedan avanzar.
jwriteclub
Sugeriría considerar WebRTC, que está obteniendo una mejor compatibilidad entre navegadores día a día.
Alex Cohn

Respuestas:

209

EDITAR 3: a partir de iOS 10, HLS admitirá archivos mp4 fragmentados. La respuesta ahora es crear activos mp4 fragmentados, con un manifiesto DASH y HLS. > Pretender flash, iOS9 y versiones posteriores e IE 10 y versiones posteriores no existen.

Todo debajo de esta línea está desactualizado. Manteniéndolo aquí para la posteridad.


EDITAR 2: Como señalan las personas en los comentarios, las cosas cambian. Casi todos los navegadores admitirán códecs AVC / AAC. iOS todavía requiere HLS. Pero a través de adaptadores como hls.js puedes jugar HLS en MSE. La nueva respuesta es HLS + hls.js si necesita iOS. o simplemente MP4 fragmentado (es decir, DASH) si no

Hay muchas razones por las que el video y, específicamente, el video en vivo es muy difícil. (Tenga en cuenta que la pregunta original especificaba que el video HTML5 es un requisito, pero el autor de la pregunta declaró que Flash es posible en los comentarios. Entonces, de inmediato, esta pregunta es engañosa)

Primero voy a repetir: NO HAY APOYO OFICIAL PARA LA TRANSMISIÓN EN VIVO SOBRE HTML5 . Hay hacks, pero su kilometraje puede variar.

EDITAR: desde que escribí esta respuesta Las extensiones de fuente de medios han madurado y ahora están muy cerca de convertirse en una opción viable. Son compatibles con la mayoría de los principales navegadores. IOS sigue siendo una resistencia.

A continuación, debe comprender que Video on demand (VOD) y video en vivo son muy diferentes. Sí, ambos son videos, pero los problemas son diferentes, por lo tanto, los formatos son diferentes. Por ejemplo, si el reloj de su computadora funciona un 1% más rápido de lo que debería, no lo notará en un VOD. Con el video en vivo, intentará reproducir el video antes de que suceda. Si desea unirse a una transmisión de video en vivo en progreso, necesita los datos necesarios para inicializar el decodificador, por lo que debe repetirse en la transmisión o enviarse fuera de banda. Con VOD, puede leer el comienzo del archivo que buscan hasta el punto que desee.

Ahora profundicemos un poco.

Plataformas:

  • iOS
  • ordenador personal
  • Mac
  • Androide

Códecs:

  • vp8 / 9
  • h.264
  • tora (vp3)

Métodos comunes de entrega de video en vivo en navegadores:

  • DASH (HTTP)
  • HLS (HTTP)
  • flash (RTMP)
  • flash (HDS)

Métodos de entrega comunes para VOD en navegadores:

  • DASH (transmisión HTTP)
  • HLS (transmisión HTTP)
  • flash (RTMP)
  • flash (transmisión HTTP)
  • MP4 (pseudo streaming HTTP)
  • No voy a hablar sobre MKV y OOG porque no los conozco muy bien.

etiqueta de video html5:

  • MP4
  • webm
  • ogg

Veamos qué navegadores admiten qué formatos

Safari:

  • HLS (solo iOS y mac)
  • h.264
  • MP4

Firefox

  • DASH (a través de MSE pero no h.264)
  • h.264 solo a través de Flash!
  • VP9
  • MP4
  • OGG
  • Webm

ES DECIR

  • Destello
  • DASH (solo a través de MSE IE 11+)
  • h.264
  • MP4

Cromo

  • Destello
  • DASH (a través de MSE)
  • h.264
  • VP9
  • MP4
  • webm
  • ogg

MP4 no se puede usar para video en vivo (NOTA: DASH es un superconjunto de MP4, así que no se confunda con eso). MP4 se divide en dos partes: moov y mdat. mdat contiene los datos de audio y video sin procesar. Pero no está indexado, por lo que sin el moov, es inútil. El moov contiene un índice de todos los datos en el mdat. Pero debido a su formato, no se puede 'aplanar' hasta que se conozcan las marcas de tiempo y el tamaño de CADA cuadro. Puede ser posible construir un moov que 'engorde' el tamaño de los cuadros, pero es muy costoso en cuanto a ancho de banda.

Entonces, si desea realizar entregas en todas partes, necesitamos encontrar el mínimo común denominador. Verá que no hay LCD aquí sin recurrir al ejemplo de flash:

  • iOS solo admite video h.264. y solo admite HLS de por vida.
  • Firefox no es compatible con h.264 en absoluto, a menos que use flash
  • Flash no funciona en iOS

Lo más parecido a una pantalla LCD es usar HLS para obtener sus usuarios de iOS y flashear para todos los demás. Mi favorito personal es codificar HLS, luego usar flash para reproducir HLS para todos los demás. Puedes jugar HLS en flash a través del reproductor JW 6 (o escribir tu propio HLS en FLV en AS3 como lo hice yo)

Pronto, la forma más común de hacer esto será HLS en iOS / Mac y DASH a través de MSE en cualquier otro lugar (esto es lo que Netflix hará pronto). Pero todavía estamos esperando que todos actualicen sus navegadores. También es probable que necesite un DASH / VP9 separado para Firefox (sé acerca de open264; apesta. No puede hacer videos en el perfil principal o alto. Por lo tanto, actualmente es inútil).

szatmary
fuente
Gracias szatmary por los antecedentes detallados y pro / contras sobre las diversas opciones. He seleccionado esta respuesta como la aceptada, ya que el esquema de los conceptos es más importante que la solución específica que encontré para responder a la pregunta original. Buena suerte con la recompensa!
deandob
99
Esta no es una solución funcional a esta pregunta. Hay una solución funcional a este problema a continuación.
jwriteclub
2
Firefox ahora es compatible con MSE y h.264 de forma nativa. Vaya a www.youtube.com/html5 con el último navegador FF para confirmar. Probé con FF 37. Safari 8+ en Mac ahora también es compatible con MSE.
BigTundra
@BigTundra sí, Safari es compatible con MSE desde el lanzamiento de Yosemite en Mac. Pero no iOS. No estoy seguro acerca de Windows. (¿Sigue siendo un safari en Windows?) Firefox 37.0.2 en (mi) Mac no parece admitir MSE en absoluto de acuerdo con ese enlace. Pero es compatible con H.264. Firefox ha agregado y eliminado y vuelto a agregar el soporte H.264 en el pasado.
szatmary
El navegador actualizado admite el formato de video MPEG-4 / H.264: caniuse.com/#feat=mpeg4
Maxence
75

Gracias a todos, especialmente a Szatmary, ya que esta es una pregunta compleja y tiene muchas capas, todas las cuales deben estar funcionando antes de poder transmitir video en vivo. Para aclarar mi pregunta original y el uso de video HTML5 vs flash: mi caso de uso tiene una fuerte preferencia por HTML5 porque es genérico, fácil de implementar en el cliente y en el futuro. Flash es un segundo mejor distante, así que sigamos con HTML5 para esta pregunta.

Aprendí mucho a través de este ejercicio y estoy de acuerdo en que la transmisión en vivo es mucho más difícil que VOD (que funciona bien con video HTML5). Pero logré que esto funcione satisfactoriamente para mi caso de uso y la solución resultó ser muy simple, después de perseguir opciones más complejas como MSE, flash, esquemas de almacenamiento intermedio elaborados en Node. El problema era que FFMPEG estaba corrompiendo el MP4 fragmentado y tuve que ajustar los parámetros de FFMPEG, y la redirección de la tubería de flujo de nodo estándar sobre http que usé originalmente era todo lo que se necesitaba.

En MP4 hay una opción de 'fragmentación' que divide el mp4 en fragmentos mucho más pequeños que tiene su propio índice y hace viable la opción de transmisión en vivo mp4. Pero no es posible buscar nuevamente en la secuencia (OK para mi caso de uso), y las versiones posteriores de FFMPEG admiten la fragmentación.

Tenga en cuenta que el tiempo puede ser un problema, y ​​con mi solución tengo un retraso de entre 2 y 6 segundos causado por una combinación de remuxing (efectivamente, FFMPEG tiene que recibir la transmisión en vivo, remuxarla y luego enviarla al nodo para servir a través de HTTP) . No se puede hacer mucho al respecto, sin embargo, en Chrome el video intenta ponerse al día tanto como sea posible, lo que hace que el video sea un poco nervioso pero más actual que IE11 (mi cliente preferido).

En lugar de explicar cómo funciona el código en esta publicación, consulte el GIST con comentarios (el código del cliente no está incluido, es una etiqueta de video HTML5 estándar con la dirección del servidor http del nodo). GIST está aquí: https://gist.github.com/deandob/9240090

No he podido encontrar ejemplos similares de este caso de uso, así que espero que la explicación y el código anteriores ayuden a otros, especialmente porque he aprendido mucho de este sitio y todavía me considero un principiante.

Aunque esta es la respuesta a mi pregunta específica, he seleccionado la respuesta de szatmary como la aceptada, ya que es la más completa.

deandob
fuente
33
Lo siento, pero encontré esto por mi cuenta, la redacción de mi respuesta lo deja bastante claro. Las respuestas anteriores fueron útiles y apreciadas, pero no contribuyeron significativamente, e incluso he enviado el código de trabajo en el GIST y nadie más lo ha hecho. No estoy interesado en la 'reputación', estoy interesado en aprender si mi enfoque y mi código pueden mejorarse. Y la respuesta que marqué resolvió mi problema, así que estoy confundido sobre cuál es el problema aquí. Soy bastante nuevo en SO, así que estoy feliz de que me digan que interactúe de una manera diferente, encuentro este sitio útil y mi respuesta debería ayudar a otros.
deandob
2
Parece que no es lo correcto en esta comunidad seleccionar su respuesta como la respuesta aceptada si hizo la pregunta, incluso si soluciona el problema original. Aunque eso parece contrario a la intuición, la documentación de los conceptos es más importante que la solución real, con lo que estoy de acuerdo, ya que ayuda a otros a aprender. Des-seleccioné mi respuesta y seleccioné szatmary como la más articulada en torno a los conceptos.
deandob
66
@deandob: publiqué una recompensa por una solución funcional a este problema que usted proporcionó con éxito. La respuesta aceptada afirma que no existe una solución de trabajo y, por lo tanto, es claramente inexacta.
jwriteclub
2
Gracias. Parece que otros han rechazado mi respuesta original como incorrecta y, como soy nueva, asumí que así es como funcionan las cosas por aquí. No quiero causar ningún escándalo, pero lo comprobaré con la gente sobre el desbordamiento de meta stack. Por cierto, mi solución funciona muy bien y debería ser viable para otros, y hay una variación en la solución publicada que puede reducir el retraso inicial (el buffer en node.js inicialmente busca el final de la secuencia en el extremo del cliente) .
deandob
44
Un moderador me aclaró que mi enfoque original de responder la pregunta yo mismo y seleccionarlo como la respuesta era el enfoque correcto. Para obtener más información (o si desea debatir esto más a fondo) vea el hilo en el meta sitio. meta.stackexchange.com/questions/224068/…
deandob
14

Echa un vistazo al proyecto JSMPEG . Hay una gran idea implementada allí: decodificar MPEG en el navegador usando JavaScript. Los bytes del codificador (FFMPEG, por ejemplo) se pueden transferir al navegador mediante WebSockets o Flash, por ejemplo. Creo que si la comunidad se pone al día, será la mejor solución de transmisión de video en vivo HTML5 por ahora.

Michael Romanenko
fuente
10
Es un decodificador de video MPEG-1. No estoy seguro de que comprenda cuán antiguo es MPEG-1; Es más antiguo que los DVD. Es un poco más avanzado que un archivo GIF.
Camilo Martin
13

Escribí un reproductor de video HTML5 alrededor del códec broadway h264 (emscripten) que puede reproducir video h264 en vivo (sin demora) en todos los navegadores (escritorio, iOS, ...).

La transmisión de video se envía a través de WebSocket al cliente, se decodifica cuadro por cuadro y se muestra en una canva (usando webgl para la aceleración)

Echa un vistazo a https://github.com/131/h264-live-player en github.

131
fuente
1
github.com/Streamedian/html5_rtsp_player Estos chicos hicieron algo similar que usa rtp h264 sobre websocket
Victor.dMdB
12

Una forma de transmitir en vivo una cámara web basada en RTSP a un cliente HTML5 (implica volver a codificar, así que espere una pérdida de calidad y necesite algo de potencia de CPU):

  • Configure un servidor Icecast (podría estar en la misma máquina en la que está su servidor web o en la máquina que recibe el flujo RTSP de la cámara)
  • En la máquina que recibe la transmisión de la cámara, no use FFMPEG sino gstreamer. Es capaz de recibir y decodificar el flujo RTSP, volver a codificarlo y transmitirlo al servidor Icecast. Canalización de ejemplo (solo video, sin audio):

    gst-launch-1.0 rtspsrc location=rtsp://192.168.1.234:554 user-id=admin user-pw=123456 ! rtph264depay ! avdec_h264 ! vp8enc threads=2 deadline=10000 ! webmmux streamable=true ! shout2send password=pass ip=<IP_OF_ICECAST_SERVER> port=12000 mount=cam.webm

=> Luego puede usar la etiqueta <video> con la URL de icecast-stream ( http://127.0.0.1:12000/cam.webm ) y funcionará en todos los navegadores y dispositivos que admitan webm

Jannis
fuente
3

Echa un vistazo a esta solución . Como sé, Flashphoner permite reproducir audio en vivo + transmisión de video en la página HTML5 pura.

Utilizan códecs MPEG1 y G.711 para la reproducción. El truco es renderizar video decodificado a un elemento de lienzo HTML5 y reproducir audio decodificado a través del contexto de audio HTML5.

ankitr
fuente
2

Esto es un malentendido muy común. No hay soporte de video HTML5 en vivo (excepto HLS en iOS y Mac Safari). Es posible que pueda 'hackearlo' utilizando un contenedor webm, pero no esperaría que eso sea universalmente compatible. Lo que está buscando se incluye en las Extensiones de origen de medios, donde puede alimentar los fragmentos al navegador de uno en uno. pero deberá escribir algunos javascript del lado del cliente.

szatmary
fuente
Hay solutionspero no hay supportpara la transmisión en vivo. Esto se refiere directamente a mi comentario visto anteriormente. Y webm es compatible con los principales navegadores, principalmente la última versión estable.
tsturzl
1
Realmente preferiría no transcodificar desde H.264 a webm y no debería ser necesario. Además, como tengo que admitir IE11 y Safari, las extensiones MediaSource no ayudarán. Pero creo que si simulo una secuencia de archivos en el lado del servidor (¡lo cual funciona!) Entonces debería funcionar, pero tendré que simular un búfer de archivos en node.js.
deandob
1
Como otro sugirió, buscaría la posibilidad de usar WebRTC que es nativo a diferencia de VLC o plugin flash. Sé que esta tecnología aún es difícil de implementar. Buena suerte.
1
Obtuve esto para trabajar actualizando a la última versión de FFMPEG, ya que parece que hubo corrupción en el mp4 al usar el modo fragmentado (necesario para la transmisión en vivo de MP4, por lo que el cliente no está esperando el archivo de índice moov que nunca vendrá cuando esté en vivo transmisión). Y mi código node.js para redirigir la transmisión FFMPEG directamente al navegador ahora funciona.
deandob
1
Sí, funciona bien en IE11 (mi navegador preferido). Recibo una respuesta nerviosa en Chrome.
deandob
2

Prueba binaryjs. Es como socket.io, pero lo único que hace bien es que transmite audio y video. Binaryjs google it

Siddharth
fuente
1
Binary.JS no es como Socket.IO. Y no es específico para la transmisión de medios.
Brad