Springboot+Minio는 조각난 다운로드를 통해 H5가 IOS에서 비디오를 재생할 수 없는 문제를 해결합니다.

1. 환경 설명

  • JDK 1.8
  • 스프링부트 2.7.5
  • 미니언 8.4.5
  • Vue3로 구현된 WeChat 공식 계정 웹 페이지

2. 문제 설명

현재 프로젝트는 springboot와 vue3의 프론트엔드와 백엔드 분리 아키텍처를 기반으로 하고 있으며, 프론트엔드는 현재 주로 H5 기반의 WeChat 공식 계정 웹 페이지에 표시됩니다. 비디오 업로드 및 온라인 재생을 구현할 때 문제가 발생했습니다. 프런트 엔드 동료는 iPhone에서 비디오를 재생할 수 없다고 말했습니다. 처음에는 비디오 태그가 균일하게 사용되었습니다. Android는 정상적으로 재생할 수 있지만 "비디오 재생에 실패했습니다. "가 iPhone에 나타났습니다. 프론트엔드 동료들은 video.js, vue3-play, html5 api, avplay, mui-player를 바꾸려고 노력했지만 문제를 해결할 수 없었기 때문에 해결책을 찾기 위해 백엔드를 시도하기 시작했습니다.

3. 백엔드 솔루션

처음으로 비디오 요청의 Content-Disposition을 attachment;filename=**에서 inline;filename=**으로 변경하여 비디오 요청을 다운로드하는 대신 브라우저에서 직접 재생할 수 있도록 하십시오. 그러나 Apple 휴대폰에서 비디오 재생 실패 문제는 여전히 해결되지 않았습니다. 그리고 " Solution for iOS Cannot Play MP4 Video Files" What to do if mp4 video can't play on iphone " 에서 nginx에 "add_header Accept-Ranges bytes;"를 추가해봐도 문제가 해결되지 않습니다.

두 번째로 인터넷에서 찾은 동영상을 분석해 보니 동영상에 내장된 동영상이 애플 휴대폰에서도 재생이 가능하다는 사실을 발견했다. 요청을 관찰하여 응답 코드 206이 있음을 발견하고 중단점 다운로드를 연구하기 시작했으며 마침내 "05.springboot는 minio를 사용하여 세그먼트화된 다운로드를 실현합니다""H5 비디오 재생 비디오 iOS 중단점" 이라는 두 기사에서 영감을 얻었습니다. 다운로드 처리" , minio 분할 다운로드로 문제를 해결했습니다.

4. 주요 지식 포인트

다운로드 요청에 대한 응답에는 다음과 같은 특수 속성이 포함되어야 합니다.

Accept-Ranges: 바이트 요청 수신 바이트
Content-Length: 2 해당 길이
Content-Range: 바이트 0-1/18494715 바이트 뒤의 공간은 작아서는 안 됩니다. 0 시작 위치, 1 끝 위치. "/" 다음에 총 파일 크기 길이가 옵니다.

Content-Disposition 인라인은 브라우저 직접 사용을 의미하고, 첨부 파일은 다운로드를 의미하며, fileName은 다운로드한 파일 이름을 의미합니다.

HttpResponse

상태 코드: 206 부분 콘텐츠는 요청이 성공했으며 본문에 요청된 데이터 범위가 포함되어 있음을 나타냅니다.

5. 백엔드 다운로드 방법의 코드 스니펫

public void downloadSlice(String bucketName, String filename, HttpServletResponse response,
                              HttpServletRequest request) throws Exception{
        if (StringUtils.isNotBlank(filename)) {
            String range = request.getHeader("Range");
            //获取文件信息
            StatObjectResponse statObjectResponse = minioClient.statObject(
                StatObjectArgs.builder().bucket(bucketName).object(filename).build());
            //开始下载位置
            long startByte = 0;
            //结束下载位置
            long endByte = statObjectResponse.size() - 1;

            //有range的话
            if (StringUtils.isNotBlank(range) && range.contains("bytes=") && range.contains("-")) {
                range = range.substring(range.lastIndexOf("=") + 1).trim();
                String[] ranges = range.split("-");
                try {
                    //判断range的类型
                    if (ranges.length == 1) {
                        //类型一:bytes=-2343
                        if (range.startsWith("-")) {
                            endByte = Long.parseLong(ranges[0]);
                        }
                        //类型二:bytes=2343-
                        else if (range.endsWith("-")) {
                            startByte = Long.parseLong(ranges[0]);
                        }
                    }
                    //类型三:bytes=22-2343
                    else if (ranges.length == 2) {
                        startByte = Long.parseLong(ranges[0]);
                        endByte = Long.parseLong(ranges[1]);
                    }

                } catch (NumberFormatException e) {
                    startByte = 0;
                    endByte = statObjectResponse.size() - 1;
                }
            }

            //要下载的长度
            long contentLength = endByte - startByte + 1;
            //文件类型
            String contentType = request.getServletContext().getMimeType(filename);

            //解决下载文件时文件名乱码问题
            byte[] fileNameBytes = filename.getBytes(StandardCharsets.UTF_8);
            filename = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1);

            //各种响应头设置
            //支持断点续传,获取部分字节内容:
            response.setHeader("Accept-Ranges", "bytes");
            //http状态码要为206:表示获取部分内容
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            response.setContentType(contentType);
            response.setHeader("Last-Modified", statObjectResponse.lastModified().toString());
            //inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名
            response.setHeader("Content-Disposition", "inline;filename=" + filename);
            response.setHeader("Content-Length", String.valueOf(contentLength));
            //Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
            response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + statObjectResponse.size());
            response.setHeader("ETag", "\"".concat(statObjectResponse.etag()).concat("\""));

            try {
                GetObjectResponse stream = minioClient.getObject(
                    GetObjectArgs.builder()
                        .bucket(statObjectResponse.bucket())
                        .object(statObjectResponse.object())
                        .offset(startByte)
                        .length(contentLength)
                        .build());
                BufferedOutputStream os = new BufferedOutputStream(response.getOutputStream());
                byte[] buffer = new byte[1024];
                int len;
                while ((len = stream.read(buffer)) != -1) {
                    os.write(buffer, 0, len);
                }
                os.flush();
                os.close();
                response.flushBuffer();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

추천

출처blog.csdn.net/yangfande362/article/details/129416339