前端封装的视频组件调用视频接口时,不支持进度条快进后退,每次都是从头开始播放,视频播放接口如下:
**
* @author wb
* @create_time 2020-03-09
* @description 视频播放测试接口
* @param response
* @throws IOException
*/
@ApiOperation(httpMethod = "GET",value = "视频播放测试接口" ,notes = "视频播放测试接口")
@RequestMapping(value = "getVideo", method = RequestMethod.GET)
public void getVideo(HttpServletResponse response) throws IOException {
String path = ClassUtils.getDefaultClassLoader().getResource("static/video").getPath();
File file = new File(path+"/three.wmv" );
response.setContentType("video/mp4");
response.setHeader("Accept-Ranges", "bytes");
try (InputStream in = new FileInputStream(file); ServletOutputStream out = response.getOutputStream();) {
int length;
byte[] buffer = new byte[4 * 1024];
// 向前台输出视频流
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
} catch (FileNotFoundException e) {
System.out.println("文件读取失败, 文件不存在");
}
}
后来研究发现视频播放接口使用断点续传,可实现进度条快进后退播放,代码如下:
/**
* @author wb
* @create_time 2020-03-10
* @description 视频断点续传播放测试接口
* @param request
* @param response
*/
@ApiOperation(httpMethod = "GET",value = "视频断点续传播放测试接口" ,notes = "视频断点续传播放测试接口")
@RequestMapping(value = "/player", method = RequestMethod.GET)
public void player2(HttpServletRequest request, HttpServletResponse response) {
String path = ClassUtils.getDefaultClassLoader().getResource("static/video").getPath()+"/three.wmv" ;
BufferedInputStream bis = null;
try {
File file = new File(path);
if (file.exists()) {
long p = 0L;
long toLength = 0L;
long contentLength = 0L;
int rangeSwitch = 0; // 0,从头开始的全文下载;1,从某字节开始的(bytes=27000-);2,从某字节开始到某字节结束的(bytes=27000-39000)
long fileLength;
String rangBytes = "";
fileLength = file.length();
// get file content
InputStream ins = new FileInputStream(file);
bis = new BufferedInputStream(ins);
// tell the client to allow accept-ranges
response.reset();
//支持断点续传
response.setHeader("Accept-Ranges", "bytes");
// client requests a file block download start byte
String range = request.getHeader("Range");
if (range != null && range.trim().length() > 0 && !"null".equals(range)) {
// 200是OK(一切正常),206是Partial Content(服务器已经成功处理了部分内容),
// 416 Requested Range Not Satisfiable(对方(客户端)发来的Range 请求头不合理)。
response.setStatus(javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT);
rangBytes = range.replaceAll("bytes=", "");
if (rangBytes.endsWith("-")) { // bytes=270000-
rangeSwitch = 1;
p = Long.parseLong(rangBytes.substring(0, rangBytes.indexOf("-")));
contentLength = fileLength - p; // 客户端请求的是270000之后的字节(包括bytes下标索引为270000的字节)
} else { // bytes=270000-320000
rangeSwitch = 2;
String temp1 = rangBytes.substring(0, rangBytes.indexOf("-"));
String temp2 = rangBytes.substring(rangBytes.indexOf("-") + 1, rangBytes.length());
p = Long.parseLong(temp1);
toLength = Long.parseLong(temp2);
contentLength = toLength - p + 1; // 客户端请求的是 270000-320000 之间的字节
}
} else {
contentLength = fileLength;
}
// 如果设设置了Content-Length,则客户端会自动进行多线程下载。如果不希望支持多线程,则不要设置这个参数。
// Content-Length: [文件的总大小] - [客户端请求的下载的文件块的开始字节]
response.setHeader("Content-Length", new Long(contentLength).toString());
// 断点开始
// 响应的格式是:
// Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小]
if (rangeSwitch == 1) {
String contentRange = new StringBuffer("bytes ").append(new Long(p).toString()).append("-")
.append(new Long(fileLength - 1).toString()).append("/")
.append(new Long(fileLength).toString()).toString();
response.setHeader("Content-Range", contentRange);
bis.skip(p);
} else if (rangeSwitch == 2) {
String contentRange = range.replace("=", " ") + "/" + new Long(fileLength).toString();
response.setHeader("Content-Range", contentRange);
bis.skip(p);
} else {
String contentRange = new StringBuffer("bytes ").append("0-").append(fileLength - 1).append("/")
.append(fileLength).toString();
response.setHeader("Content-Range", contentRange);
}
response.setContentType("video/mp4");
// String fileName = file.getName();
// response.setContentType("application/octet-stream");
// response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
OutputStream out = response.getOutputStream();
int n = 0;
long readLength = 0;
int bsize = 1024;
byte[] bytes = new byte[bsize];
if (rangeSwitch == 2) {
// 针对 bytes=27000-39000 的请求,从27000开始写数据
while (readLength <= contentLength - bsize) {
n = bis.read(bytes);
readLength += n;
out.write(bytes, 0, n);
}
if (readLength <= contentLength) {
n = bis.read(bytes, 0, (int) (contentLength - readLength));
out.write(bytes, 0, n);
}
} else {
while ((n = bis.read(bytes)) != -1) {
out.write(bytes, 0, n);
}
}
out.flush();
out.close();
bis.close();
}
} catch (IOException ie) {
// 忽略 ClientAbortException 之类的异常
} catch (Exception e) {
e.printStackTrace();
}
}
什么是断点续传?
断点续传其实正如字面意思,就是在下载的断开点继续开始传输,不用再从头开始。所以理解断点续传的核心后,发现其实和很简单,关键就在于对传输中断点的把握。
在HTTP/1.1协议没出的时候,也就是HTTP/1.0协议,这种协议不可以使用长链接和断点续传和其他新特性;自从这个1.1被广大使用的现在,很多的下载器都被支持断点续传。
断点续传也就是从下载断开的哪里,重新接着下载,直到下载完整/可用。如果要使用这种断点续传,4个HTTP头不可少的,分别是Range头、Content-Range头、Accept-Ranges头、Content-Length头。这里我讲的是服务端,其中要用Range头是因为它是客户端发过来的信息。服务端是响应,而客户端(浏览器)是请求。
Range头必须要了解它,否则没法解析。请求中会带过来的断点信息,一般三种格式。
Range : bytes=50- 意思是从第50个字节开始到最后一个字节
Range : bytes=-70 意思是最后的70个字节
Range : bytes=50-100 意思是从第50字节到100字节
读取客户端发来的Range头解析为:
假设文件总大小为130字节。
第一种Range 50-130
第二种Range ( 130 - 70 )-130
第三种Range 50-100
还有一点要晓得的就是返回的HTTP状态码200、206、416这些意义。200是OK(一切正常),206是Partial Content(服务器已经成功处理了部分内容),416 Requested Range Not Satisfiable(对方(客户端)发来的Range 请求头不合理)。
一般处理单线程处理: 客户端发来请求 ——-> 服务端返回200 ——> 客户端开始接受数据 ——> 用户手多多把下载停止了 ——> 客户端突然停止接受数据 ——> 然后客户端都没说再见就与服务端断开了 ——> 用户手的痒了又按回开始键 ——> 客户端再次与服务端连接上,并发送Range请求头给服务端 ——> 这时服务端返回是206 ——> 服务端从断开的数据那继续发送,并且会发送响应头:Content-Range给客户端 ——>客户端接收数据 ——>直到完成。
再服务端返回206的前面,客户端假如发送了些不合理的Range请求头,服务端就不是返回206而是416。就是结尾字节大于开始字节或者是结尾字节是0什么的,这必定是416的。
单线程通常就是这样,那么我们的客户端是多线程呢,那么我们必定也是多线程。客户端会一次性发来多个请求,来贪婪的快速地下载完成文件。链接别太多就行了。
GET /123.zip HTTP/1.1 客户端发来请求了。
那我们告诉它。
HTTP/1.1 200 OK
Accept-Ranges : bytes //告诉客户端,我们是支持断点传输的,你知道了吗?
Content-Length : 1900 //文件总大小
Content-Type : image/jpeg //文件类型
二进制数据。
好了,就这样发送去了。发着发着,咦TM断掉了。我的七舅姥爷姑奶奶,为毛就断掉了呢,包租婆,怎么霎时间摸左水呐。
客户端又发来请求这回有点意思。
GET /123.zip HTTP/1.1
Range:bytes=580-
大家看到没,会多了怎么一行,我们解析为从580字节开始到1900字节,是要部分内容耶,那么返回什么呢。没错206啊。
HTTP/1.1 206 Partial Content
Accept-Ranges : bytes
Content-Type : image/jpeg //文件类型
Content-Length : (1900 - 580) //长度则不是总长度了,而580到1900共有多少字节。
Content-Range :bytes 580-(1900-1 ) / 1900 //这位同学,我想问问你,为什么结束字节要减1呢。这是因为发来的Range请求头文件下标是0开始,那么结尾数显示也要减1;但是实际上输出的字节是不减1的,完全是写法问题。
下面一段代码是在做笔记时从网上copy来的,未做验证,原文地址:https://blog.csdn.net/zhaowen25/article/details/41779221
服务端代码
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.net.ServerSocket;
import java.net.Socket;
// 断点续传服务端
public class FTPServer {
// 文件发送线程
class Sender extends Thread{
// 网络输入流
private InputStream in;
// 网络输出流
private OutputStream out;
// 下载文件名
private String filename;
public Sender(String filename, Socket socket){
try {
this.out = socket.getOutputStream();
this.in = socket.getInputStream();
this.filename = filename;
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
System.out.println("start to download file!");
int temp = 0;
StringWriter sw = new StringWriter();
while((temp = in.read()) != 0){
sw.write(temp);
//sw.flush();
}
// 获取命令
String cmds = sw.toString();
System.out.println("cmd : " + cmds);
if("get".equals(cmds)){
// 初始化文件
File file = new File(this.filename);
RandomAccessFile access = new RandomAccessFile(file,"r");
//
StringWriter sw1 = new StringWriter();
while((temp = in.read()) != 0){
sw1.write(temp);
sw1.flush();
}
System.out.println(sw1.toString());
// 获取断点位置
int startIndex = 0;
if(!sw1.toString().isEmpty()){
startIndex = Integer.parseInt(sw1.toString());
}
long length = file.length();
byte[] filelength = String.valueOf(length).getBytes();
out.write(filelength);
out.write(0);
out.flush();
// 计划要读的文件长度
//int length = (int) file.length();//Integer.parseInt(sw2.toString());
System.out.println("file length : " + length);
// 缓冲区10KB
byte[] buffer = new byte[1024*10];
// 剩余要读取的长度
int tatol = (int) length;
System.out.println("startIndex : " + startIndex);
access.skipBytes(startIndex);
while (true) {
// 如果剩余长度为0则结束
if(tatol == 0){
break;
}
// 本次要读取的长度假设为剩余长度
int len = tatol - startIndex;
// 如果本次要读取的长度大于缓冲区的容量
if(len > buffer.length){
// 修改本次要读取的长度为缓冲区的容量
len = buffer.length;
}
// 读取文件,返回真正读取的长度
int rlength = access.read(buffer,0,len);
// 将剩余要读取的长度减去本次已经读取的
tatol -= rlength;
// 如果本次读取个数不为0则写入输出流,否则结束
if(rlength > 0){
// 将本次读取的写入输出流中
out.write(buffer,0,rlength);
out.flush();
} else {
break;
}
// 输出读取进度
//System.out.println("finish : " + ((float)(length -tatol) / length) *100 + " %");
}
//System.out.println("receive file finished!");
// 关闭流
out.close();
in.close();
access.close();
}
} catch (IOException e) {
e.printStackTrace();
}
super.run();
}
}
public void run(String filename, Socket socket){
// 启动接收文件线程
new Sender(filename,socket).start();
}
public static void main(String[] args) throws Exception {
// 创建服务器监听
ServerSocket server = new ServerSocket(8888);
// 接收文件的保存路径
String filename = "E:\\ceshi\\mm.pdf";
for(;;){
Socket socket = server.accept();
new FTPServer().run(filename, socket);
}
}
}
客户端代码
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
// 断点续传客户端
public class FTPClient {
/**
* request:get0startIndex0
* response:fileLength0fileBinaryStream
*
* @param filepath
* @throws Exception
*/
public void Get(String filepath) throws Exception {
Socket socket = new Socket();
// 建立连接
socket.connect(new InetSocketAddress("127.0.0.1", 8888));
// 获取网络流
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
// 文件传输协定命令
byte[] cmd = "get".getBytes();
out.write(cmd);
out.write(0);// 分隔符
int startIndex = 0;
// 要发送的文件
File file = new File(filepath);
if(file.exists()){
startIndex = (int) file.length();
}
System.out.println("Client startIndex : " + startIndex);
// 文件写出流
RandomAccessFile access = new RandomAccessFile(file,"rw");
// 断点
out.write(String.valueOf(startIndex).getBytes());
out.write(0);
out.flush();
// 文件长度
int temp = 0;
StringWriter sw = new StringWriter();
while((temp = in.read()) != 0){
sw.write(temp);
sw.flush();
}
int length = Integer.parseInt(sw.toString());
System.out.println("Client fileLength : " + length);
// 二进制文件缓冲区
byte[] buffer = new byte[1024*10];
// 剩余要读取的长度
int tatol = length - startIndex;
//
access.skipBytes(startIndex);
while (true) {
// 如果剩余长度为0则结束
if (tatol == 0) {
break;
}
// 本次要读取的长度假设为剩余长度
int len = tatol;
// 如果本次要读取的长度大于缓冲区的容量
if (len > buffer.length) {
// 修改本次要读取的长度为缓冲区的容量
len = buffer.length;
}
// 读取文件,返回真正读取的长度
int rlength = in.read(buffer, 0, len);
// 将剩余要读取的长度减去本次已经读取的
tatol -= rlength;
// 如果本次读取个数不为0则写入输出流,否则结束
if (rlength > 0) {
// 将本次读取的写入输出流中
access.write(buffer, 0, rlength);
} else {
break;
}
System.out.println("finish : " + ((float)(length -tatol) / length) *100 + " %");
}
System.out.println("finished!");
// 关闭流
access.close();
out.close();
in.close();
}
public static void main(String[] args) {
FTPClient client = new FTPClient();
try {
client.Get("E:\\ceshi\\test\\mm.pdf");
} catch (Exception e) {
e.printStackTrace();
}
}
}