代码片段示例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-age31536000 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试图返回 BaseResponseJSON。但此时响应头已经是 video/mp4Spring 找不到对应的 HttpMessageConverter抛出 HttpMessageNotWritableException。最终解决方案推荐这样改1.修改全局异常处理器最关键JavaControllerAdvice public class ControllerAdviceConfig { private static final Logger log LoggerFactory.getLogger(ControllerAdviceConfig.class); ExceptionHandler(Exception.class) public ResponseEntityObject 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 方法中Javapublic 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 层也加保护JavaGetMapping(/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 日志噪声可选YAMLlogging: level: org.apache.catalina.connector.ClientAbortException: WARNAOP 注意你的 CommonWrapAop 也在包裹视频接口如果里面有异常处理也需要做类似判断。请把下面代码贴给我我帮你精准修改ControllerAdviceConfig.java 的完整异常处理器代码TaskInfoService.java 中 videoPlayRangeOss 方法的关键部分尤其是写流的那一段这样我可以给你直接可复制的修复代码。
http协议处理播放video/mp4视频
代码片段示例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-age31536000 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试图返回 BaseResponseJSON。但此时响应头已经是 video/mp4Spring 找不到对应的 HttpMessageConverter抛出 HttpMessageNotWritableException。最终解决方案推荐这样改1.修改全局异常处理器最关键JavaControllerAdvice public class ControllerAdviceConfig { private static final Logger log LoggerFactory.getLogger(ControllerAdviceConfig.class); ExceptionHandler(Exception.class) public ResponseEntityObject 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 方法中Javapublic 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 层也加保护JavaGetMapping(/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 日志噪声可选YAMLlogging: level: org.apache.catalina.connector.ClientAbortException: WARNAOP 注意你的 CommonWrapAop 也在包裹视频接口如果里面有异常处理也需要做类似判断。请把下面代码贴给我我帮你精准修改ControllerAdviceConfig.java 的完整异常处理器代码TaskInfoService.java 中 videoPlayRangeOss 方法的关键部分尤其是写流的那一段这样我可以给你直接可复制的修复代码。