苹果浏览器无法直接播放MP4(谷歌浏览器可以)

一、问题提出
项目开发过程中遇到一个问题:
>基于webkit内核的浏览器H5的Video标签(获取android手机,一般也是webkit浏览器)可以正常播放MP4文件,但是基于苹果操作系统的safari浏览器或者苹果微信小程序内置浏览器都无法播放远程后台的MP4文件。

发现问题:
为了能发现android端与IOS端微信小程序内置浏览器的不同,通过对比两个浏览器发送给后台的包,可以发现如下端倪:  

android浏览器:苹果浏览器无法直接播放MP4(谷歌浏览器可以)_第1张图片

 

苹果浏览器:  

苹果浏览器无法直接播放MP4(谷歌浏览器可以)_第2张图片

对比之后发现没有什么区别,最后发现问题并不是没有区别,而是真实的IOS系统或者IOS微信公众号内置浏览器发出来的包与上述IOS模拟器发出来的请求时不一样的!为了还原真相,我特意搭建了一个“黑苹果操作系统”模拟IOS浏览器发出的请求。得到如下结果:   

苹果浏览器无法直接播放MP4(谷歌浏览器可以)_第3张图片

 

以上可以看到android或webkit与IOS苹果浏览器播放的区别:Range字段,通过查询可以知道两者区别在于:
> android或webkit播放文件是一次请求到所有的数据,然后下载后进行播放(这对于移动手机来说会消耗很大流量),苹果针对这个问题进行了改进,所以才有了分段请求数据的问题,也就是我们常说的http1.1中的断点续传。

 

二、问题解决

知道两者的区别之后,我们其实在后台支持两种请求协议即可,一种是不包含Range的请求,一种是包含Range的分段请求:

我后台节后如下:
```
    @ApiOperation("文件下载")
    @GetMapping("/download")
    @ApiImplicitParams({@ApiImplicitParam(paramType = "query", dataType = "String", name = "path", value = "文件路径", required = true)})
    public void downLoad(@RequestParam(value="path", required=true) String path,
                         @RequestHeader(value="range", required=false) String range,
                         HttpServletRequest request,
                         HttpServletResponse response) throws IOException {
        try {
            if (CheckUtil.isNull(path)) {
                response.sendError(-1, "参数不合法");
            }
            printHeaders(request);

            // 端点续传:如果是苹果是分段请求,如果是android或webkit则直接下载整个文件
            int start = 0;
            int end = -1;
            if (!CheckUtil.isNull(range)){
                // bytes=0-1 or bytes=0-
                String v = range.trim().split("=")[1];
                String[] range_size = v.split("-");
                if (range_size.length >= 1) {
                    start = Integer.valueOf(range_size[0]);
                }
                if (range_size.length >= 2) {
                    end = Integer.valueOf(range_size[1]);
                }
            }

            System.out.println("start:" + start + " end:" + end);
            if (path.startsWith("group")) {
                downloadFromFDFS(path, start, end, response);
            } else {
                downloadFromHDFS(path, start, end, response);
            }
        } catch (Exception e) {
            log.error("下载文件出错:{}", e.getMessage());
        }
    }
```

由于我后台的数据存储包括两种方式:基于FastDFS的图片存储和基于HDFS的大文件(如视频附件等)存储,这里的视频主要就存储在HDFS中,接口中我们主要分析了请求头参数Range,得到请求的数据的start和请求结束end,如果包含Range我们就发送range指定的内容给前端,如果没有指定我们就发送0-end的所有文件数据给前端(一次性),所以我们主要看HDFS下载接口即可:

```
    /**
     * @功能描述: 从HDFS中下载文件
     * @编写作者: [email protected]
     * @开发日期: 2020年4月4日
     * @历史版本: V1.0  
     * @参数说明:
     */
    private boolean downloadFromHDFS(String path,int start, int end, HttpServletResponse response) {
        String fileName = path.substring(path.lastIndexOf('/')+1);
        String extName = FilenameUtils.getExtension(fileName);
        
        // 创建文件
        HdfsProxy hdfsProxy = new HdfsProxy(HadoopConfig);
        hdfsProxy.open();
        
        // 写文件
        ServletOutputStream out = null;
        FSDataInputStream in = null;

        try {
            // 获取输出流
            out = response.getOutputStream();
            // 设置相应类型application/octet-stream(注:applicatoin/octet-stream 为通用,一些其它的类型苹果浏览器下载内容可能为空)
            response.setContentType(getContentTypeByExtName(extName));
            // 设置头信息 Content-Disposition为属性名  附件形式打开下载文件   指定名称  为 设定的fileName
            //response.setHeader("Content-Disposition", "attachment;inline;filename=" + URLEncoder.encode(fileName, "UTF-8"));
            response.setHeader("Content-Disposition", "filename=" + URLEncoder.encode(fileName, "UTF-8"));
            response.setHeader("Accept-Ranges", "bytes");

            long size = hdfsProxy.getFileSize(path);
            in = hdfsProxy.Open(path);
            if (null == in){
                return false;
            }

            // webkit可以不设置文件大小
            int need = 1024*1024;
            if (end > 0){
                need = end - start + 1;
                response.setHeader("Content-Length", String.valueOf(need));
            } else {
                response.setHeader("Content-Length", String.valueOf(size));
            }

            byte buffer[] = new byte[need];
            in.seek(start);
            int total = 0;
            boolean toFileEnd = false;
            while (true) {
                int read = in.read(buffer);
                if (read <= 0) {
                    toFileEnd = true;
                    break;
                }
                out.write(buffer, 0, read);
                total += read;
                if (end > 0 && total >= end + 1) {
                    break;
                }
            }

            // 苹果分段请求(HTTP续传方式)
            if (end > 0){
                // 未达到文件末尾
                if(!toFileEnd){
                    response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
                }
                String value = String.format("bytes %d-%d/%d", start, end, size);
                response.setHeader("Content-Range", value);
            }

            /*
            // 从HDFS中下载文件
            ServletOutputStream out2 = out;
            status = hdfsProxy.download(path, new ReadProgress() {
                @Override
                public void progress(byte[] buffer, long size) {
                    try {
                        out2.write(buffer, 0, (int)size);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
            */
        } catch (Exception e) {
            log.error("读取HDFS文件文件异常:{}", e.getMessage());
        } finally {
            if(null != out) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (in != null) {
                hdfsProxy.close(in);
            }
            hdfsProxy.close();
        }

        return false;
    }
```

从以上代码中我们可以看到如果是苹果的分段请求,我们需要注意一下几点:
- 我们在回复的请求头中多了“Content-Range”字段,表名本次请求我回复的内容大小以及数据的**总长度**(这个很关键,苹果浏览器分段请求前发送的分段请求为0-1,目的就是探测到文件的总长度以便后续进行播放控制)
- 分段请求回复的数据为真实的分段数据,如请求10-20的分段,我们直接定位到文件偏移量为10的位置然后发送20-10+1=11个字节数据
- Content-Length指定为本次真实发送的数据长度
- 如果分段请求没有到文件末尾,我们回复的http状态码为206(表示只返回部分数据),如果最终读取达到文件末尾则http状态码返回200(默认)

经过以上修改后,后端代码就支持android和苹果浏览器视频播放了!


技术交流群:961179337
微信交流:lixiang1653
邮箱:[email protected]

你可能感兴趣的:(spring,cloud,Java,边下边播)