最近遇到奇葩问题,苹果手机公众号和h5网页中视频无法播放,在网络中找寻了好多解决方案,但还是没能彻底解决。
出现这个问题网上反馈多数因为两个情况,一、视频输出流问题;二、视频格式问题;围绕这两个点展开处理。
首先解决视频流输出问题,本次项目采用的java,springboot方式,项目默认结构为文件存储服务器本地,通过转换读取方式,直接访问文件地址即可获取。之后再网上找寻了很多输出视频流方式的例子;
这里将我们使用的例子代码贴出,仅供参考,具体可根据项目需求调整;其核心的断点流传输工具类可不同改动。
fileUpload.path: D:/fileUpload/
fileServic.path: http://192.168.2.198:8069/filestatic/
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;@Slf4j
@RestController
public class CommonController {/*** 请求访问域名地址*/@Value("${fileServic.path}")private String fileServicPath;/*** 文件存储物理路径*/@Value("${fileUpload.path}")private String fileUploadPath;/*** 获取视频** @param request* @param response*/@GetMapping("/filestatic/{date}/{fileName}")public void getPlayResource(HttpServletRequest request, HttpServletResponse response,@PathVariable(name = "date") String date,@PathVariable(name = "fileName") String fileName) {String rangeString = request.getHeader(HttpHeaders.RANGE);log.info("RANGE================,{}", rangeString);fileName = fileUploadPath + "/" + date + "/" + fileName;if (StringUtils.isNotEmpty(fileName)) {if (fileName.indexOf("mp4") > -1) {play(fileName,request,response);} else {try {writeBytes(fileName, response.getOutputStream());} catch (IOException e) {log.error("下载文件失败", e);}}}}/*** 非视频类文件预览加载** @param filePath* @param os* @throws IOException*/public void writeBytes(String filePath, OutputStream os) throws IOException {FileInputStream fis = null;try {File file = new File(filePath);if (!file.exists()) {throw new FileNotFoundException(filePath);}fis = new FileInputStream(file);byte[] b = new byte[1024];int length;while ((length = fis.read(b)) > 0) {os.write(b, 0, length);}} catch (IOException e) {throw e;} finally {if (os != null) {try {os.close();} catch (IOException e1) {e1.printStackTrace();}}if (fis != null) {try {fis.close();} catch (IOException e1) {e1.printStackTrace();}}}}/*** 下载视频文件 path为本地文件路劲** @param path* @param request* @param response*/public void play(String path, HttpServletRequest request, HttpServletResponse response) {RandomAccessFile targetFile = null;OutputStream outputStream = null;try {outputStream = response.getOutputStream();response.reset();//获取请求头中Range的值String rangeString = request.getHeader(HttpHeaders.RANGE);//打开文件File file = new File(path);if (file.exists()) {//使用RandomAccessFile读取文件targetFile = new RandomAccessFile(file, "r");long fileLength = targetFile.length();long requestSize = (int) fileLength;//分段下载视频if (StringUtils.isNotEmpty(rangeString)) {//从Range中提取需要获取数据的开始和结束位置long requestStart = 0, requestEnd = 0;String[] ranges = rangeString.split("=");if (ranges.length > 1) {String[] rangeDatas = ranges[1].split("-");requestStart = Integer.parseInt(rangeDatas[0]);if (rangeDatas.length > 1) {requestEnd = Integer.parseInt(rangeDatas[1]);}}if (requestEnd != 0 && requestEnd > requestStart) {requestSize = requestEnd - requestStart + 1;}//根据协议设置请求头response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");response.setHeader(HttpHeaders.CONTENT_TYPE, "video/mp4");long length;if (requestEnd > 0) {length = requestEnd - requestStart + 1;response.setHeader(HttpHeaders.CONTENT_LENGTH, "" + length);response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + requestStart + "-" + requestEnd + "/" + fileLength);} else {length = fileLength - requestStart;response.setHeader(HttpHeaders.CONTENT_LENGTH, "" + length);response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + requestStart + "-" + (fileLength - 1) + "/"+ fileLength);}
// }boolean scPartialContent = true;//断点传输下载视频返回206//如果是第一次请求,不返回206if (ranges.length > 1) {String[] rangeDatas = ranges[1].split("-");requestStart = Integer.parseInt(rangeDatas[0]);if (rangeDatas.length > 1 && requestStart == 0 && Integer.parseInt(rangeDatas[1]) == 1) {
// requestEnd = Integer.parseInt(rangeDatas[1]);scPartialContent = false;log.info("第一次请求rangeString,{}", rangeString);}}if (scPartialContent) {log.info("不是第一次请求rangeString,{}", rangeString);response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);}//设置targetFile,从自定义位置开始读取数据targetFile.seek(requestStart);} else {//如果Range为空则下载整个视频response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=test.mp4");//设置文件长度response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileLength));}//从磁盘读取数据流返回byte[] cache = new byte[4096];try {while (requestSize > 0) {int len = targetFile.read(cache);if (requestSize < cache.length) {outputStream.write(cache, 0, (int) requestSize);} else {outputStream.write(cache, 0, len);if (len < cache.length) {break;}}requestSize -= cache.length;}} catch (IOException e) {// tomcat原话。写操作IO异常几乎总是由于客户端主动关闭连接导致,所以直接吃掉异常打日志//比如使用video播放视频时经常会发送Range为0- 的范围只是为了获取视频大小,之后就中断连接了log.info(e.getMessage());}} else {
// throw new RuntimeException("文件路劲有误");}outputStream.flush();} catch (Exception e) {log.error("文件传输错误", e);
// throw new RuntimeException("文件传输错误");} finally {if (outputStream != null) {try {outputStream.close();} catch (IOException e) {log.error("流释放错误", e);}}if (targetFile != null) {try {targetFile.close();} catch (IOException e) {log.error("文件流释放错误", e);}}}}}
视频格式问题,参考地址https://zhuanlan.zhihu.com/p/532430872
我们的对比发下,上传的视频帧速率为25帧/秒无法播放,帧速率为30帧/秒可以正常。这和上述链接中讲解的苹果对视频帧数支持和格式说明有关系。
于是我们使用格式工厂(视频处理工具,免费的),进行转换后上传,真的可以正常播放了。到此问题全部解决。