返回视频文件并支持断点续传
本文档介绍如何通过 TioBoot Handler 返回视频文件,支持 HTTP Range 断点续传,以便前端页面使用 <video>
标签直接播放视频。文档中同时说明了如何禁用视频文件的 gzip 压缩,以避免浏览器在解码时出现错误。
背景说明
对于视频文件(例如 MP4 格式),由于其本身已经经过压缩,如果服务器再次对其进行 gzip 压缩,可能会破坏文件格式,从而导致浏览器解码失败并出现 ERR_CONTENT_DECODING_FAILED
错误。因此,在返回视频数据时应当确保不对视频数据进行 gzip 二次压缩。同时,为了支持用户拖动进度条等操作,需要实现 HTTP Range 请求支持,也称为断点续传。
代码实现
以下是完整的 Java 示例代码,展示了如何在 TioBoot Handler 中返回视频文件,并处理 Range 请求。
package com.litongjava.linux.handler;
import java.io.File;
import java.io.RandomAccessFile;
import com.litongjava.tio.boot.http.TioRequestContext;
import com.litongjava.tio.http.common.HttpRequest;
import com.litongjava.tio.http.common.HttpResponse;
import com.litongjava.tio.http.common.ResponseHeaderKey;
import com.litongjava.tio.http.server.util.CORSUtils;
import com.litongjava.tio.http.server.util.Resps;
import com.litongjava.tio.utils.http.ContentTypeUtils;
import com.litongjava.tio.utils.hutool.FileUtil;
import com.litongjava.tio.utils.hutool.FilenameUtils;
public class CacheHandler {
public HttpResponse index(HttpRequest request) {
String path = request.getRequestLine().getPath();
HttpResponse response = TioRequestContext.getResponse();
// 开启跨域支持
CORSUtils.enableCORS(response);
// 定位文件位置,文件存放在项目根目录下
File file = new File("." + File.separator + path);
String suffix = FilenameUtils.getSuffix(path);
String contentType = ContentTypeUtils.getContentType(suffix);
if (!file.exists()) {
response.setStatus(404);
return response;
}
long fileLength = file.length();
// 检查是否存在 Range 头信息(用于支持断点续传)
String range = request.getHeader("Range");
if (range != null && range.startsWith("bytes=")) {
// 例如 Range: bytes=0-1023
String rangeValue = range.substring("bytes=".length());
String[] parts = rangeValue.split("-");
try {
long start = parts[0].isEmpty() ? 0 : Long.parseLong(parts[0]);
long end = (parts.length > 1 && !parts[1].isEmpty()) ? Long.parseLong(parts[1]) : fileLength - 1;
// 检查 range 合法性
if (start > end || end >= fileLength) {
response.setStatus(416); // Range Not Satisfiable
return response;
}
long contentLength = end - start + 1;
byte[] data = new byte[(int) contentLength];
// 使用 RandomAccessFile 定位到指定位置读取数据
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
raf.seek(start);
raf.readFully(data);
}
// 设置响应头,返回部分内容
response.setStatus(206); // Partial Content
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
response.setHeader("Accept-Ranges", "bytes");
response.setHeader(ResponseHeaderKey.Content_Length, String.valueOf(contentLength));
Resps.bytesWithContentType(response, data, contentType);
} catch (Exception e) {
response.setStatus(416);
}
} else {
// 如果没有 Range 请求,则直接返回整个文件
byte[] readBytes = FileUtil.readBytes(file);
response.setHeader("Accept-Ranges", "bytes");
Resps.bytesWithContentType(response, readBytes, contentType);
}
// 注意:视频文件(如 mp4)本身已压缩,再进行 gzip 压缩可能会破坏文件格式
// 因此此处设置 hasGzipped 为 true,以防止 TioBoot 对响应进行 gzip 压缩
response.setHasGzipped(true);
return response;
}
}
代码解析
跨域支持:
使用CORSUtils.enableCORS(response)
开启跨域支持,确保前端页面能够跨域访问视频文件。文件定位与 MIME 类型:
根据请求路径构建文件对象,并使用工具类ContentTypeUtils
获取正确的 MIME 类型。注意,视频文件建议使用video/mp4
。断点续传处理:
检查请求头中是否存在Range
,若存在则解析范围,利用RandomAccessFile
定位文件,并返回对应部分的数据。响应状态码设置为 206(Partial Content),同时返回Content-Range
和Accept-Ranges
头。完整文件返回:
如果请求中不包含 Range,则直接读取整个文件,并在响应头中标识支持断点续传。禁用 gzip 压缩:
视频文件已经经过压缩,为避免二次压缩导致格式错误,调用response.setHasGzipped(true)
告知框架不对该响应进行 gzip 压缩。
前端 HTML 示例
下面的 HTML 示例展示了如何在前端页面中使用 <video>
标签播放视频。由于后端已正确实现 Range 支持与禁用 gzip 压缩,前端视频播放可以正常进行拖动和定位。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>视频播放示例</title>
</head>
<body>
<video controls width="640" height="360">
<source src="http://127.0.0.1/cache/495615957315309568/videos/1080p30/CombinedScene.mp4" type="video/mp4" />
您的浏览器不支持 video 标签。
</video>
</body>
</html>
说明
- controls 属性: 显示播放器控件,允许用户进行播放、暂停、全屏等操作。
- source 标签: 指定视频文件的 URL 与 MIME 类型(这里使用
video/mp4
)。 - 跨域与断点续传: 前端通过 URL 访问视频时,将依赖后端提供的跨域支持和断点续传能力,实现顺畅播放和拖动操作。
总结
通过上述代码和配置,您可以使用 TioBoot Handler 实现视频文件的断点续传支持,同时避免对视频数据进行不必要的 gzip 压缩,确保前端 HTML5 播放器能正常播放视频。该方案不仅提升了用户体验,还保证了视频传输的稳定性和兼容性。