1

I am working on a web-application in node.js. Some parts of the application allow users to upload/retrieve files. I use the 'SoftLayer Object Storage'-service to host these files. When serving a video, however, it plays fine in the HTML5-video element on Chrome/Firefox/Android but it does not on Safari/iOS.

To get to the bottom of this problem, I ran into this explanation on another Stackoverflow-post:

iOS devices expect the videos to arrive in small chunks. So for instance a streaming server is able to do this. However, a blob server just hands the video as a blob which is not what the iOS device expects. Some browsers are smart enough to handle this but others not.

In order to check whether this explanation made any sense, I tried to stream the same mp4-file. In order to do so, I based myself on the accepted answer on this stackoverflow-post and this actually worked fine on all browsers and platforms which leads me to believe that the explanation I quoted above is actually true.

Unfortunately, this streaming-code will not work in my case because the file is hosted on SoftLayer whereas the accepted assumes that the file exists on the filesystem of the server. I communicate with the "Softlayer Object Store" through this API. The code I have now looks like this:

router.get('/testFile', function (req, res) {
  res.writeHead(200, {'Content-Type' : 'video/mp4','Accept-Ranges': 'bytes'});
  request.get({url: authEndpoint, headers: {"X-Auth-Key": apiKey, "X-Auth-User": user}}, function (err, res1) {
    var data = JSON.parse(res1.body);
    var objectPath = data.storage.public + '/' + container + '/' + filename;
    request.get({
      url: objectPath,
      headers: {"X-Auth-Token": res1.headers['x-auth-token']}
    }, function (err) {
      if (err) {
        console.log('error', err);
      }
    }).pipe(res);
  });
});

Obviously, this code doesn't "stream" the video and just sends the entire video to the client at once. Using this code, the video does not work in Safari.

I would like to somehow send the video chunk by chunk (as is the case in the stackoverflow-answer discussed above) rather than entirely at once but I have no idea how to do this when the video is retrieved through a REST-request. I did notice that the API allows one to provide an optional "Range"-parameter when doing a GET-request but I'm not sure how I can use this to my advantage.

Finally I would also like the mention that I don't believe that the encoding of the video is the problem. I am using this video http://www.w3schools.com/html/mov_bbb.mp4 and when I type in that link on iOS/safari it works just fine.

Community
  • 1
  • 1
Simon
  • 871
  • 1
  • 11
  • 23

1 Answers1

1

The problem is that you're trying to pipe the whole response from the API, with status and headers and so on.

Instead, you need to convert the response body to a readable stream and pipe it to the response. To convert a Buffer to a ReadableStream you could use a library like stream-buffers

So, your code should look somewhat like this:

router.get('/testFile', function (req, res) {
  res.writeHead(200, {'Content-Type' : 'video/mp4','Accept-Ranges': 'bytes'});
  request.get({url: authEndpoint, headers: {"X-Auth-Key": apiKey, "X-Auth-User": user}}, function (err, res1) {
    var data = JSON.parse(res1.body);
    var objectPath = data.storage.public + '/' + container + '/' + filename;
    request.get({
      url: objectPath,
      headers: {
        "X-Auth-Token": res1.headers['x-auth-token']
      }
    }, function (err, data) {
      if (err) {
        console.log('error', err);
      } else {
        var bodyStream = new streamBuffers.ReadableStreamBuffer({
          frequency: 10,   // in milliseconds. 
          chunkSize: 2048  // in bytes. 
        });
        bodyStream.pipe(res);
        bodyStream.put(data.body);
      }
    });
  });
});
Petr
  • 5,999
  • 2
  • 19
  • 25
  • Thank you for your comment. I believe you put me on the right track but I was hoping you could give me some more guidance since the documentation of stream-buffers is very minimal. The code I have now looks like this (http://pastebin.com/X7LNj7Qa) but it's not working. I'm not sure whether I'm putting the data on the ReadableStreamBuffer in the right way (line 23), whether I am setting the headers in the correct way (line 12) and whether I am piping the buffer in the correct way (line 27). – Simon Mar 30 '16 at 19:42
  • I am testing it by having a web-page that has a HTML5-video element with the '/testFile'-endpoint as its source. "Writing data" is written to the console quite a number of times (because of line 26) so I am quite sure that I am putting data on bodyStream. The video, however, is not playing at all and node.js gives the following warning: possible EventEmitter memory leak detected. 11 listeners added. Use emitter.setMaxListeners() to increase limit. – Simon Mar 30 '16 at 20:45
  • @Simon nonono, you don't need to add a 'data' listener. Just pipe the bodyStream to res and put the data. I've edited my answer it should have correct code now – Petr Mar 30 '16 at 21:33
  • @Petr Someone else pointed out that the option "encoding:null" should also be included in the request-call. After adding this, the movie works in chrome & firefox. It still does not work in safari but that falls outside of the scope of this question. Thank you for your help! – Simon Apr 04 '16 at 10:28