Skip to content

返回视频文件并支持断点续传 ​

本文档介绍如何通过 TioBoot Handler 返回视频文件,支持 HTTP Range 断点续传,以便前端页面使用 <video> 标签直接播放视频。文档中同时说明了如何禁用视频文件的 gzip 压缩,以避免浏览器在解码时出现错误。

背景说明 ​

对于视频文件(例如 MP4 格式),由于其本身已经经过压缩,如果服务器再次对其进行 gzip 压缩,可能会破坏文件格式,从而导致浏览器解码失败并出现 ERR_CONTENT_DECODING_FAILED 错误。因此,在返回视频数据时应当确保不对视频数据进行 gzip 二次压缩。同时,为了支持用户拖动进度条等操作,需要实现 HTTP Range 请求支持,也称为断点续传。

代码实现 ​

以下是完整的 Java 示例代码,展示了如何在 TioBoot Handler 中返回视频文件,并处理 Range 请求。

java
package nexus.io.linux.handler;

import java.io.File;
import java.io.RandomAccessFile;

import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.http.common.ResponseHeaderKey;
import nexus.io.tio.http.server.util.CORSUtils;
import nexus.io.tio.http.server.util.Resps;
import nexus.io.tio.utils.http.ContentTypeUtils;
import nexus.io.tio.utils.hutool.FileUtil;
import nexus.io.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");
        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.setSkipGzipped(true);
    return response;
  }
}

代码解析 ​

  1. 跨域支持:
    使用 CORSUtils.enableCORS(response) 开启跨域支持,确保前端页面能够跨域访问视频文件。

  2. 文件定位与 MIME 类型:
    根据请求路径构建文件对象,并使用工具类 ContentTypeUtils 获取正确的 MIME 类型。注意,视频文件建议使用 video/mp4。

  3. 断点续传处理:
    检查请求头中是否存在 Range,若存在则解析范围,利用 RandomAccessFile 定位文件,并返回对应部分的数据。响应状态码设置为 206(Partial Content),同时返回 Content-Range 和 Accept-Ranges 头。

  4. 完整文件返回:
    如果请求中不包含 Range,则直接读取整个文件,并在响应头中标识支持断点续传。

  5. 禁用 gzip 压缩:
    视频文件已经经过压缩,为避免二次压缩导致格式错误,调用 response.setSkipGzipped(true) 告知框架不对该响应进行 gzip 压缩。

前端 HTML 示例 ​

下面的 HTML 示例展示了如何在前端页面中使用 <video> 标签播放视频。由于后端已正确实现 Range 支持与禁用 gzip 压缩,前端视频播放可以正常进行拖动和定位。

html
<!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 播放器能正常播放视频。该方案不仅提升了用户体验,还保证了视频传输的稳定性和兼容性。

MIT Licensed