赞
踩
一、问题提出
项目开发过程中遇到一个问题:
>基于webkit内核的浏览器H5的Video标签(获取android手机,一般也是webkit浏览器)可以正常播放MP4文件,但是基于苹果操作系统的safari浏览器或者苹果微信小程序内置浏览器都无法播放远程后台的MP4文件。
发现问题:
为了能发现android端与IOS端微信小程序内置浏览器的不同,通过对比两个浏览器发送给后台的包,可以发现如下端倪:
android浏览器:
苹果浏览器:
对比之后发现没有什么区别,最后发现问题并不是没有区别,而是真实的IOS系统或者IOS微信公众号内置浏览器发出来的包与上述IOS模拟器发出来的请求时不一样的!为了还原真相,我特意搭建了一个“黑苹果操作系统”模拟IOS浏览器发出的请求。得到如下结果:
以上可以看到android或webkit与IOS苹果浏览器播放的区别:Range字段,通过查询可以知道两者区别在于:
> android或webkit播放文件是一次请求到所有的数据,然后下载后进行播放(这对于移动手机来说会消耗很大流量),苹果针对这个问题进行了改进,所以才有了分段请求数据的问题,也就是我们常说的http1.1中的断点续传。
二、问题解决
知道两者的区别之后,我们其实在后台支持两种请求协议即可,一种是不包含Range的请求,一种是包含Range的分段请求:
我后台节后如下:
```
@ApiOperation("文件下载")
@GetMapping("/download")
@ApiImplicitParams({@ApiImplicitParam(paramType = "query", dataType = "String", name = "path", value = "文件路径", required = true)})
public void downLoad(@RequestParam(value="path", required=true) String path,
@RequestHeader(value="range", required=false) String range,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
try {
if (CheckUtil.isNull(path)) {
response.sendError(-1, "参数不合法");
}
printHeaders(request);
// 端点续传:如果是苹果是分段请求,如果是android或webkit则直接下载整个文件
int start = 0;
int end = -1;
if (!CheckUtil.isNull(range)){
// bytes=0-1 or bytes=0-
String v = range.trim().split("=")[1];
String[] range_size = v.split("-");
if (range_size.length >= 1) {
start = Integer.valueOf(range_size[0]);
}
if (range_size.length >= 2) {
end = Integer.valueOf(range_size[1]);
}
}
System.out.println("start:" + start + " end:" + end);
if (path.startsWith("group")) {
downloadFromFDFS(path, start, end, response);
} else {
downloadFromHDFS(path, start, end, response);
}
} catch (Exception e) {
log.error("下载文件出错:{}", e.getMessage());
}
}
```
由于我后台的数据存储包括两种方式:基于FastDFS的图片存储和基于HDFS的大文件(如视频附件等)存储,这里的视频主要就存储在HDFS中,接口中我们主要分析了请求头参数Range,得到请求的数据的start和请求结束end,如果包含Range我们就发送range指定的内容给前端,如果没有指定我们就发送0-end的所有文件数据给前端(一次性),所以我们主要看HDFS下载接口即可:
```
/**
* @功能描述: 从HDFS中下载文件
* @编写作者: lixx2048@163.com
* @开发日期: 2020年4月4日
* @历史版本: V1.0
* @参数说明:
*/
private boolean downloadFromHDFS(String path,int start, int end, HttpServletResponse response) {
String fileName = path.substring(path.lastIndexOf('/')+1);
String extName = FilenameUtils.getExtension(fileName);
// 创建文件
HdfsProxy hdfsProxy = new HdfsProxy(HadoopConfig);
hdfsProxy.open();
// 写文件
ServletOutputStream out = null;
FSDataInputStream in = null;
try {
// 获取输出流
out = response.getOutputStream();
// 设置相应类型application/octet-stream(注:applicatoin/octet-stream 为通用,一些其它的类型苹果浏览器下载内容可能为空)
response.setContentType(getContentTypeByExtName(extName));
// 设置头信息 Content-Disposition为属性名 附件形式打开下载文件 指定名称 为 设定的fileName
//response.setHeader("Content-Disposition", "attachment;inline;filename=" + URLEncoder.encode(fileName, "UTF-8"));
response.setHeader("Content-Disposition", "filename=" + URLEncoder.encode(fileName, "UTF-8"));
response.setHeader("Accept-Ranges", "bytes");
long size = hdfsProxy.getFileSize(path);
in = hdfsProxy.Open(path);
if (null == in){
return false;
}
// webkit可以不设置文件大小
int need = 1024*1024;
if (end > 0){
need = end - start + 1;
response.setHeader("Content-Length", String.valueOf(need));
} else {
response.setHeader("Content-Length", String.valueOf(size));
}
byte buffer[] = new byte[need];
in.seek(start);
int total = 0;
boolean toFileEnd = false;
while (true) {
int read = in.read(buffer);
if (read <= 0) {
toFileEnd = true;
break;
}
out.write(buffer, 0, read);
total += read;
if (end > 0 && total >= end + 1) {
break;
}
}
// 苹果分段请求(HTTP续传方式)
if (end > 0){
// 未达到文件末尾
if(!toFileEnd){
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
}
String value = String.format("bytes %d-%d/%d", start, end, size);
response.setHeader("Content-Range", value);
}
/*
// 从HDFS中下载文件
ServletOutputStream out2 = out;
status = hdfsProxy.download(path, new ReadProgress() {
@Override
public void progress(byte[] buffer, long size) {
try {
out2.write(buffer, 0, (int)size);
} catch (IOException e) {
e.printStackTrace();
}
}
});
*/
} catch (Exception e) {
log.error("读取HDFS文件文件异常:{}", e.getMessage());
} finally {
if(null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (in != null) {
hdfsProxy.close(in);
}
hdfsProxy.close();
}
return false;
}
```
从以上代码中我们可以看到如果是苹果的分段请求,我们需要注意一下几点:
- 我们在回复的请求头中多了“Content-Range”字段,表名本次请求我回复的内容大小以及数据的**总长度**(这个很关键,苹果浏览器分段请求前发送的分段请求为0-1,目的就是探测到文件的总长度以便后续进行播放控制)
- 分段请求回复的数据为真实的分段数据,如请求10-20的分段,我们直接定位到文件偏移量为10的位置然后发送20-10+1=11个字节数据
- Content-Length指定为本次真实发送的数据长度
- 如果分段请求没有到文件末尾,我们回复的http状态码为206(表示只返回部分数据),如果最终读取达到文件末尾则http状态码返回200(默认)
经过以上修改后,后端代码就支持android和苹果浏览器视频播放了!
技术交流群:961179337
微信交流:lixiang1653
邮箱:lixx2048@163.com
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。