当前位置:网站首页>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 .
边栏推荐
- Visualization Document Feb 12 16:42
- [webrtc] M98 screen and window acquisition
- 测试周期被压缩?教你9个方法去应对
- Six methods of flattening arrays with JS
- pytest+allure+jenkins安装问题:pytest: error: unrecognized arguments: --alluredir
- MySQL multi column index (composite index) features and usage scenarios
- Solution: could not find kf5 (missing: coreaddons dbusaddons doctools xmlgui)
- A bit of knowledge - about Apple Certified MFI
- 【p2p】本地抓包
- @component(““)
猜你喜欢
2022 simulated examination question bank and online simulated examination of tea master (primary) examination questions
Is the test cycle compressed? Teach you 9 ways to deal with it
leetcode:105. Constructing binary trees from preorder and inorder traversal sequences
Jenkins远程构建项目超时的问题
Ansible
Detailed explanation of Kalman filter for motion state estimation
PHP exports millions of data
misc ez_usb
numpy中dot函数使用与解析
【Unity】物体做圆周运动的几个思路
随机推荐
242. Bipartite graph determination
[2022 ciscn] replay of preliminary web topics
numpy中dot函数使用与解析
智联+影音,AITO问界M7想干翻的不止理想One
What is the difference between TCP and UDP?
dash plotly
SQL优化的魅力!从 30248s 到 0.001s
有 Docker 谁还在自己本地安装 Mysql ?
Regular e-commerce problems part1
2022 recurrent training question bank and answers of refrigeration and air conditioning equipment operation
【斯坦福计网CS144项目】Lab4: TCPConnection
2022 tea master (intermediate) examination questions and mock examination
JSON introduction and JS parsing JSON
Qt学习26 布局管理综合实例
Button wizard collection learning - mineral medicine collection and running map
buuctf misc USB
leetcode:105. Constructing binary trees from preorder and inorder traversal sequences
2022-07-06: will the following go language codes be panic? A: Meeting; B: No. package main import “C“ func main() { var ch chan struct
[performance pressure test] how to do a good job of performance pressure test?
pytorch 参数初始化