最近遇到奇葩问题,苹果手机公众号和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
//如果是第一次请求,不返回206
if (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帧/秒可以正常。这和上述链接中讲解的苹果对视频帧数支持和格式说明有关系。
于是我们使用格式工厂(视频处理工具,免费的),进行转换后上传,真的可以正常播放了。到此问题全部解决。