当前位置:网站首页>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

Original link

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 .

原网站

版权声明
本文为[Beihai shad is awake]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/188/202207070423319430.html