当前位置:网站首页>The principle and implementation of buffer playback of large video files
The principle and implementation of buffer playback of large video files
2022-07-07 07:53:00 【Beihai shad is awake】
Source of problem
When our project involves the online playback of large video files , Generally, the stream that directly returns this file will lead to a long response time , This may be because the stream must be loaded to the client before it can be displayed on the client , Now what we want to accomplish is to buffer and play a large video file instead of returning all at once and then playing .
Of course, if the video file is placed on the disk of a special server, it can be directly passed nginx Configure static resource access directly through url Access the video file . But what this article wants to do is to stream the video files of the server back to the front-end display and display them while buffering , Avoid long response times .
Realization principle
Reference article 1
Reference article 2
We can read part of this large file at a time and return it to the front end , The front end plays this part , Continue to request part of the following video content when playing , In this way, you can play while loading , At the same time, if users click on the back part, they can directly request the content of the back part of the video .
http The request has the relevant breakpoint transmission protocol ,http Protocol status code 206 It is the protocol to realize breakpoint transmission ,Http The request header needs to specify the range of data acquisition : Range: bytes=first-end
,first, The index location of the starting data ,end, End the index position of the data . for example :Range: bytes=0-2000.Range Parameters also support multiple intervals , Separate with commas , for example :Range: bytes=0-5,6-10. At this time response Of Content-Type It is no longer the original document mime type , And use a multipart/byteranges Type said .Http The response needs to specify the range response header :content-range bytes first-end
, also http The status code is set to 206.
- Range The format of
Range: bytes=0-499 It means the first one 0-499 The contents of the byte range
Range: bytes=500-999 It means the first one 500-999 The contents of the byte range
Range: bytes=-500 Said the last 500 The contents of bytes
Range: bytes=500- Says from the first 500 From the beginning of a byte to the end of a file
Range: bytes=0-0,-1 Represents the first and last byte
Range: bytes=500-600,601-999 Specify several ranges at the same time
- Response head Content-Range The format of
Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]
for example :
Content-Range: bytes 0-499/22400
0-499 It refers to the range of data currently sent , and 22400 Is the total size of the file .
And after the response , The returned response header content is also different :
HTTP/1.1 200 Ok( Do not use breakpoint continuation )
HTTP/1.1 206 Partial Content( Use breakpoint continuation )
This is handled at the front end , Of course, in the front end, we can directly use the requests encapsulated in the component .
The first implementation
Front end use video.js
- First install the components
npm install video.js
- Import components
import Video from 'video.js'
import 'video.js/dist/video-js.css'
Vue.prototype.$video = Video
- Use components
<video controls="controls" controls="controls">
<source src="http://ip:port/xxxxxxx" type="video/mp4" />
</video>
When clicking on the video content, this component will directly request the required content from the backend , The back end can directly transmit the file content of the response part according to the content of the requested part of the video .
- Back end reception
//path For local files
public void play(String path, HttpServletRequest request, HttpServletResponse response) {
RandomAccessFile targetFile = null;
OutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
response.reset();
// In the request header Range Value
String rangeString = request.getHeader(HttpHeaders.RANGE);
// Open file
File file = new File(path);
if (file.exists()) {
// Use RandomAccessFile Read the file
targetFile = new RandomAccessFile(file, "r");
long fileLength = targetFile.length();
long requestSize = (int) fileLength;
// Download video in segments
if (StringUtils.hasText(rangeString)) {
// from Range Extract the start and end positions of the data that need to be obtained
long requestStart = 0, requestEnd = 0;
String[] ranges = rangeString.split("=");
if (ranges.length > 1) {
String[] rangeDatas = ranges[1].split("-");
requestStart = Integer.parseInt(rangeDatas[0]);
if (rangeDatas.length > 1) {
requestEnd = Integer.parseInt(rangeDatas[1]);
}
}
if (requestEnd != 0 && requestEnd > requestStart) {
requestSize = requestEnd - requestStart + 1;
}
// Set the request header according to the protocol
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
response.setHeader(HttpHeaders.CONTENT_TYPE, "video/mp4");
if (!StringUtils.hasText(rangeString)) {
response.setHeader(HttpHeaders.CONTENT_LENGTH, fileLength + "");
} else {
long length;
if (requestEnd > 0) {
length = requestEnd - requestStart + 1;
response.setHeader(HttpHeaders.CONTENT_LENGTH, "" + length);
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + requestStart + "-" + requestEnd + "/" + fileLength);
} else {
length = fileLength - requestStart;
response.setHeader(HttpHeaders.CONTENT_LENGTH, "" + length);
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + requestStart + "-" + (fileLength - 1) + "/"
+ fileLength);
}
}
// Breakpoint transmission download video return 206
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
// Set up targetFile, Start reading data from a custom location
targetFile.seek(requestStart);
} else {
// If Range If it is empty, download the whole video
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=test.mp4");
// Set the file length
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileLength));
}
// Read data stream from disk and return
byte[] cache = new byte[4096];
try {
while (requestSize > 0) {
int len = targetFile.read(cache);
if (requestSize < cache.length) {
outputStream.write(cache, 0, (int) requestSize);
} else {
outputStream.write(cache, 0, len);
if (len < cache.length) {
break;
}
}
requestSize -= cache.length;
}
} catch (IOException e) {
// tomcat Original words . Write operations IO Exceptions are almost always caused by the client actively closing the connection , So eat the exception directly and log it
// For example, use video When playing video, you often send Range by 0- The range is just to get the video size , Then the connection was disconnected
log.info(e.getMessage());
}
} else {
throw new RuntimeException(" The strength of the document is wrong ");
}
outputStream.flush();
} catch (Exception e) {
log.error(" File transfer error ", e);
throw new RuntimeException(" File transfer error ");
}finally {
if(outputStream != null){
try {
outputStream.close();
} catch (IOException e) {
log.error(" Stream release error ", e);
}
}
if(targetFile != null){
try {
targetFile.close();
} catch (IOException e) {
log.error(" File stream release error ", e);
}
}
}
}
The second way to achieve it
Of course, this is not the only way to realize , Don't use vedio.js It can be achieved in other ways , The principle of implementation is similar .
Now let's introduce another way , It uses ReactPlayer.
- The front-end implementation
import React from 'react'
import ReactPlayer from 'react-player'
export default class OperationEdit extends React.Component {
render() {
return (
<ReactPlayer
className='react-player'
// url='https://stream7.iqilu.com/10339/upload_transcode/202002/18/20200218093206z8V1JuPlpe.mp4'
url='/api/baseInfo/file/videoPlayback'
width='100%'
height='100%'
playing={
true}
controls
/>
)
}
}
- The back-end processing
@ApiOperation(" Video streaming ")
@RequestMapping(value = "/videoPlayback", method = RequestMethod.GET)
@Override
public void videoPlayback() throws IOException {
String filepath = "C:\\Users\\admin\\Pictures\\ Test video \\16MSize.mp4";
// Check if it is Range request
if (request.getHeader("Range") != null) {
// Read the file
File targetFile = new File(filepath);
BufferedInputStream in = new BufferedInputStream(new FileInputStream(targetFile));
Long fileSize = targetFile.length();
// analysis Range
Map<String, Integer> range = this.analyzeRange(request.getHeader("Range"), fileSize.intValue());
// Set the response header
response.setContentType("video/mp4");
response.setHeader("Content-Length", String.valueOf(fileSize.intValue()));
response.setHeader("Content-Range", "bytes " + range.get("startByte") + "-" + range.get("endByte") + "/" + fileSize.intValue());
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Accept-Ranges", "bytes");
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
// Start to output
OutputStream os = response.getOutputStream();
int length = range.get("endByte") - range.get("startByte");
System.out.println("length:" + length);
byte[] buffer = new byte[length < 1024 ? length : 1024];
in.skip(range.get("startByte"));
int i = in.read(buffer);
length = length - buffer.length;
while (i != -1) {
os.write(buffer, 0, i);
if (length <= 0) {
break;
}
i = in.read(buffer);
length = length - buffer.length;
}
os.flush();
// close
os.close();
in.close();
return;
}
}
/** * analysis range, Parse out the start byte(startByte) And the end byte(endBytes) * * @param range From the request range * @param fileSize The size of the target file * @return */
private Map<String, Integer> analyzeRange(String range, Integer fileSize) {
String[] split = range.split("-");
Map<String, Integer> result = new HashMap<>();
if (split.length == 1) {
// from xxx Length read to end
Integer startBytes = new Integer(range.replaceAll("bytes=", "").replaceAll("-", ""));
result.put("startByte", startBytes);
result.put("endByte", fileSize - 1);
} else if (split.length == 2) {
// from xxx Length read to yyy length
Integer startBytes = new Integer(split[0].replaceAll("bytes=", "").replaceAll("-", ""));
Integer endBytes = new Integer(split[1].replaceAll("bytes=", "").replaceAll("-", ""));
result.put("startByte", startBytes);
result.put("endByte", endBytes > fileSize ? fileSize : endBytes);
} else {
log.info(" Unrecognized range:", range);
}
return result;
}
Enhanced verification
In the real world , There may be such a situation , That is, when the terminal initiates a renewal request ,URL The corresponding file content has changed on the server , At this time, the continued data must be wrong , Without such error handling, the completion of file transfer is definitely wrong . How to solve this problem ? At this time, there needs to be a method to identify the uniqueness of the file .
stay RFC2616 There are corresponding definitions in , Such as the implementation Last-Modified To identify the last modification time of the file , In this way, it can be judged whether there has been any change when resuming the file . meanwhile FC2616 There is also a ETag The head of the , have access to ETag Header to place the unique identification of the file .
Last-Modified
If-Modified-Since, and Last-Modified The same is used to record the last modification time of the page HTTP Header information , It's just Last-Modified It's sent from the server to the client HTTP head , and If-Modified-Since Is the header sent by the client to the server , You can see , Re request locally existing cache When the page is , The client will go through If-Modified-Since The header will be sent from the server Last-Modified Finally, modify the timestamp and send it back , This is to allow the server side to verify , Use this timestamp to determine whether the client's page is up-to-date , If it's not the latest , The new content is returned , If it's the latest , Then return to 304 Tell the client its local cache Your page is up to date , So the client can load the page directly from the local , In this way, the data transmitted on the network will be greatly reduced , At the same time, it also reduces the burden of the server .
Etag
Etag(Entity Tags) Mainly to solve Last-Modified Some unsolvable problems .
Some files may change periodically , But the content doesn't change ( Only change the modification time ), At this time, we don't want the client to think that the file has been modified , And again GET.
Some files are modified very frequently , for example : Modify in less than seconds (1s It has been modified in N Time ),If-Modified-Since The particle size that can be detected is s Class , This modification cannot be judged ( Or say UNIX Record MTIME It's only accurate to seconds ).
Some servers can't get the exact last modification time of the file .
So ,HTTP/1.1 Introduced Etag.Etag Just a file related tag , It can be a version tag , for example :v1.0.0; Or say “627-4d648041f6b80” Such a mysterious looking code . however HTTP/1.1 The standard does not specify Etag What is the content or how to implement , The only rule is Etag Need to put in “” Inside .
If-Range
Used to determine whether the entity has changed , If the entity does not change , The server sends the missing part of the client , Otherwise send the whole entity .
- Format :
If-Range: Etag | HTTP-Date
in other words ,If-Range have access to Etag perhaps Last-Modified The value returned . When there is no ETage But there are Last-modified when , You can put Last-modified As If-Range Value of field .
for example :
If-Range: “627-4d648041f6b80”
If-Range: Fri, 22 Feb 2013 03:45:02 GMT
If-Range Must be with Range Matching use of . If there is no Range, that If-Range Will be ignored . If the server does not support If-Range, that Range It's also ignored .
If Etag With the target content of the server Etag equal , That is, there is no change , Then the status code of the response message is 206. If the target content of the server changes , Then the status code of the response message is 200.
Others for verification HTTP Header information :If-Match/If-None-Match、If-Modified-Since/If-Unmodified-Since.
working principle
Etag Generated by the server , Client pass If-Range Condition judgment request to verify whether the resource is modified . The process of requesting a file is as follows :
First request :
- Client initiated HTTP GET Request a file .
- Server processing request , Return the contents of the file and the corresponding Header, These include Etag( for example :627-4d648041f6b80)( Suppose the server supports Etag Generated and turned on Etag) Status code for 200.
Second request ( Breakpoint continuation ):
- Client initiated HTTP GET Request a file , Send... At the same time If-Range( The content of this header is what the server returns when it first requests Etag:627-4d648041f6b80).
- The server determines the received Etag And calculated Etag match , If the match , Then the response status code is 206; otherwise , Status code for 200.
Check whether the server supports breakpoint resuming
curl -i --range 0-9 http://www.baidu.com/img/bdlogo.gif
If you can find Content-Range, It indicates that the server supports breakpoint continuation . Some servers also return Accept-Ranges, Output results Accept-Ranges: bytes , Description the server supports downloading by bytes .
边栏推荐
- [SUCTF 2019]Game
- Value sequence (subsequence contribution problem)
- Leetcode 43 String multiplication (2022.02.12)
- Most elements
- A bit of knowledge - about Apple Certified MFI
- 智联+影音,AITO问界M7想干翻的不止理想One
- After the interview, the interviewer roast in the circle of friends
- 海思芯片(hi3516dv300)uboot镜像生成过程详解
- Linux server development, SQL statements, indexes, views, stored procedures, triggers
- pytest+allure+jenkins安装问题:pytest: error: unrecognized arguments: --alluredir
猜你喜欢
PHP exports millions of data
Resource create package method
3D reconstruction - stereo correction
Jenkins远程构建项目超时的问题
idea添加类注释模板和方法模板
Kbu1510-asemi power supply special 15A rectifier bridge kbu1510
2022-07-06:以下go语言代码是否会panic?A:会;B:不会。 package main import “C“ func main() { var ch chan struct
KBU1510-ASEMI电源专用15A整流桥KBU1510
Qt学习28 主窗口中的工具栏
[webrtc] m98 Screen and Window Collection
随机推荐
@component(““)
今日现货白银操作建议
[webrtc] M98 screen and window acquisition
2022年全国最新消防设施操作员(初级消防设施操作员)模拟题及答案
What are the positions of communication equipment manufacturers?
【斯坦福计网CS144项目】Lab4: TCPConnection
【webrtc】m98 screen和window采集
idea添加类注释模板和方法模板
2022 recurrent training question bank and answers of refrigeration and air conditioning equipment operation
Figure out the working principle of gpt3
Leanote private cloud note building
2022焊工(初级)判断题及在线模拟考试
Rxjs - observable doesn't complete when an error occurs - rxjs - observable doesn't complete when an error occurs
vus.SSR在asynData函数中请求数据的注意事项
IO stream file
php导出百万数据
Use and analysis of dot function in numpy
Six methods of flattening arrays with JS
[UVM practice] Chapter 1: configuring the UVM environment (taking VCs as an example), run through the examples in the book
Linux server development, MySQL process control statement