ipad上的视频流不适用于Tapestry5

ipad上的视频流不适用于Tapestry5,ipad,servlets,html5-video,tapestry,Ipad,Servlets,Html5 Video,Tapestry,我想通过HTML5视频标签将视频流到我的IPad上,后端带有tapestry5(5.3.5)。通常服务器端框架甚至不应该在这方面发挥作用,但不知怎的,它确实起了作用 无论如何,希望这里有人能帮我。请记住,我的项目在很大程度上是一个原型,我所描述的内容被简化/简化为相关部分。如果人们不回答强制性的“你想做错事”或与问题无关的安全/性能问题,我将不胜感激 下面是: 安装程序 我有一段来自苹果HTML5展示的视频,所以我知道格式不是问题。我有一个简单的tml页面“播放”,其中只包含一个“视频”标签 问

我想通过HTML5视频标签将视频流到我的IPad上,后端带有tapestry5(5.3.5)。通常服务器端框架甚至不应该在这方面发挥作用,但不知怎的,它确实起了作用

无论如何,希望这里有人能帮我。请记住,我的项目在很大程度上是一个原型,我所描述的内容被简化/简化为相关部分。如果人们不回答强制性的“你想做错事”或与问题无关的安全/性能问题,我将不胜感激

下面是:

安装程序 我有一段来自苹果HTML5展示的视频,所以我知道格式不是问题。我有一个简单的tml页面“播放”,其中只包含一个“视频”标签

问题 我首先实现了一个RequestFilter,它通过打开引用的视频文件并将其流式传输到客户端来处理来自视频控件的请求。这是基本的“如果路径以‘file’开头,那么将文件inputstream复制到响应outputstream”。这在Chrome上非常有效,但在Ipad上却不行。好吧,我想,一定是我遗漏了一些标题,所以我再次查看了Apple Showcase,包括了相同的标题和内容类型,但没有乐趣

接下来,我想,好吧,让我们看看如果我让t5提供文件会发生什么。我将视频复制到webapp上下文中,禁用了我的请求过滤器,并将简单文件名放在视频的src属性中。这适用于Chrome和IPad。 这让我感到惊讶,并促使我研究T5如何处理静态文件/上下文请求。到目前为止,我只觉得有两条不同的路径,我通过将硬连线的“video src”切换到带有@Path(“context:”)的资产来确认。这同样适用于Chrome,但不适用于IPad

所以我在这里真的迷路了。在允许它在IPad上工作的“简单上下文”请求中,这种秘密果汁是什么?没有什么特别的事情发生,但这是唯一可行的方法。问题是,我无法从我的webapp上下文中提供这些视频

解决方案 因此,事实证明,有一个http头称为“范围”,IPad与Chrome不同,它与视频一起使用。“秘诀”是静态资源请求的servlet处理程序知道如何处理范围请求,而T5则不知道。以下是我的自定义实现:

        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();
        }

我怀疑这更多的是关于iPad而不是Tapestry

在将流写入响应之前,我可能会调用Response.disableCompression();Tapestry可能正试图压缩你的流,而iPad可能还没有准备好,因为视频和图像格式通常已经被压缩了


此外,我没有看到设置内容类型标题;同样,iPad可能比Chrome更敏感。

我想从上面发布我的优化解决方案。希望这对某人有用

所以基本上问题似乎是我忽略了IPad不喜欢的“范围”http请求头。简而言之,这个头意味着客户端只需要响应的某一部分(在本例中是一个字节范围)

这就是iPad html视频请求的样子::

[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
这意味着iPad只需要第一个字节。如果您忽略此标题,只需发送一个完整的200响应,那么视频将无法播放。因此,您需要发送206响应(部分响应)并设置以下响应头:

[INFO] RequestLogger Content-Range:bytes 0-1/357772702
[INFO] RequestLogger Content-Length:2
这意味着“我将向您发送357772702个可用总字节中的字节0到1”

当您实际开始播放视频时,下一个请求将如下所示(除了范围标头ommited之外的所有内容):

因此,我的优化解决方案如下所示:

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();

        }
这个解决方案比我的第一个更好,因为它处理“范围”请求的所有情况,这似乎是像Chrome这样的客户端能够支持在视频中跳过的一个先决条件(此时他们将为视频中的该点发出范围请求)


但它仍然不完美。进一步的改进是正确设置“Last Modified”标头,并正确处理客户端请求的无效范围或其他范围,而不是字节。

Hi Howard。我认为在Stackoverflow上花时间回答T5(一个很棒的框架)是件好事。无论如何,我发现了问题所在,并为我的问题添加了解决方案。TL;DR版本是,如果忽略“范围”http请求头,iPad不喜欢它。这对于T5来说可能是个问题,因为从我所说的,当框架服务于一个资产时,它也会忽略范围头。我将发布一个更详细的答案。这是有用的信息;Tapestry没有理由不能在标准资产处理代码中自动处理这个问题;我们只是没有意识到它需要做。将这一级别的信息添加到我们的JIRA中是第一步。非常好的答案。马上就很有魅力了。谢谢。
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();

        }