5

I have a Rails app that protects the uploaded videos putting them into a private folder.

Now I need to play these videos, and when I do something like this in the controller:

  def show
    video = Video.find(params[:id])
    send_file(video.full_path, type: "video/mp4", disposition: "inline")
  end

And open the browser(Chrome or FF) at /videos/:id it doesn't play the video.

If I put the same video at the public folder, and access it like /video.mp4 it will play.

If I remove the dispositon: "inline" it will download the video and I can play it from my computer. Samething happens with webm videos.

What am I missing? Is this something possible to do?

Bruno Campos
  • 2,188
  • 1
  • 20
  • 34
  • If you use FireBug to look at the response headers of a working request from the public folder and compare that to the response headers of the failing request, it may highlight the relevant difference... – Brad Werth Nov 08 '12 at 00:39

1 Answers1

6

To stream videos, we have to handle the requested byte range for some browsers.

Solution 1: Use the send_file_with_range gem

The easy way would be to have the send_file method patched by the send_file_with_range gem.

Include the gem in the Gemfile

# Gemfile
gem 'send_file_with_range'

and provide the range: true option for send_file:

def show
  video = Video.find(params[:id])
  send_file video.full_path, type: "video/mp4", 
    disposition: "inline", range: true
end

The patch is quite short and worth a look. But, unfortunately, it did not work for me with Rails 4.2.

Solution 2: Patch send_file manually

Inspired by the gem, extending the controller manually is fairly easy:

class VideosController < ApplicationController

  def show
    video = Video.find(params[:id])
    send_file video.full_path, type: "video/mp4",
      disposition: "inline", range: true
  end

private

  def send_file(path, options = {})
    if options[:range]
      send_file_with_range(path, options)
    else
      super(path, options)
    end
  end

  def send_file_with_range(path, options = {})
    if File.exist?(path)
      size = File.size(path)
      if !request.headers["Range"]
        status_code = 200 # 200 OK
        offset = 0
        length = File.size(path)
      else
        status_code = 206 # 206 Partial Content
        bytes = Rack::Utils.byte_ranges(request.headers, size)[0]
        offset = bytes.begin
        length = bytes.end - bytes.begin
      end
      response.header["Accept-Ranges"] = "bytes"
      response.header["Content-Range"] = "bytes #{bytes.begin}-#{bytes.end}/#{size}" if bytes

      send_data IO.binread(path, length, offset), options
    else
      raise ActionController::MissingFile, "Cannot read file #{path}."
    end
  end

end

Further reading

Because, at first, I did not know the difference between stream: true and range: true, I found this railscast helpful:

http://railscasts.com/episodes/266-http-streaming

fiedl
  • 5,667
  • 4
  • 44
  • 57
  • 1
    FYI, there are two problems with this code: 1) `status_code` isn't used anywhere, I think we're supposed to do `options.merge(status: status_code)`. 2) `length` var has off-by-one error as described in the comments under this answer: https://stackoverflow.com/a/16593030/742872 In the end I combined code from this answer (since it's more general and better structured) with the fixes from the other one to produce something that works. – Rafał Cieślak Mar 22 '21 at 10:09
  • Thanks Rafał! Do you have access rights for this answer to suggest an edit? Otherwise, you could just paste me your working code into a gist (http://gist.github.com) and I'd be happy to update this answer accordingly. Thanks for noticing! – fiedl Mar 22 '21 at 15:57