12

We're having trouble serving mp4s that will play on an ipad using a default rails 3 app. The mp4 is served correctly when viewing the route in chrome and other browsers on a desktop.

Here is our code:

file_path = File.join(Rails.root, 'test.mp4')
send_file(file_path, :disposition => "inline", :type => "video/mp4")

We hit 0.0.0.0:3000/video/test.mp4 to view the video and are presented with cannot play icon on the ipad. We've tried modifying various headers "Content-Length", "Content-Range", etc but they don't seem to affect the end result.

We've also tried using send_data to some extent

i.e.

File.open(file_path, "r") do |f|
    send_data f.read, :type => "video/mp4"
end 

The same video serves fine from the public folder when viewed on the Ipad.

What is the proper way to serve mp4 files through rails to an Ipad?

Colin Wagner
  • 519
  • 3
  • 10

1 Answers1

22

The problem seems to be that rails doesn't handle http-range requests which ios needs for streaming mp4s.

This was our solution for development, (using thin as our server):

  if(request.headers["HTTP_RANGE"]) && Rails.env.development?

    size = File.size(file_path)
    bytes = Rack::Utils.byte_ranges(request.headers, size)[0]
    offset = bytes.begin
    length = bytes.end - bytes.begin + 1

    response.header["Accept-Ranges"]=  "bytes"
    response.header["Content-Range"] = "bytes #{bytes.begin}-#{bytes.end}/#{size}"
    response.header["Content-Length"] = "#{length}"

    send_data IO.binread(file_path,length, offset), :type => "video/mp4", :stream => true,  :disposition => 'inline',
              :file_name => file_name

  else
    send_file(file_path, :disposition => 'inline', :stream => true, :file_name => file_name)
  end

Ultimately we will be using nginx XSendfile to serve the assets in our production environment as the above solution is much slower than what we need.

Thomas Desert
  • 1,346
  • 3
  • 13
  • 28
Colin Wagner
  • 519
  • 3
  • 10
  • 2
    I think there's a fencepost error in the code. I believe length should be `bytes.end - bytes.begin + 1` - if the byte range is from byte 10 through 12, that should be 3 bytes. Also, if you're using `send_data` for some reason, be sure to set `Content-Length` in the response headers. – tovodeverett Sep 17 '13 at 23:47
  • Thank you for this solution! I added corrections based on @tovodeverett comment. Using Sinatra (instead of Rails) for development, I managed to replicate the send_data behaviour using sinatra/streaming contrib as follows: `stream {|out| out.write IO.binread(file_path,length, offset); out.flush}` – Thomas Desert Nov 05 '15 at 14:43
  • 1
    Thanks for the solution! We noticed, that in Order to make this approach work in Chrome one has to explicitly set the response status to `206`. – smallbutton Oct 11 '17 at 12:07