I am exploring Http Range requests and video streaming with Java. I wanted to create a controller which streams a video to a tag.
For some reason, after end range of 32768, the browser sends request for start of 100237312. Here are a slice of the logs from my console:
...
Start: 27648, End: 28672, chunk size: 1024
Start: 28672, End: 29696, chunk size: 1024
Start: 29696, End: 30720, chunk size: 1024
Start: 30720, End: 31744, chunk size: 1024
Start: 31744, End: 32768, chunk size: 1024
Start: 100237312, End: 100238336, chunk size: 1024
Start: 100238336, End: 100239360, chunk size: 1024
...
My code:
package com.example.demo.controllers;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.*;
import java.nio.file.Files;
import java.util.Arrays;
@RestController
@RequestMapping("/video")
public class VideoCtrl {
@CrossOrigin
@ResponseBody
@GetMapping
public ResponseEntity<InputStreamResource> getVideo(@RequestHeader("Range") String range) {
String[] rangeHeaderParams = HttpRange.parseHttpRangeHeader(range);
File file = new File(getClass().getClassLoader().getResource("video_for_test.mp4").getFile());
InputStream is = null;
try {
is = new FileInputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
long fileSize = file.length();
assert is != null;
final int CHUNK_SIZE = 1024;
String type = rangeHeaderParams[0];
int start = Integer.parseInt(rangeHeaderParams[1]);
int end = start + CHUNK_SIZE;
System.out.println(String.format(
"Start: %d, End: %d, chunk size: %d\n", start, end, end - start
));
byte[] chunk = new byte[CHUNK_SIZE];
try {
is.skip(start);
is.read(chunk, 0, end - start);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("Content-Range", String.format("%s %d-%d/%d", type, start, end, fileSize));
responseHeaders.set("Accept-Ranges", type);
responseHeaders.set("Content-Length", String.format("%d", CHUNK_SIZE));
responseHeaders.set("Content-Type", "video/mp4");
responseHeaders.set("Connection", "keep-alive");
responseHeaders.set("Content-Transfer-Encoding", "binary");
responseHeaders.set("Cache-Control", "no-cache, no-store");
responseHeaders.set("Expires", "0");
responseHeaders.set("Keep-Alive", "timeout=100000 max=50");
return new ResponseEntity<>(new InputStreamResource(new ByteArrayInputStream(chunk)), responseHeaders, HttpStatus.PARTIAL_CONTENT);
} catch (IOException e) {
return new ResponseEntity<>(new InputStreamResource(new ByteArrayInputStream("error".getBytes())), null, HttpStatus.BAD_REQUEST);
}
}
}
The client:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<video controls width="480">
<source src="http://localhost:8080/video" type="video/mp4">
</video>
</body>
</html>
I would appreciate if someone explains the whole concept, because I do not see it working in practice. In theory, I should just return a slice of the file and send the end range, so that the browser knows what is the next 'start'.
MP4 files don't work the way you think they do. You dont just start playing at the beginning. There is a "index" called the "moov box" that describes the layout of the data in the "mdat box". The moov box can be located at the start or the end of the file. In this case the moov is probably located at offset 100237312. But the browser had to download the start of the file to learn the location of the moov. Once the moov is fully downloaded, the browser can calculate the byte range offset for any start time of the file. This is how seeking works.