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:
- Video streaming with HTML 5 via node.js
- video streaming in nodejs through fs module
- https://github.com/davidgatti/How-to-Stream-Movies-using-NodeJS/blob/master/routes/video.js
- https://github.com/tsukhu/rwd-spa-alljs-app/blob/master/routes/video_streamer.js
- 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:
- it uses synchronous
statSync()
instead of asynchronous version, - it does not parse the full grammar of
Range
header, and has no error handling, - it is hard coded,
else
brunch is possibly redundant,- 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
- these lines are more complex than
const end = fileSize-1
- we need to choose
preferred_chunksize
wisely. E.g small chunk sizes likepreferred_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 aboutpreferred_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?