情况说明
一般情况下,我们返回文件的方式都是前端调用接口,然后后端获取文件,转为二进制流,再返回给前端。
这样处理的方式会有问题,如果是手机端需要看视频,如果每次请求都是返回整个文件的流,那么消耗的流量会非常大,所以我们会采用分段返回的方式实现。(下图的截图来自于B站的请求接截图)
实现思路
前端会在请求头中告诉后端,当前请求的文件数据范围,用Range
字段表示,具体的格式:
后端接收到请求后,会读取指定的文件,解析请求头中Range
字段, 读取指定范围内的文件内容,通过流返回给前端,前端不断调用接口的方式,分段获取文件流,后端会在响应头中告知文件的总大小,当前获取流的区间范围,具体的格式如下:
后端代码演示
我们会采用自定义的缓冲区实现,这样可以根据实际情况去更改缓冲区的大小。
注意一下Content-Type
这个响应头,如果我们能够确认返回的视频格式是MP4,我们可以设置为video/mp4
。如果不知道返回的是什么类型,可以用application/octet-stream
,它代表通用的二进制流文件,不确定类型。
新建一个FileUtil工具类
public class FileUtil {
/**
* 缓冲区大小(字节)
*/
static int bufferSize = 1024 * 8;
/**
* 根据前端的请求分段输出
* @param fileName 文件名
* @param range 分段范围
* @param response http响应
* @throws IOException
*/
public static void rangeRead(String fileName, String range, HttpServletResponse response) throws IOException {
System.out.println("-----分段读取范围:" + range);
//读取类路径下的文件按
ClassPathResource classPath = new ClassPathResource("file/" + fileName);
File file = classPath.getFile();
//获取文件总大小
long total = file.length();
//创建缓冲区
byte[] buffer = new byte[bufferSize];
//获取分段读取的区间
String[] split = range.substring(6).split("-");
long start = Long.parseLong(split[0]);
//默认结束是文件总长度
long end = total;
if (split.length == 2) {
//如果请求有结束长度,则用请求的长度
end = Long.parseLong(split[1]);
}
//计算需要读取的长度
long readLength = end - start +1;
//设置响应头
//206代表部分资源
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(fileName, "UTF-8"));
response.addHeader("Content-Type", "application/octet-stream");
response.addHeader("Content-Length", String.valueOf(readLength));
response.addHeader("Content-Range", "bytes " + start + "-" + end + "/" + total);
try (FileInputStream input = new FileInputStream(file)) {
//跳过前面已经读取过的字节
input.skip(start);
//记录剩下的长度
long remaining = readLength;
while (remaining > 0) {
//将文件写入缓冲池中
int bytesRead = input.read(buffer, 0, (int) Math.min(bufferSize, remaining));
if (bytesRead < 0) {
//如果写入的实际长度小于0,则退出循环
break;
}
//将缓冲池的数据写入输出流中
response.getOutputStream().write(buffer, 0, bytesRead);
remaining -= bytesRead;
}
//将输出流的数据强制写出
response.getOutputStream().flush();
}
//最后关闭输出流
response.getOutputStream().close();
}
/**
* 处理整个资源请求
* @param fileName
* @param response
* @throws IOException
*/
public static void allRead(String fileName, HttpServletResponse response) throws IOException {
ClassPathResource classPath = new ClassPathResource("file/" + fileName);
File file = classPath.getFile();
//获取文件总大小
long total = file.length();
//创建缓冲区
byte[] buffer = new byte[bufferSize];
//设置响应头
response.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(fileName, "UTF-8"));
response.addHeader("Content-Type", "video/mp4");
response.addHeader("Content-Length", String.valueOf(total));
try (FileInputStream input = new FileInputStream(file)) {
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
//将缓冲池的数据写入输出流中
response.getOutputStream().write(buffer, 0, bytesRead);
}
//将输出流的数据强制写出
response.getOutputStream().flush();
}
//最后关闭输出流
response.getOutputStream().close();
}
}
在controller中解析请求头的range
字段, 确认是分段读取还是一次性返回:
@GetMapping("/video")
public void getVideo(HttpServletRequest request, HttpServletResponse response) throws IOException {
//判断读取的视频是分段还是整读
// 检查客户端是否支持范围请求
String range = request.getHeader("Range");
if (range != null && range.startsWith("bytes=")) {
//读取指定范围的数据
FileUtil.rangeRead("xxx.mp4", range, response);
} else {
//一次性返回
FileUtil.allRead("xxx.mp4", response);
}
}
前端处理
完整的视频流的话,直接请求接口,浏览器可以自动解析然后播放。但是切片的视频流,浏览器没有办法直接解析,所以还需要前端用其他组件支持。