2012-10-07 46 views
6

我想通过后端带有tapestry5(5.3.5)的HTML5视频标签将视频流式传输到我的iPad。通常服务器端框架甚至不应该在这方面发挥作用,但它不知道如何。视频流式传输到ipad不适用于Tapestry5

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

所以这里有云:

设置

我从苹果HTML5拍摄的视频展示,所以我知道该格式是不是一个问题。我有一个简单的tml页面“播放”,只包含一个“视频”标签。

问题

我开始通过实施处理通过打开引用的视频文件,并将其流媒体客户端从视频控制的请求RequestFilter。这是基本的“如果路径以'文件'开始,然后将文件输入流复制到响应输出流”。这对Chrome非常适用,但不适用于Ipad。好吧,我虽然,一定是我失踪的一些标题,所以我再次看着Apple Showcase,并包含相同的标题和内容类型,但没有喜悦。

接下来,我想,让我们看看如果我让t5服务文件会发生什么。我将视频复制到了webapp上下文,禁用了我的请求过滤器,并将简单文件名放在视频的src属性中。这适用于Chrome和iPad。 这让我感到惊讶,并促使我看看T5如何处理静态文件/上下文请求。到目前为止,我只是觉得有两种不同的方式,我已经通过使用@Path(“context:”)将硬连线“视频src”切换到资产来确认。这又一次适用于Chrome,但不适用于iPad。

所以我真的迷失在这里。在“简单的上下文”请求中,这个秘密果汁是什么使它能够在IPad上工作?没有什么特别的,但它是唯一的方法。问题是,我真的不能从我的web应用程序上下文服务于那些西元...

解决方案

所以,事实证明,有这个所谓的“范围” 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(); 
     } 

回答

7

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

所以基本上这个问题似乎是我无视IPad不喜欢的“Range”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 1可用的总字节数字节0”。

当你真正开始播放视频,下一个请求看起来像这样(除范围标头的一切中省略):

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

+0

这是有用的信息;没有理由为什么Tapestry无法在标准资产处理代码中自动处理;我们只是不知道它需要做什么。将这一级别的信息添加到我们的JIRA是第一步。 –

+0

优秀的答案。马上就像魅力一样工作。非常感谢。 –

0

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

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

另外,我没有看到正在设置的内容类型标题;再次,iPad可能会比Chrome更敏感。

+0

嗨,霍华德。我认为你花时间在Stackoverflow上回答T5(一个很好的框架)是非常棒的。无论如何,我发现问题是什么,并将解决方案添加到我的问题中。 TL; DR版本是iPad不喜欢它,如果你忽略“范围”http请求标头。这可能是T5的一个问题,也是因为从我说的话来看,当框架提供资源时,它也会忽略Range头部。我会发布一个更详细的答案。 – Wulf