蘋果手機H5 video標籤播放影片問題以及.mov格式處理方案

夏威夷8080發表於2023-03-08

最近在做一個手機端拍照上傳,並預覽檔案的功能,前端用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));
        }
    }
}

 

相關文章