Streaming mp4 in Chrome with rails, nginx and send_file Streaming mp4 in Chrome with rails, nginx and send_file google-chrome google-chrome

Streaming mp4 in Chrome with rails, nginx and send_file


I finally have the answers to my original questions. I didn't think I'd ever get here. All my research had lead to dead-ends, hacky non-solutions and "it just works out of the box" (well, not for me).

Why doesn't nginx/apache handle all this stuff automagically with send_file (X-Accel-Redirect/X-Sendfile) like it does when the file is served statically from public? Handling this stuff in rails is so backwards.

They do, but they have to be configured properly to please Rack::Sendfile (see below). Trying to handle this in rails is a hacky non-solution.

How the heck can I actually use send_file with nginx (or apache) so that Chrome will be happy and allow seeking?

I got desperate enough to start poking around rack source code and that's where I found my answer, in the comments of Rack::Sendfile. They are structured as documentation that you can find at rubydoc.

For whatever reason, Rack::Sendfile requires the front end proxy to send a X-Sendfile-Type header. In the case of nginx it also requires a X-Accel-Mapping header. The documentation also has examples for apache and lighttpd as well.

One would think the rails documentation could link to the Rack::Sendfile documentation since send_file does not work out of the box without additional configuration. Perhaps I'll submit a pull request.

In the end I only needed to add a couple lines to my app.conf:

upstream unicorn {  server unix:/tmp/unicorn.app.sock fail_timeout=0;}server {  listen 80 default deferred;  root /vagrant/public;  try_files $uri/index.html $uri @unicorn;  location @unicorn {    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    proxy_set_header HOST $http_host;    proxy_set_header X-Sendfile-Type X-Accel-Redirect; # ADDITION    proxy_set_header X-Accel-Mapping /=/; # ADDITION    proxy_redirect off;    proxy_pass http://localhost:3000;  }  error_page 500 502 503 504 /500.html;  client_max_body_size 4G;  keepalive_timeout 5;}

Now my original code works as expected:

def show  send_file(Video.find(params[:id]).location)end

Edit:

Although this worked initially, it stopped working after I restarted my vagrant box and I had to make further changes:

upstream unicorn {  server unix:/tmp/unicorn.app.sock fail_timeout=0;}server {  listen 80 default deferred;  root /vagrant/public;  try_files $uri/index.html $uri @unicorn;  location ~ /files(.*) { # NEW    internal;             # NEW    alias $1;             # NEW  }                       # NEW  location @unicorn {    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    proxy_set_header HOST $http_host;    proxy_set_header X-Sendfile-Type X-Accel-Redirect;    proxy_set_header X-Accel-Mapping /=/files/; # CHANGED    proxy_redirect off;    proxy_pass http://localhost:3000;  }  error_page 500 502 503 504 /500.html;  client_max_body_size 4G;  keepalive_timeout 5;}

I find this whole thing of mapping one URI to another and then mapping that URI to a location on disk to be totally unnecessary. It's useless for my use case and I'm just mapping one to another and back again. Apache and lighttpd don't require it. But at least it works.

I also added Mime::Type.register('video/mp4', :mp4) to config/initializers/mime_types.rb so the file is served with the correct mime type.