【问题标题】:Video streaming to ipad does not work with Tapestry5视频流到 ipad 不适用于 Tapestry5
【发布时间】:2012-09-27 22:35:49
【问题描述】:

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

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

就这样吧:

设置

我有一段来自 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();
        }

【问题讨论】:

    标签: ipad servlets html5-video tapestry


    【解决方案1】:

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

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

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

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

    【讨论】:

    • 这是有用的信息; Tapestry 没有理由不能在标准资产处理代码中自动处理这个问题;我们只是不知道它需要做。将这一级别的信息添加到我们的 JIRA 是第一步。
    • 优秀的答案。立即像魅力一样工作。非常感谢。
    【解决方案2】:

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

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

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

    【讨论】:

    • 嗨霍华德。我认为您花时间在 Stackoverflow 上回答 T5(一个很棒的框架)真是太好了。无论如何,我发现了问题所在,并将解决方案添加到我的问题中。 TL;DR 版本是,如果您忽略“范围”http 请求标头,iPad 将不喜欢它。这也可能是 T5 的一个问题,因为据我所说,当框架为资产提供服务时,它也会忽略 Range 标头。我会发布更详细的答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-27
    • 2013-03-15
    • 1970-01-01
    • 1970-01-01
    • 2011-11-07
    相关资源
    最近更新 更多