• 苹果手机H5 video标签播放视频问题以及.mov格式处理方案


    最近在做一个手机端拍照上传,并预览文件的功能,前端用h5 video 标签,后端用springboot+minio。

    问题

    刚开始写代码和测试的时候,都是用的安卓手机,照片和视频都没问题,后来换成用苹果手机后,播放视频就出现各种问题,先是苹果手机拍的mov视频不支持播放,后面又出现苹果手机播放不了视频,应该是ios浏览器不兼容video标签。

    后来网上找了半天,终于找到了解决方案。

    解决方案

    iOS上播放视频,http协议中应用rang请求头。

    视频格式MP4是正确的,但是你的后台没有对ios的视频播放器做适配。如果想要在iOS上播放视频,那么必须在http协议中应用rang请求头。
    对于有的朋友还对ios播放器http的range标记不是很懂。我再讲解下。

    视频文件总长度是123456789
    range是播放器要求的区间也就是客户端发送请求的时候http会带有这个标记,这个区间的值在http.headers.range中获取,一般是bytes=0-1这样的。

    我们需要做的处理是返回文件的指定区间(如上面的例子,我们就应该返回0到1的字符),并添加Content-Range:btyes 0-1、Accept-Ranges:bytes、'Content-Length: 123456789','Content-Type: video/mp4'到http.headers中

    代码

    下面是前后端代码,上传就用的minio sdk上传的,这个代码就不贴了。

    前端

    复制代码
    -webkit-playsinline="true"/*这个属性是ios 10中设置可以让视频在小窗内播放,即不全屏播放*/
    playsinline="true"/*I0s微信浏览器支持小窗内播放*/
    x-webkit-airplay="allow"/*使此视频支持ios的AirPlay功能*/
    x5-video-player-type="h5”/*启用H5播放器,是wechat?安卓版特性*/
    x5-video-player-fullscreen="true”/*全屏设置,设置为true是防止横屏*/>
    -->
    <video
        autoplay
        class="video"
        v-if="urlType === 'video'"
        :src="previewUrl"
        controls
        type="video/mp4"
        webkit-playsinline="true"
        playsinline="true"
        x5-playsinline="true"
        x-webkit-airplay="allow"
        x5-video-player-fullscreen="true"
        x5-video-player-type="h5"
    >video>
    复制代码

    后端

    复制代码
    /**
     * 分段下载
     *
     * @param bucket
     * @param fileName
     * @param response
     */
    @GetMapping("file/range/{bucket}/{fileName}")
    public void fileRangeIgnoreToken(@PathVariable String bucket, @PathVariable String fileName, HttpServletRequest request, HttpServletResponse response) {
        String property = System.getProperty("user.dir");
        String filePath = property + "/" + fileName;
        log.info("新生文件的路径:{}", filePath);
        File file = new File(filePath);
    
        InputStream inputStream = minioTemplate.getObject(bucket, fileName);
    
        try {
            FileUtils.copyInputStreamToFile(inputStream, file);
            this.rangeVideo(request, response, file, fileName);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("分段发送文件出错,失败原因:{}", Throwables.getStackTraceAsString(e));
        } finally {
            FileUtil.del(file);
        }
    }
    
    /**
     * 新增视频加载方法,解决ios系统vedio标签无法播放视频问题
     *
     * @param request
     * @param response
     * @param file
     * @param fileName
     * @throws FileNotFoundException
     * @throws IOException
     */
    public void rangeVideo(HttpServletRequest request, HttpServletResponse response, File file, String fileName) throws FileNotFoundException, IOException {
        RandomAccessFile randomFile = new RandomAccessFile(file, "r");//只读模式
        long contentLength = randomFile.length();
        log.info("获取导的contentLength={}", contentLength);
        String range = request.getHeader("Range");
        int start = 0, end = 0;
        if (range != null && range.startsWith("bytes=")) {
            String[] values = range.split("=")[1].split("-");
            start = Integer.parseInt(values[0]);
            if (values.length > 1) {
                end = Integer.parseInt(values[1]);
            }
        }
        int requestSize = 0;
        if (end != 0 && end > start) {
            requestSize = end - start + 1;
        } else {
            requestSize = Integer.MAX_VALUE;
        }
    
        response.setContentType("video/mp4");
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("ETag", fileName);
        response.setHeader("Last-Modified", new Date().toString());
        //第一次请求只返回content length来让客户端请求多次实际数据
        if (range == null) {
            response.setHeader("Content-length", contentLength + "");
        } else {
            //以后的多次以断点续传的方式来返回视频数据
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);//206
            long requestStart = 0, requestEnd = 0;
            String[] ranges = range.split("=");
            if (ranges.length > 1) {
                String[] rangeDatas = ranges[1].split("-");
                requestStart = Integer.parseInt(rangeDatas[0]);
                if (rangeDatas.length > 1) {
                    requestEnd = Integer.parseInt(rangeDatas[1]);
                }
            }
            long length = 0;
            if (requestEnd > 0) {
                length = requestEnd - requestStart + 1;
                response.setHeader("Content-length", "" + length);
                response.setHeader("Content-Range", "bytes " + requestStart + "-" + requestEnd + "/" + contentLength);
            } else {
                length = contentLength - requestStart;
                response.setHeader("Content-length", "" + length);
                response.setHeader("Content-Range", "bytes " + requestStart + "-" + (contentLength - 1) + "/" + contentLength);
            }
        }
        ServletOutputStream out = response.getOutputStream();
        int needSize = requestSize;
        randomFile.seek(start);
        while (needSize > 0) {
            byte[] buffer = new byte[4096];
            int len = randomFile.read(buffer);
            if (needSize < buffer.length) {
                out.write(buffer, 0, needSize);
            } else {
                out.write(buffer, 0, len);
                if (len < buffer.length) {
                    break;
                }
            }
            needSize -= buffer.length;
        }
        randomFile.close();
        out.close();
    }
    复制代码

    上面这段代码可以解决本文开头提到的两个问题。

    补充:.mov视频播放不了怎么解决?

    还有一种思路,也是我一开始的做法,就是先从文件服务器上读取视频文件,然后在后台强制把.mov转成.mp4输出。

    代码也贴下

    复制代码
    @GetMapping("file/{bucket}/{fileName}")
    @IgnoreUserToken
    @IgnoreClientToken
    public void fileIgnoreToken(@PathVariable String bucket, @PathVariable String fileName, HttpServletResponse response) {
        if (fileName.toLowerCase().contains(".mov")) {
            log.info("进入mov文件转码");
            File source = null;
            File target = null;
            try {
                InputStream inputStream = minioTemplate.getObject(bucket, fileName);
                source = new File("/" + IdUtil.simpleUUID() + ".mov");
                target = new File("/" + IdUtil.simpleUUID() + ".mp4");
                FileUtils.copyInputStreamToFile(inputStream, source);
    
                AudioAttributes audio = new AudioAttributes();
                audio.setCodec("libmp3lame");
                audio.setBitRate(new Integer(800000));//设置比特率
                audio.setChannels(new Integer(1));//设置音频通道数
                audio.setSamplingRate(new Integer(44100));//设置采样率
                VideoAttributes video = new VideoAttributes();
    //            video.setCodec("mpeg4");
                video.setCodec("libx264");
                video.setBitRate(new Integer(3200000));
                video.setFrameRate(new Integer(15));
                EncodingAttributes attrs = new EncodingAttributes();
                attrs.setOutputFormat("mp4");
                attrs.setAudioAttributes(audio);
                attrs.setVideoAttributes(video);
                Encoder encoder = new Encoder();
                encoder.encode(new MultimediaObject(source), target, attrs);
                IoUtil.copy(new FileInputStream(target), response.getOutputStream());
            } catch (Exception e) {
                log.error("转码文件出错,失败原因:{}", Throwables.getStackTraceAsString(e));
            } finally {
                log.info("删除临时文件");
                FileUtil.del(source);
                FileUtil.del(target);
            }
        } else {
            try {
                IoUtil.copy(minioTemplate.getObject(bucket, fileName), response.getOutputStream());
            } catch (IOException e) {
                log.error("读取文件出错,失败原因:{}", Throwables.getStackTraceAsString(e));
            }
        }
    }
    复制代码

     可能会出现的问题

    java.io.IOException: Connection reset by peer 或 IOException Broken pipe 问题记录。

     

     

     有的时候,播放视频时会出现get video error的问题,查看后台日志,提示 java.io.IOException: Connection reset by peer 或 IOException Broken pipe。

    这个问题,归纳下来的原因就是前端等待后台视频流处理的时间超长了,前端就关闭了等待,导致server端找不到client端。

    这个问题引起的原因有很多:

    1、nginx配置的缓存大小太小,或者超时时间设置太短。

    复制代码
    proxy_buffer_size 1024k;
    proxy_buffers 16 1024k;
    proxy_busy_buffers_size 2048k;
    proxy_temp_file_write_size 2048k;
    
    proxy_buffering off;
    proxy_send_timeout 1000;
    proxy_read_timeout 1000;
    复制代码

    2、视频文件太大,导致超时。

    我这边是由于上面的代码写的有问题,可以这样优化。

    复制代码
            String property = System.getProperty("user.dir");
            String filePath = property + "/" + fileName;
            log.info("新生文件的路径:{}", filePath);
            boolean exist = FileUtil.exist(filePath);
            File file = new File(filePath);
            try {
                if (exist == false) {
                    // 这步是关键,属于耗时操作
                    FileUtils.copyInputStreamToFile(minioTemplate.getObject(bucket, fileName), file);
                }
                this.rangeVideo(request, response, file, fileName);
            } catch (Exception e) {
                log.error("分段发送文件出错,失败原因:{}", Throwables.getStackTraceAsString(e));
            } finally {
                FileUtil.del(file);
            }
    复制代码

     

  • 相关阅读:
    HDR相关文章收集
    坚持与努力的背后:从校园到职场,我选择奋斗
    吴恩达机器学习课程笔记1-2
    四、Qt自定义UI界面(细节的使用)
    怎么在 CSDN 写好技术博客
    Mybatis01
    Java网络编程-IO模型篇之从BIO、NIO、AIO到内核select、epoll剖析!
    首尔伟傲世将在2022年IFA展会上展示其微型LED的非凡价值
    Go语言进阶篇,单元测试、基准测试的性能测试、内存占用测试
    LLM实战(二)| 使用ChatGPT API提取文本topic
  • 原文地址:https://www.cnblogs.com/shamo89/p/17191480.html