0

I'm currently working on a web project using NodeJS that have video upload and player. The video uploaded will be stored in mongodb in binary form, and when retrive it via API localhost/api/media/:id :

/*getting media from db in binary format*/
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Range', 'bytes=0-1/' + media.length);
res.setHeader('Content-Type', 'video/mp4');
res.setHeader('Cache-Control', 'public,max-age=0');
res.send(media);

This work fine on Chrome and Firefox, using this API will give me a html5 page with video tag that look like this:

<video controls="" autoplay="" name="media">
    <source src="localhost/api/media/abcde" type="video/mp4">
</video>

which run fine even on older Safari like 7. But on Safari 9 in particular it's not working and will only show the media controller with the text "loading" and can never play the video. I try to put the same video file in localhost and access it via localhost/test.mp4 and this run ok in Safari 9 so there's no problem with encoding of the video. Any idea how do I make this one work?

Megazero
  • 35
  • 7
  • Your `Content-Range` response headers say that you're only going to respond with the first two bytes of the file, but then you send the entire thing. Why are you setting the `Content-Range` response headers? – idbehold Aug 29 '16 at 20:09
  • At first I dont have anything other than Content-Type, and it dont work, so I was reading this: http://stackoverflow.com/questions/32996396/safari-9-0-can-not-play-mp4-video-on-the-storage-server And it said that Safari browser only asked for the first 2 bytes, so at first I try with Range = bytes=0-1, and when it dont work either I tried with Content-Range, not that it make any different. I also try with "0-" + media.length + "/" + media.length to request the entire thing, and it also dont work for me. – Megazero Aug 30 '16 at 02:45
  • Are you using GridFS? – idbehold Aug 30 '16 at 03:18
  • Nope, never heard of that before. – Megazero Aug 30 '16 at 04:31

1 Answers1

0

Using GridFS you can properly stream the file to the client with byte-range support:

var db
var app = require('express')()
var parseRange = require('range-parser')
var mongo = require('mongodb')
var MongoClient = mongo.MongoClient
var GridStore = mongo.GridStore
var ObjectID = mongo.ObjectID

function StreamGridFile(req, res, next, GridFile) {
  res.setHeader('Accept-Ranges', 'bytes')
  res.setHeader('Content-Disposition', 'filename="' + req.params.file + '"')
  res.setHeader('Content-Type', GridFile.contentType)
  var ranges = parseRange(GridFile.length, req.headers.range || '', { combine: true })
  if (Array.isArray(ranges) && ranges.length === 1 && ranges.type = 'bytes') {
    var type = ranges.type
    var start = ranges[0].start
    var end = ranges[0].end
    var total = end - start + 1

    res.statusCode = 206
    res.setHeader('Content-Range', 'bytes ' + start + '-' + end + '/' + GridFile.length)
    res.setHeader('Content-Length', total)

    return GridFile.seek(start, function() {
      var finished = false
      GridFile.stream(true).on('data', function (buff) {
        if (finished) return;
        var remaining = total
        total -= buff.length
        if (total <= 0) {
          res.end(buff.slice(0, remaining))
          GridFile.close()
          finished = true
        } else {
          res.write(buff)
        }
      }).on('end', function () {
        if (finished) return;
        finished = true
        res.end()
      }).on('error', next)
    })
  } else if (ranges === -1) {
    res.statusCode = 416
    res.setHeader('Content-Range', 'bytes */' + GridFile.length)
    return res.end()
  } else {
    res.statusCode = 200
    res.setHeader('Content-Length', GridFile.length)
    return GridFile.stream(true).pipe(res)
  }
}

app.get('/files/get/:file', function (req, res, next) {
  new GridStore(db, new ObjectID(req.params.file), null, 'r').open(function(err, GridFile) {
    return GridFile ? StreamGridFile(req, res, next, GridFile) : res.send(404, 'Not Found')
  })
})

MongoClient.connect("mongodb://localhost/test", function(err, database) {
  if (err) throw err;
  db = database;
})
idbehold
  • 16,833
  • 5
  • 47
  • 74
  • Thank you so much. I end up using gridfs-stream instead so my code in the end look different, but reading your code make me realized what's actually is byte-range and how safari end up required that. – Megazero Aug 31 '16 at 09:05