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