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-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); // 其他真实异常才往上抛
        }

视频流接口异常处理

这是一个典型的视频流接口异常处理不当导致的连锁问题:

核心错误链

  1. videoPlayRangeOss 方法在往客户端写视频数据时,客户端突然断开连接(Broken pipe)。
  2. 这触发了 ClientAbortException。
  3. 全局异常处理器(ControllerAdviceConfig#runTimeExceptionHandler)试图返回 BaseResponse(JSON)。
  4. 但此时响应头已经是 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);
    }
}

额外建议

  1. 抑制 Broken Pipe 日志噪声(可选):

    YAML

    logging:
      level:
        org.apache.catalina.connector.ClientAbortException: WARN
  2. AOP 注意:你的 CommonWrapAop 也在包裹视频接口,如果里面有异常处理,也需要做类似判断。

请把下面代码贴给我,我帮你精准修改:

  • ControllerAdviceConfig.java 的完整异常处理器代码
  • TaskInfoService.java 中 videoPlayRangeOss 方法的关键部分(尤其是写流的那一段)

这样我可以给你直接可复制的修复代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值