后端返回视频流 断点续传
1. 返回stream的所有数据
- 从本地读取视频文件,返回前端视频流。
- 前端拼接请求地址,放在例如video标签的src属性里;或者浏览器键入也可以直接播放视频。
- 这种方式只能请求所有数据,如果中途断开,不能从断开处重新获取数据;并且有可能出现作为视频流时,拖动进度条无法正常播放的情况。
@GetMapping("/stream")
public void stream(@RequestParam Long key, HttpServletResponse response) {
LearningFile storage = learningService.getById(key);
if (ObjectUtil.isNull(storage)) {
throw new ServiceException(ErrorCode.FILE_NOT_EXIST);
}
String filePath = storage.getAbsolutePath();
File file = FileUtil.file(filePath);
if (!FileUtil.exist(file)) {
throw new ServiceException(ErrorCode.FILE_NOT_EXIST);
}
try (BufferedInputStream is = FileUtil.getInputStream(file); ServletOutputStream os = response.getOutputStream()) {
response.reset();
response.addHeader("Content-Length", "" + file.length());
response.setContentType("video/mp4;charset=UTF-8");
IOUtils.copy(is, os);
os.flush();
response.flushBuffer();
} catch (IOException e) {
throw new ServiceException(ErrorCode.FILE_DOWNLOAD_FAIL);
}
}
2. 断点续传,请求部分stream数据
即使线路中断,恢复时继续上传,视频也能拖动进度条了。
2.1 Http Header参数
-
Range
请求头中参数,表明请求数据的起始和末尾,一般格式Range: bytes=0-499 表示第 0-499 字节范围的内容 Range: bytes=500-999 表示第 500-999 字节范围的内容 Range: bytes=-500 表示最后 500 字节的内容 Range: bytes=500- 表示从第 500 字节开始到文件结束部分的内容 Range: bytes=0-0,-1 表示第一个和最后一个字节 Range: bytes=500-600,601-999 同时指定几个范围
-
Content-Range
用于响应头中,在发出带 Range 的请求后,服务器会在 Content-Range 头部返回当前接受的范围和文件总大小。一般格式:Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]
例如:
Content-Range: bytes 0-499/22400
0-499 是指当前发送的数据的范围,而 22400 则是文件的总大小。
而在响应完成后,返回的响应头内容也不同:
HTTP/1.1 200 Ok(不使用断点续传方式) HTTP/1.1 206 Partial Content(使用断点续传方式)
@GetMapping("/stream") public void stream(@RequestParam Long key, HttpServletResponse response, HttpServletRequest request) { LearningFile storage = learningService.getById(key); if (ObjectUtil.isNull(storage)) { throw new ServiceException(ErrorCode.FILE_NOT_EXIST); } String filePath = storage.getAbsolutePath(); File file = FileUtil.file(filePath); // 文件不存在 if (!FileUtil.exist(file)) { throw new ServiceException(ErrorCode.FILE_NOT_EXIST); } // 判断文件类型 String type = FileTypeUtil.getType(file); boolean isVideo = com.nbport.answer.domain.util.FileUtil.isVideoType(type); response.reset(); if (isVideo) { // 处理视频文件 // 请求头中range, 不存在则退出 String rangeString = request.getHeader("Range"); if (!StringUtils.hasText(rangeString)) { return; } try (RandomAccessFile accessFile = new RandomAccessFile(file, "r"); ServletOutputStream os = response.getOutputStream()) { // 获取range的起始 末尾 long rangeStart = 0, rangeEnd = 0; String[] ranges = rangeString.split("="); if (ranges.length > 1) { String[] rangeDatas = ranges[1].split("-"); rangeStart = Integer.parseInt(rangeDatas[0]); if (rangeDatas.length > 1) { rangeEnd = Integer.parseInt(rangeDatas[1]); } } long contentLength = rangeEnd > 0 ? rangeEnd - rangeStart + 1 : accessFile.length(); rangeEnd = rangeEnd > 0 ? rangeEnd : file.length() - 1; response.addHeader(HttpHeaders.CONTENT_LENGTH, "" + contentLength); response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + rangeStart + "-" + rangeEnd + "/" + accessFile.length()); response.setContentType("video/" + type + ";charset=UTF-8"); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); accessFile.seek(rangeStart); byte[] cache = new byte[4096]; while (contentLength > 0) { int len = accessFile.read(cache); if (contentLength < cache.length) { os.write(cache, 0, (int) contentLength); } else { os.write(cache, 0, len); if (len < cache.length) { break; } } contentLength -= cache.length; } os.flush(); } catch (IOException e) { log.error("异常读取", e); } } else { // 非视频文件 try (BufferedInputStream is = FileUtil.getInputStream(file); ServletOutputStream os = response.getOutputStream()) { IOUtils.copy(is, os); response.setHeader(HttpHeaders.CONTENT_LENGTH, file.length() + ""); response.setHeader(HttpHeaders.CONTENT_TYPE, "application/octet-stream"); os.flush(); } catch (Exception e) { log.error("处理文件异常, {}", e); } } }