1

I've tried to send a video file into HTML5 <video>-tag.
I've found a snippet which dates back to 2010 if not earlier.
It's replicated all over the Internet and still circulates it, with some minor differences in code style, names, used Node.js API versions or libraries.
I have some issues with it.
This is the snippet:

app.get('/video', function(req, res) {
    const path     = 'some_path/video.mp4'
    const stat     = fs.statSync(path)
    const fileSize = stat.size
    const range    = req.headers.range
    if( range ) {
        const parts     = range.replace(/bytes=/, "").split("-")
        const start     = parseInt(parts[0],10);
        const end       = parts[1] ? parseInt(parts[1],10) : fileSize-1
        const chunksize = (end-start)+1
        const file      = fs.createReadStream(path, {start, end})
        const head = {
            'Content-Range' : `bytes ${start}-${end}/${fileSize}`,
            'Accept-Ranges' : 'bytes',
            'Content-Length': chunksize,
            'Content-Type'  : 'video/mp4',
        }
        res.writeHead(206, head)
        file.pipe(res)
    } else {
        const head = {
            'Content-Length': fileSize,
            'Content-Type'  : 'video/mp4',
        }
        res.writeHead(200, head)
        fs.createReadStream(path).pipe(res)
    }
})

Here are just few sources of it:

  1. Video streaming with HTML 5 via node.js
  2. video streaming in nodejs through fs module
  3. https://github.com/davidgatti/How-to-Stream-Movies-using-NodeJS/blob/master/routes/video.js
  4. https://github.com/tsukhu/rwd-spa-alljs-app/blob/master/routes/video_streamer.js
  5. https://tewarid.github.io/2011/04/25/stream-webm-file-to-chrome-using-node.js.html

It is obvious that this snippet isn't production ready:

  1. it uses synchronous statSync() instead of asynchronous version,
  2. it does not parse the full grammar of Range header, and has no error handling,
  3. it is hard coded,
  4. else brunch is possibly redundant,
  5. etc.

I have no problem with that. It's an "entry level" code. It's OK.
But the most important thing is that it does not work as it's intended... but still works.

As far as I know, browsers send requests for sources of a <video>-tag with Range header in the form of

Range: bytes=12345-

and, in case of initial request it will be

Range: bytes=0-

so, the line

const end = parts[1] ? parseInt(parts[1],10) : fileSize-1

is identical to

const end = fileSize-1

And, on the initial request, the server will send not a small chunk of a video, but the total file. And in case of video rewind - a really big chunk from the requested position until the end.

This definitely won't work as intended if you request a file through Javascript. You will wait for the file to load completely, or you'll need to somehow deal with tracing the request progress, and that will lead to considerably more complex client code.
But it works like a charm for <video>-tag because browsers deal with this on your behalf.

We can fix this by calculating end this way:

const preferred_chunksize = 10000000 // arbitrary selected value
let end = parts[1] ? parseInt(parts[1],10) : start + preferred_chunksize
if( end > fileSize-1 ) end = fileSize-1

or, considering the form of Range header used for <video>-tag, even this way:

const preferred_chunksize = 10000000 // arbitrary selected value
let end = start + preferred_chunksize
if( end > fileSize-1 ) end = fileSize-1

OK, now it really sends partial responses of expected sizes. But

  1. these lines are more complex than
    const end = fileSize-1
    
  2. we need to choose preferred_chunksize wisely. E.g small chunk sizes like preferred_chunksize=1000 would issue a lot of requests and would work noticeably slower.
    While, at least with Chrome and Firefox, the original version of code streams video files quite alright: I see no excessive cache or memory usage, or speed issues. And I have no need to worry about preferred_chunksize value.

So my question is: should I even bother to send chunks of correct sizes (if I just need to send a video into <video>-tag), is there any popular browser/client js library that will fail to play the video file served with the original snippet? Or is there any over hidden issues with this snippet's approach?

x00
  • 13,643
  • 3
  • 16
  • 40

1 Answers1

1

The server does not get to choose what byte range to send back to the client. The client makes the request, and the server MUST fulfill it. Sending anything else back is an error, and the browser will likely not play the video

The video tag is not making an XHR request, it has access the the response data before the download has completed (so do you via the streaming apis). The browser can (and often does) cancel open ended range requests(Range: bytes=0-) by closing the TCP session. When it makes a request for a range such as Range: bytes=12345- it is because it has already parsed the video file header, and it knows the data it needs is located at offset 12345.

szatmary
  • 29,969
  • 8
  • 44
  • 57
  • The snippet does send a Range with the right starting point. Browsers do not ask for the end point. Formally server makes no mistake. But also not quite aligned with the idea of partial content delivery. – x00 Jan 31 '20 at 06:29