playsinline best app ipad servlets html5-video tapestry

ipad - playsinline - best video editor app iphone



La transmisiĆ³n de video a ipad no funciona con Tapestry5 (2)

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

Entonces, básicamente, el problema parecía ser que estaba ignorando el encabezado de solicitud http "Range", 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.

Así es como se ve una solicitud de video html para iPad ::

[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

Significa que el iPad solo 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, debe enviar una respuesta 206 (respuesta parcial) y establecer los siguientes encabezados de respuesta:

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

Esto significa que "te estoy enviando byte 0 a 1 de 357772702 bytes totales disponibles".

Cuando realmente comiences a reproducir el video, la próxima solicitud se verá así (todo excepto el encabezado del rango omitido):

[INFO] RequestLogger Range:bytes=0-357772701

Entonces mi solución refinada se ve así:

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

Sin embargo, 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.

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.

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:

Preparar

Tengo un video tomado de la presentación Apple HTML5, 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é implementando un RequestFilter que maneja la solicitud del control de video abriendo el archivo de video referenciado y transmitiéndolo al 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ó nada.

Luego, pensé, bueno, veamos qué sucede si dejo que t5 sirva 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 estoy realmente 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 realmente no puedo servir esos videos desde mi contexto de aplicaciones web ...

Solución

Entonces, resulta que hay un encabezado http llamado "Rango" y que el iPad, a diferencia de Chrome, lo usa 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(); }


Sospecho que se trata más de iPad que de Tapestry.

Podría invocar Response.disableCompression () antes de escribir la secuencia 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 un encabezado de tipo de contenido establecido; de nuevo, el iPad simplemente puede ser más sensible a eso que Chrome.