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

时间:2021-08-12 00:43:50

最近在做一个手机端拍照上传,并预览文件的功能,前端用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));
        }
    }
}