背景:心血来潮想用jsp调用后端接口播放视频,因为一直不成功历尽艰难,故有此记。
一、Java代码
public void executeDown() {
// 1-1.判断资源是否存在:
if (file == null || !file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);// 不存在直接返回404
return;
}
// 1-2.判断是否超出大小:(正常 and 系统配置最大 and 固定最大)
fileSize = file.length();
if (fileSize <= 0) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);// 大小有误,返回404
return;
}
if (fileSize > Math.min(FILE_MAXSIZE, maxSize)) {// 超出配置大小,表示无法处理了
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);// 返回503
return;
}
// 1-3.判断是否断点续传:
Long[] ranges = readRequestRange();
if (ranges == null) {
execute(); //另外一个直接回传的方法
return;
}
long startByte = ranges[0], endByte = ranges[1];
if (endByte == 0) {
execute();
return;
}
long time_log = System.currentTimeMillis();
// 1-4.提取访问日志相关:
fileSize = file.length();
StringBuffer logs = "这里是一段日志内容而已,如IP、访问地址、文件大小、耗时等";//getFormatLogs();
String state = "SUCCESS";
// 1-5.执行返回数据:
RandomAccessFile raf = null;
OutputStream outPut = null;
try {
response.reset();
response.resetBuffer();
// 通知客户端允许断点续传,响应格式为:Accept-Ranges: bytes
response.setHeader("Accept-Ranges", "bytes");
// 通知客户端使用的是断点续传协议,即返回206状态:
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
// 文件名
String fileName = file.getName();
// 文件类型
String contentType = StringUtils.toString(request.getServletContext().getMimeType(fileName), "application/octet-stream");
// 解决下载文件时文件名乱码问题
byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);
String downloadFileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1);
response.setHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
// 响应的格式是:
response.setContentType(contentType);
response.addHeader("Content-Length", String.valueOf(endByte - startByte + 1));
// 重点1啊 [要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader("Content-Range", "bytes " + startByte + "-" + (fileSize - 1) + "/" + fileSize);
outPut = new BufferedOutputStream(response.getOutputStream());
raf = new RandomAccessFile(file, "r");
// 跳过已下载字节
raf.seek(startByte);
endByte = Math.min(endByte, fileSize);
byte[] bs = new byte[1024];
while (true) {
long fp = raf.getFilePointer();
if (raf.read(bs) == -1)
break;
outPut.write(bs);
if (fp >= endByte)
break;
}
outPut.flush();
} catch (org.apache.catalina.connector.ClientAbortException e) {
// 捕获此异常表示拥护停止下载
state = "WARN:" + e.getMessage();
// e.printStackTrace();
} catch (Exception e) {
//("获取文件失败,编号100093,详细:", e);
state = "FAIL:" + e.getMessage();
} finally {
StreamUtils.closeCloseable(outPut);
StreamUtils.closeCloseable(raf);
}
// 1-9.写访问日志:
saveFormatLogs(new StringBuffer(logs.toString().replace("#STATE#", state).replace("#TIME#", System.currentTimeMillis() - time_log + "ms")));
}
private Long[] readRequestRange() {
String range = RequestUtils.getRequestHeaderByKey(request, "Range");
if (StringUtils.isBlank(range) || !range.contains("bytes=") || !range.contains("-"))
return null;
range = range.substring(range.lastIndexOf("=") + 1).trim();
String[] ranges = range.split("-");
// 判断range的类型
long startByte = 0, endByte = 0;
if (ranges.length == 1) {
if (range.startsWith("-")) { // 类型一:bytes=-2343
endByte = StringUtils.toLong(StringUtils.toString(ranges[0]).trim());
} else if (range.endsWith("-")) { // 类型二:bytes=2343-
startByte = StringUtils.toLong(StringUtils.toString(ranges[0]).trim());
endByte = startByte + 1024 * 512;
}
endByte = Math.min(fileSize, endByte);
} else if (ranges.length == 2) { // 类型三:bytes=22-2343
startByte = Long.parseLong(ranges[0]);
endByte = Long.parseLong(ranges[1]);
}
if (endByte == 0)
return null;
return new Long[] { startByte, endByte };
}
具体踩坑记录请查看文末《附件2:Java配合video标签断点续传踩坑记录》
二、HTML代码
HTML5中video元素详解
1、使用示例
- 最简单的:
<video class="ep-video" id="ep-video" controls="controls" autoplay
src="https://www.runoob.com/try/demo_source/mov_bbb.mp4"></video>
- 推荐使用:
<video class="none ep-video" controls autoplay>
<source src="http..." type="video/mp4">
<source src="http..." type="video/ogg">
<source src="http..." type="video/webm">
<object data="http..." width="320" height="360">
<embed src="http..." width="320" height="360">
</object>
<p>你的浏览器不支持 <code>video</code> 标签.</p>
</video>
- 附加的:
<video
controls
autoplay
loop
preload="auto"
poster="img/popup-img.png"
webkit-playsinline="true"
playsinline="true"
x5-video-player-type="h5"
x5-video-player-fullscreen="true"
x-webkit-airplay="allow"
x5-video-orientation="portraint"
style="object-fit:fill">
<!-- ...... -->
</video>
具体参见《附件1:video特殊参数说明》:
2、相关JS方法
- 获取对象(与js同,老猿跳过)
let video = document.querySelector('.player'); //class name is player
let video = document.getElementById('myvideo'); //id is myvideo
let video = $('.player')[0];
let video = $('#myvideo')[0];
- 监听事件
事件请查看《video标签所有事件》
video.addEventListener('error', myMethod);
video.onerror = myMethod;
使用jQuery:(新大陆)
var event = 'progress suspend abort error emptied stalled play pause loadedmetadata waiting canplay canplaythrough seeking seeked timeupdate ended ratechange druationchange volumechange';
$(video).bind(event, function(event) {
console.log(event.type); //type 是指触发了哪个事件
if (event.type == 'error') {
console.log(event);
console.log(video.error);
//PIPELINE_ERROR_READ: FFmpegDemuxer: data source error
}
});
八、附件1:video特殊参数说明
属性 | 说明 |
controls | 显示标准的 HTML5 视频/音频播放器控制条、控制按钮。 |
autoplay | 让文件自动播放。 |
loop | 让文件循环播放。 |
preload | 属性是用来缓存大体积文件的。它有三个可选值:“none” 不缓存、“auto” 缓存、“metadata” 只缓存文件元信息 |
poster | 视频封面 |
webkit-playsinlin=“true” | 这个属性在 ios 10中设置有用,其他的目前还不起作用,让视频在小窗内播放,也就是不是全屏播放 |
playsinline=“true” | IOS微信浏览器支持小窗内播放 |
x5-video-player-type=“h5” | 启用H5播放器,是wechat安卓版特性 |
x5-video-player-fullscreen=“true” | 全屏设置,设置为 true 是防止横屏 |
x5-video-orientation=“portraint” | 播放器屏幕的方向,landscape横屏,portraint竖屏,默认值为竖屏。 |
source | 标签是为了能够兼容各种浏览器对不同媒体类型的支持,我们可以用多个source元素来提供多个不同的媒体类型。支持mp4格式视频流的浏览器可以播放mp4文件,如果不支持,可以播放ogg文件。 |
codecs=dirac, speex | 是用来指定播放使用的解码器(codecs); 这样就可以更精确的让浏览器如何播放提供的视频。 |
九、附件2:Java配合video标签断点续传踩坑记录
1、笔者踩坑
搜索了好多文章,都是说用 startByte - endByte ,结果看了别的网络请求的数据才发现,end应该是size-1,一试还真的是。
之前以为各种header或Access的问题,导致浪费了两三天,emmm…
- 错误写法:
// [要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + fileSize);
- 正确写法:
response.setHeader("Content-Range", "bytes " + startByte + "-" + (fileSize - 1) + "/" + fileSize);
原理也没去查,也没搞明白,反正size-1是正确的,啊哈~
2、网友踩坑
看文章发现其他网友还有以下踩坑:
- 没有传headers:
// 通知客户端允许断点续传,响应格式为:Accept-Ranges: bytes
response.setHeader("Accept-Ranges", "bytes");
// 通知客户端使用的是断点续传协议,即返回206状态:
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
- 没有传标识:
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 响应的格式是:
response.setContentType(contentType);
response.addHeader("Content-Length", String.valueOf(endByte - startByte + 1));
// [要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader("Content-Range", "bytes " + startByte + "-" + (fileSize - 1) + "/" + fileSize);
3、附文件大众读法
int b;
while (raf.getFilePointer() < endByte) {
if ((b = raf.read()) == -1)
continue;
outPut.write(b);
}