代码片段示例
String ossUrl = getVideoOssURL(taskCode,checkCode,fileName);
// ossUrl = ossRoot+"/"+ossUrl;
SimplifiedObjectMeta simplifiedObjectMeta = AliyunUtils.getSimplifiedObjectMeta(bucketName, ossUrl);
long fileLength = simplifiedObjectMeta.getSize();
String range = request.getHeader("Range");
long start = 0, end = fileLength - 1;
response.setContentType("video/mp4");
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
if (range != null) {
String[] ranges = range.replace("bytes=", "").split("-");
start = Long.parseLong(ranges[0]);
if (ranges.length > 1 && !ranges[1].isEmpty()) {
end = Long.parseLong(ranges[1]);
} else {
end = Math.min(fileLength - 1, start + (maxPartSize * 1024L * 1024L) - 1);
}
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + fileLength);
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(end - start + 1));
response.setStatus(206);
}else{
//浏览器发出的探测包
/*
对于视频播放(尤其是 MP4/WebM 等 progressive streaming),浏览器第一个探测请求通常是 不带 Range 头 的 GET 请求(用来探活、获取文件信息)。
HTTP/1.1 200 OK
Content-Type: video/mp4 # 或者 video/webm
Content-Length: 123456789 # 文件真实总大小(必须准确)
Accept-Ranges: bytes # ← 最重要!告诉浏览器支持 Range 请求
Content-Disposition: inline # 可选,推荐 inline
Cache-Control: public, max-age=31536000
ETag: "xxx" # 强烈推荐,加上 ETag
Last-Modified: Wed, 08 May 2026 00:00:00 GMT
必须返回 Accept-Ranges: bytes,否则浏览器后续不会发 Range 请求(或降级为全量下载)。
Content-Length 必须是完整文件大小。
*/
response.setStatus(200);
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileLength));
}
// Set the content type and attachment header.
// String contentType = request.getServletContext().getMimeType(video.getFile().getAbsolutePath());
// response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName);
// headers.setContentType(MediaType.parseMediaType(contentType));
GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, ossUrl);
// 对于大小为1000 Bytes的文件,正常的字节范围为0~999。
// 获取0~999字节范围内的数据,包括0和999,共1000个字节的数据。如果指定的范围无效(比如开始或结束位置的指定值为负数,或指定值大于文件大小),则下载整个文件。
getObjectRequest.setRange(start, end);
// Create resource that represents the part of the video file.
try (OSSObject ossObject = AliyunUtils.getOssObject(getObjectRequest);
InputStream in = ossObject.getObjectContent();){
ServletOutputStream out = response.getOutputStream();
byte[] buf = new byte[8192];
for (int n = 0; n != -1; ) {
n = in.read(buf, 0, buf.length);
out.write(buf, 0, n);
}
} catch (ClientAbortException e) {
// 【关键修复】客户端主动断开属于正常现象,不要抛异常
log.info("视频流被客户端中断(正常现象): {}", e.getMessage());
// 不要继续写数据,也不要抛异常
} catch (Exception e) {
log.error("视频流处理异常", e);
throw new RuntimeException(e); // 其他真实异常才往上抛
}
视频流接口异常处理
这是一个典型的视频流接口异常处理不当导致的连锁问题:
核心错误链
- videoPlayRangeOss 方法在往客户端写视频数据时,客户端突然断开连接(Broken pipe)。
- 这触发了 ClientAbortException。
- 全局异常处理器(ControllerAdviceConfig#runTimeExceptionHandler)试图返回 BaseResponse(JSON)。
- 但此时响应头已经是 video/mp4,Spring 找不到对应的 HttpMessageConverter,抛出 HttpMessageNotWritableException。
最终解决方案(推荐这样改)
1. 修改全局异常处理器(最关键)
Java
@ControllerAdvice
public class ControllerAdviceConfig {
private static final Logger log = LoggerFactory.getLogger(ControllerAdviceConfig.class);
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> runTimeExceptionHandler(HttpServletRequest request, Exception e) {
// 新增:视频流请求特殊处理
if (isVideoRequest(request)) {
log.warn("视频流请求发生异常,客户端可能已断开: {}", e.getMessage());
// 直接返回空响应,避免转换器错误
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.build();
}
log.error("其他异常!原因是: ", e);
BaseResponse error = new BaseResponse(500, e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
// 判断是否为视频流请求
private boolean isVideoRequest(HttpServletRequest request) {
String uri = request.getRequestURI();
String accept = request.getHeader("Accept");
return uri != null && (uri.contains("/video") || uri.contains("videoPlay"));
}
}
2. 优化视频流方法(强烈建议)
在 TaskInfoService.videoPlayRangeOss 或 videoPlay 方法中:
Java
public void videoPlayRangeOss(...) {
try {
// ... 正常视频流写入逻辑
response.setContentType("video/mp4");
// 使用 OutputStream 写入视频数据
} catch (ClientAbortException | IOException e) {
// 客户端主动断开连接属于正常现象(用户暂停、拖动进度条、关闭视频等)
log.info("客户端断开视频连接: {}", e.getMessage());
// 不要往外抛异常
} catch (Exception e) {
log.error("视频播放异常", e);
throw e; // 其他异常才往上抛
}
}
3. 在 Controller 层也加保护
Java
@GetMapping("/videoPlay")
public void video(HttpServletRequest request, HttpServletResponse response) {
try {
taskInfoService.videoPlay(...);
} catch (ClientAbortException e) {
log.info("视频流被客户端中断");
} catch (Exception e) {
log.error("视频播放失败", e);
// 这里不要返回 BaseResponse
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
额外建议
-
抑制 Broken Pipe 日志噪声 (可选):
YAML
logging: level: org.apache.catalina.connector.ClientAbortException: WARN -
AOP 注意:你的 CommonWrapAop 也在包裹视频接口,如果里面有异常处理,也需要做类似判断。
请把下面代码贴给我,我帮你精准修改:
- ControllerAdviceConfig.java 的完整异常处理器代码
- TaskInfoService.java 中 videoPlayRangeOss 方法的关键部分(尤其是写流的那一段)
这样我可以给你直接可复制的修复代码。