Spring 大文件下载(前端视频播放)优化

文件是存在对象存储 MinIO 上面的,这里是视频文件,前端页面需要播放。
Java后端开发一个文件下载接口,这里没有直接提供minio的地址。问题是前端播放器播放视频的时候,会把整个视频下载完毕,播放也不能拖拽播放,只能从头到尾顺序播放。
用户体验很差

优化后。我们解析请求headerRange实现分片下载。这样就能实现拖拽播放,不用将文件一次性全部下载完毕。

@GetMapping("/download")
    public void download(HttpServletResponse res, HttpServletRequest request, @RequestParam("fileName") String fileName) {
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(BUCKETNAME)
                .object(fileName).build();
        StatObjectArgs statObjectArgs = StatObjectArgs.builder().bucket(BUCKETNAME).object(fileName).build();

        long fileSize;
        try {
            fileSize = minioClient.statObject(statObjectArgs).size();
        } catch (Exception e) {
            log.error("文件下载失败:{}", fileName, e);
            return;
        }

        String range = request.getHeader("Range");
        if(range != null && range.startsWith("bytes=")) {
            try {
                int idx = range.indexOf("-");
                long start = Long.parseLong(range.substring("bytes=".length(), idx));
                long end = minioClient.statObject(statObjectArgs).size() - 1;
                if (idx + 1 < range.length()) {
                    end = Long.parseLong(range.substring(idx + 1));
                }
                long length = end - start + 1;
                InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(BUCKETNAME).object(fileName).offset(start).length(length).build());
                res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                res.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
                res.setContentType("application/octet-stream");
                res.setHeader("Content-Disposition", "attachment; filename=" + FileUtil.getName(fileName));
                res.setContentLength((int)length);
                try {
                    IOUtils.copy(inputStream, res.getOutputStream());
                    inputStream.close();
                    res.flushBuffer();
                } catch (ClientAbortException e) {
                    // pass
                }
            } catch (Exception e) {
                log.error("断点下载失败:{}", fileName, e);
            }
        } else {
            try (InputStream is = minioClient.getObject(objectArgs)) {
                res.setContentType("application/octet-stream");
                res.addHeader("Content-Disposition", "attachment;filename=" + FileUtil.getName(fileName));
                res.setContentLength((int)fileSize);
                IOUtils.copy(is, res.getOutputStream());
                res.flushBuffer();
            } catch (Exception e) {
                log.error("文件下载失败:{}", fileName, e);
            }
        }
    }

优化前代码

@GetMapping("/download")
public void download(HttpServletResponse res, @RequestParam("fileName") String fileName) {
        GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(BUCKETNAME)
                .object(fileName).build();
        try (GetObjectResponse response = minioClient.getObject(objectArgs)){
            byte[] buf = new byte[1024];
            int len;
            try (FastByteArrayOutputStream os = new FastByteArrayOutputStream()){
                while ((len=response.read(buf))!=-1){
                    os.write(buf,0,len);
                }
                os.flush();
                byte[] bytes = os.toByteArray();
                res.setCharacterEncoding("utf-8");
                res.setContentType("application/octet-stream");
                res.setContentLength(bytes.length);
                res.addHeader("Content-Disposition", "attachment;filename=" + FileUtil.getName(fileName));
                try (ServletOutputStream stream = res.getOutputStream()){
                    stream.write(bytes);
                    stream.flush();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }