背景:心血来潮想用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.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);
}