我想通过后端的tapestry5(5.3.5)通过HTML5视频标签将视频流式传输到我的IPad。通常,服务器端框架甚至不应该在这方面发挥作用,但不知何故。
无论如何,希望有人可以帮助我。请记住,我的项目非常原型,我所描述的内容被简化/简化为相关部分。如果人们没有回应强制性的事情,那么我会非常感激。#34。你想做错事"或者与问题无关的安全/表现挑剔。
所以这就是:
我有一段来自Apple HTML5展示的视频,所以我知道格式不是问题。我有一个简单的tml页面" Play"只包含一个"视频"标签。
我首先实现了一个RequestFilter,它通过打开引用的视频文件并将其传输到客户端来处理来自视频控件的请求。这是基本的"如果路径以'文件'然后将文件输入流复制到响应输出流"。这适用于Chrome,但不适用于Ipad。好吧,但是,我必须是一些我不知道的标题,所以我再次看了Apple Showcase,包括相同的标题和内容类型,但没有快乐。
接下来,我想,如果我让t5提供文件,我们会看到会发生什么。我将视频复制到了webapp上下文,禁用了我的请求过滤器并将简单文件名放在视频的src属性中。这适用于Chrome和iPad。 这让我很吃惊,并促使我看看T5如何处理静态文件/上下文请求。到目前为止,我只是觉得有两种不同的路径,我通过切换硬连线的视频src"到具有@Path的资产("上下文:")。这同样适用于Chrome,但不适用于iPad。
所以我真的迷失在这里。这个秘密果汁在"简单的背景下是什么"请求允许它在IPad上工作?没有什么特别的事情,但它是唯一有效的方法。问题是,我无法从我的webapp上下文中提供这些视频...
因此,事实证明,这个http标头名为" Range"与Chrome不同,iPad与视频一起使用。 "秘密酱"然后是静态资源请求的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();
}
答案 0 :(得分: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个可用字节的字节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”标题并正确处理客户端请求无效范围或除了字节之外的其他内容。
答案 1 :(得分:0)
我怀疑这更多是关于iPad而不是Tapestry。
我可以在将流写入响应之前调用Response.disableCompression(); Tapestry可能正试图GZIP您的流,而iPad可能没有为此做好准备,因为视频和图像格式通常已经被压缩。
另外,我没有看到设置内容类型标题;再一次,iPad可能比Chrome更敏感。