1

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'.

Vallerious
  • 570
  • 6
  • 13

1 Answers1

1

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.

szatmary
  • 29,969
  • 8
  • 44
  • 57
  • As a streaming server, how could I know where that metadata is? I still want to send a portion of the file each packet and if I leave it to the browser to find the metadata it is defeating the purpose of streaming. – Vallerious Mar 24 '21 at 09:31
  • "As a streaming server," Its not a streaming server, is a plain http server. It's done this way on purpose because it's cheeper to operate at scale. "it is defeating the purpose of streaming" No it's not. This is how streaming mp4 works. This is how ALL mp4 streaming's on the internet works (DASH and HLS are different). The server is dumb, and just responds to the clients request. You can always put the moov box at the start of the file by applying "faststart" to avoid one seek. I suspect this is really an https://xyproblem.info problem and there is something underlying your question though. – szatmary Mar 24 '21 at 16:00
  • 1
    If you want to know more about "where the media is at", you should read about the structure of mp4 files. Google can help you with that. – szatmary Mar 24 '21 at 16:01