http斷點續傳 ,支持視頻播放支持進度條快進後退

前端封裝的視頻組件調用視頻接口時,不支持進度條快進後退,每次都是從頭開始播放,視頻播放接口如下:

**
     * @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();
		}
	}
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章