Springboot+Minio通过分片下载解决IOS下H5无法播放视频问题

一、环境说明

  • JDK 1.8
  • Springboot 2.7.5
  • Minio 8.4.5
  • Vue3实现的微信公众号网页

二、问题描述

当前项目是基于springboot和vue3的前后端分离架构,前端目前主要是基于H5展示在微信公众号的网页中。在实现视频上传、在线播放时遇到问题:前端同事说苹果手机播放不了视频,刚开始是统一用的video标签,安卓可以正常播放,但是苹果手机就出现“视频播放失败”。前端同事尝试换过video.js、vue3-play、html5 api、avplay、mui-player,都无法解决该问题,于是开始尝试后端寻找解决方案。

三、后端解决思路

第一次,是尝试将视频请求的Content-Disposition由attachment;filename=**改成inline;filename=**,这样视频请求可以直接在浏览器播放,而不是下载。但是依旧没有解决苹果手机视频播放失败的问题。并尝试《iOS无法播放MP4视频文件的解决方案 mp4视频iphone播放不了怎么办》在nginx中加入“add_header Accept-Ranges bytes;”,未能解决该问题。

第二次,分析网上找的视频,将该视频嵌入video是可以在苹果手机播放的。通过观察该请求,发现是有206的响应码,开始研究断点下载,最终在《05.springboot使用minio实现分段下载》《H5 Video播放视频iOS 断点下载处理》两篇文章的启发下,通过minio分段下载解决了该问题。

四、关键知识点

对于下载请求的响应需要包含以下几个特殊属性:

Accept-Ranges: bytes          接收字节请求
Content-Length: 2                 相应长度
Content-Range: bytes 0-1/18494715    bytes后面的空格不能少,0开始位置,1结束位置。“/”后面的是文件总大小长度

Content-Disposition     inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名

HttpResponse

Status Code: 206 Partial Content      指示请求已成功并且主体包含所请求的数据范围

五、后端下载方法代码片段

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