前端封裝的視頻組件調用視頻接口時,不支持進度條快進後退,每次都是從頭開始播放,視頻播放接口如下:
**
* @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();
}
}
}