苹果手机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); }