2012-10-07 36 views
6

Quiero transmitir un video a mi IPad a través de la etiqueta de video HTML5 con tapiz5 (5.3.5) en el back-end. Por lo general, el marco del servidor ni siquiera debe desempeñar un papel en esto, pero de alguna manera lo hace.La transmisión de video a ipad no funciona con Tapestry5

De todos modos, con suerte alguien aquí puede ayudarme. Tenga en cuenta que mi proyecto es en gran medida un prototipo y que lo que describo se simplifica/reduce a las partes relevantes. Le agradecería mucho si las personas no respondieran con la obligación de "querer hacer lo incorrecto" o detalles de seguridad/rendimiento que no son relevantes para el problema.

Así que aquí va:

Configuración

Tengo un video tomado de la manzana HTML5 mostrar así que sé que el formato no es un problema. Tengo una página simple de tml "Reproducir" que solo contiene una etiqueta de "video".

Problema

empecé mediante la implementación de un RequestFilter que controla la solicitud de control de vídeo abriendo el archivo de vídeo y streaming de referencia para el cliente. Eso es básico "si la ruta comienza con 'archivo' y luego copia el archivo inputstream a response outputstream". Esto funciona muy bien con Chrome, pero no con el Ipad. Bien, pensé, deben faltar algunos encabezados, así que volví a mirar el Apple Showcase e incluí los mismos encabezados y el mismo tipo de contenido, pero no me gustó.

A continuación, pensé, bueno, veamos qué sucede si dejo que t5 entregue el archivo. Copié el video en el contexto de la aplicación web, deshabilité el filtro de mi solicitud y coloqué el nombre de archivo simple en el atributo src del video. Esto funciona en Chrome AND IPad. Eso me sorprendió y me impulsó a ver cómo T5 maneja los archivos estáticos/solicitud de contexto. Hasta ahora solo he llegado a sentir que hay dos caminos diferentes que he confirmado al cambiar el "cable src" cableado a un Asset con @Path ("contexto:"). Esto, de nuevo, funciona en Chrome pero no en IPad.

Así que realmente estoy perdido aquí. ¿Qué es este jugo secreto en las solicitudes de "contexto simple" que le permiten funcionar en el iPad? No pasa nada especial y, sin embargo, es la única forma en que esto funciona. El problema es que no puedo servir a esos vids de mi contexto webapp ...

Solución

Así, resulta que existe esta cabecera HTTP llamado "Rango" y que el IPad, a diferencia de Chrome utiliza con video La "salsa secreta" es que el manejador de servlets para la solicitud de recursos estáticos sabe cómo tratar las solicitudes de rango, mientras que las de T5 no. Aquí está mi implementación personalizada:

 OutputStream os = response.getOutputStream("video/mp4"); 
     InputStream is = new BufferedInputStream(new FileInputStream(f)); 
     try { 
      String range = request.getHeader("Range"); 
      if(range != null && !range.equals("bytes=0-")) { 
       logger.info("Range response _______________________"); 
       String[] ranges = range.split("=")[1].split("-"); 
       int from = Integer.parseInt(ranges[0]); 
       int to = Integer.parseInt(ranges[1]); 
       int len = to - from + 1 ; 

       response.setStatus(206); 
       response.setHeader("Accept-Ranges", "bytes"); 
       String responseRange = String.format("bytes %d-%d/%d", from, to, f.length()); 
       logger.info("Content-Range:" + responseRange); 
       response.setHeader("Connection", "close"); 
       response.setHeader("Content-Range", responseRange); 
       response.setDateHeader("Last-Modified", new Date().getTime()); 
       response.setContentLength(len); 
       logger.info("length:" + len); 

       byte[] buf = new byte[4096]; 
       is.skip(from); 
       while(len != 0) { 

        int read = is.read(buf, 0, len >= buf.length ? buf.length : len); 
        if(read != -1) { 
         os.write(buf, 0, read); 
         len -= read; 
        } 
       } 


      } else { 
        response.setStatus(200); 
        IOUtils.copy(is, os); 
      } 

     } finally { 
      os.close(); 
      is.close(); 
     } 

Respuesta

7

Quiero publicar mi solución refinada desde arriba. Espero que esto sea útil para alguien.

Así que, básicamente, el problema parecía ser que estaba haciendo caso omiso del encabezado de solicitud http "Rango" que no le gustó al iPad.En pocas palabras, este encabezado significa que el cliente solo quiere una determinada parte (en este caso, un rango de bytes) de la respuesta.

Esto es lo que parece una solicitud de vídeo html iPad como ::

[INFO] RequestLogger Accept:*/* 
[INFO] RequestLogger Accept-Encoding:identity 
[INFO] RequestLogger Connection:keep-alive 
[INFO] RequestLogger Host:mars:8080 
[INFO] RequestLogger If-Modified-Since:Wed, 10 Oct 2012 22:27:38 GMT 
[INFO] RequestLogger Range:bytes=0-1 
[INFO] RequestLogger User-Agent:AppleCoreMedia/1.0.0.9B176 (iPad; U; CPU OS 5_1 like Mac OS X; en_us) 
[INFO] RequestLogger X-Playback-Session-Id:BC3B397D-D57D-411F-B596-931F5AD9879F 

Esto significa que el iPad sólo quiere el primer byte. Si ignora este encabezado y simplemente envía una respuesta 200 con todo el cuerpo, entonces el video no se reproducirá. Por lo tanto, es necesario enviar una respuesta 206 (respuesta parcial) y establezca las siguientes cabeceras de respuesta:

[INFO] RequestLogger Content-Range:bytes 0-1/357772702 
[INFO] RequestLogger Content-Length:2 

Esto significa "yo estoy enviando Byte 0 a 1 de 357.772.702 bytes totales disponibles".

El momento para iniciar la reproducción del vídeo, la siguiente solicitud se verá así (todo excepto la cabecera gama omite,):

[INFO] RequestLogger Range:bytes=0-357772701 

Así que mi solución refinada se parece a esto:

OutputStream os = response.getOutputStream("video/mp4"); 

     try { 
       String range = request.getHeader("Range"); 
       /** if there is no range requested we will just send everything **/ 
       if(range == null) { 
        InputStream is = new BufferedInputStream(new FileInputStream(f)); 
        try { 
         IOUtils.copy(is, os); 
         response.setStatus(200); 
        } finally { 
         is.close(); 
        } 
        return true; 
       } 
       requestLogger.info("Range response _______________________"); 


       String[] ranges = range.split("=")[1].split("-"); 
       int from = Integer.parseInt(ranges[0]); 
       /** 
       * some clients, like chrome will send a range header but won't actually specify the upper bound. 
       * For them we want to send out our large video in chunks. 
       */ 
       int to = HTTP_DEFAULT_CHUNK_SIZE + from; 
       if(to >= f.length()) { 
        to = (int) (f.length() - 1); 
       } 
       if(ranges.length == 2) { 
        to = Integer.parseInt(ranges[1]); 
       } 
       int len = to - from + 1 ; 

       response.setStatus(206); 
       response.setHeader("Accept-Ranges", "bytes"); 
       String responseRange = String.format("bytes %d-%d/%d", from, to, f.length()); 

       response.setHeader("Content-Range", responseRange); 
       response.setDateHeader("Last-Modified", new Date().getTime()); 
       response.setContentLength(len); 

       requestLogger.info("Content-Range:" + responseRange); 
       requestLogger.info("length:" + len); 
       long start = System.currentTimeMillis(); 
       RandomAccessFile raf = new RandomAccessFile(f, "r"); 
       raf.seek(from); 
       byte[] buf = new byte[IO_BUFFER_SIZE]; 
       try { 
        while(len != 0) { 
         int read = raf.read(buf, 0, buf.length > len ? len : buf.length); 
         os.write(buf, 0, read); 
         len -= read; 
        } 
       } finally { 
        raf.close(); 
       } 
       logger.info("r/w took:" + (System.currentTimeMillis() - start)); 




     } finally { 
      os.close(); 

     } 

Esta solución es mejor que la primera, ya que maneja todos los casos de solicitudes de "Rango" que parece ser un prerrequisito para que los clientes como Chrome puedan admitir saltos dentro del video (en ese momento emitirán una solicitud de rango para ese punto en el video).

Todavía no es perfecto. Mejoras adicionales serían establecer correctamente el encabezado "Última modificación" y al hacer un manejo adecuado de los clientes se solicita un rango no válido o un rango de algo más que bytes.

+0

Esta es información útil; no hay ninguna razón por la cual Tapestry no pueda manejar esto automáticamente dentro del código estándar de manejo de activos; simplemente no somos conscientes de que es necesario hacerlo. Agregar este nivel de información a nuestra JIRA es el primer paso. –

+0

Excelente respuesta. Funciona como un encanto de inmediato. Muchas gracias. –

0

Sospecho que esto es más sobre iPad que sobre Tapestry.

Podría invocar Response.DesaCompression() antes de escribir la transmisión a la respuesta; Tapestry puede intentar GZIP tu transmisión y es posible que el iPad no esté preparado para eso, ya que los formatos de video e imagen ya están comprimidos.

Además, no veo que se configure un encabezado de tipo de contenido; de nuevo, el iPad simplemente puede ser más sensible a eso que Chrome.

+0

Hola Howard. Creo que es genial que te tomes el tiempo para responder T5 (un gran marco) aquí en Stackoverflow. De todos modos, descubrí cuál era el problema y agregué la solución a mi pregunta. La versión TL; DR es que al iPad no le gusta si no tiene en cuenta el encabezado de solicitud http "Rango". Esto podría ser un problema para T5 también porque, según lo que digo, cuando el marco está sirviendo un activo, también ignorará el encabezado del rango. Voy a publicar una respuesta con más detalles. – Wulf

Cuestiones relacionadas