7

我想通过 HTML5 视频标签在后端使用 Tapestry5 (5.3.5) 将视频流式传输到我的 iPad。通常,服务器端框架甚至不应该在其中发挥作用,但它确实发挥了作用。

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

所以这里是:

设置

我有一段取自 Apple HTML5 展示的视频,所以我知道格式不是问题。我有一个简单的 tml 页面“播放”,它只包含一个“视频”标签。

问题

我首先实现了一个 RequestFilter,它通过打开引用的视频文件并将其流式传输到客户端来处理来自视频控件的请求。这是基本的“如果路径以'文件'开头,则将文件输入流复制到响应输出流”。这适用于 Chrome,但不适用于 Ipad。好吧,不过,我一定是缺少一些标题,所以我再次查看了 Apple Showcase 并包含了相同的标题和内容类型,但没有任何乐趣。

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

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

解决方案

因此,事实证明,有一个名为“Range”的 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();
        }
4

2 回答 2

8

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

所以基本上问题似乎是我忽略了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”。

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

[INFO] RequestLogger Range:bytes=0-357772701

所以我的改进解决方案如下所示:

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”标头并正确处理客户端请求的无效范围或字节范围以外的其他内容。

于 2012-10-10T22:40:11.480 回答
0

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

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

另外,我没有看到设置内容类型标头;再一次,iPad 可能比 Chrome 更敏感。

于 2012-10-09T17:54:49.163 回答