2

How can I implement the flow in the first answer here on Phoenix? At first I thought I had to use Conn.send_chunked and Conn.chunk but I am starting to realize I will be getting many requests instead of me sending a various responses.

Community
  • 1
  • 1
Cristian Garcia
  • 9,630
  • 6
  • 54
  • 75

3 Answers3

2

If anybody is interested, this is my solution. Talks correctly to video players, and also handles normal files.

[range_start, range_end] = 
    if Enum.empty?(Conn.get_req_header(conn, "range")) do
      [0, file.content_length - 1]  
    else
      [rn] = Conn.get_req_header(conn, "range")

      res = Regex.run(~r/bytes=([0-9]+)-([0-9])?/, rn)
      default_end = Integer.to_string(file.content_length - 1)

      {range_start, _} = res |> Enum.at(1) |> Integer.parse
      {range_end, _} = res |> Enum.at(2, default_end) |> Integer.parse

      [range_start, range_end]
    end

    content_length = range_end - range_start + 1

    conn = conn
      |> Conn.put_resp_content_type(file.content_type)
      |> Conn.put_resp_header("content-length", Integer.to_string(content_length))
      |> Conn.put_resp_header("accept-ranges", "bytes")
      |> Conn.put_resp_header("content-disposition", ~s(inline; filename="#{file.filename}"))
      |> Conn.put_resp_header("content-range", "bytes #{range_start}-#{range_end}/#{file.content_length}")
      |> Conn.send_file(206, "#{files_path}/#{id}", range_start, content_length)
Cristian Garcia
  • 9,630
  • 6
  • 54
  • 75
1

Thanks for posting your solution Cristian! I never would've figured this out otherwise...

For any Elixir newbies such as myself who run across this and can't quite get the above snippet to work, here's my full controller method:

def load_song(conn, params) do
  filename = List.first(params["song"])
  file_path = "priv/static/sounds/#{filename}"
  {:ok, stats} = File.stat(file_path)
  filesize = stats.size

  [range_start, range_end] =
      if Enum.empty?(Plug.Conn.get_req_header(conn, "range")) do
        [0, filesize - 1]
      else
        [rn] = Plug.Conn.get_req_header(conn, "range")

        res = Regex.run(~r/bytes=([0-9]+)-([0-9])?/, rn)
        default_end = Integer.to_string(filesize - 2)

        {range_start, _} = res |> Enum.at(1) |> Integer.parse
        {range_end, _} = res |> Enum.at(2, default_end) |> Integer.parse

        [range_start, range_end]
      end

  content_length = range_end - range_start + 2

  conn
    |> Plug.Conn.put_resp_content_type("audio/mpeg")
    |> Plug.Conn.put_resp_header("content-length", Integer.to_string(content_length))
    |> Plug.Conn.put_resp_header("accept-ranges", "bytes")
    |> Plug.Conn.put_resp_header("content-disposition", ~s(inline; filename="#{filename}"))
    |> Plug.Conn.put_resp_header("content-range", "bytes #{range_start}-#{range_end}/#{filesize}")
    |> Plug.Conn.send_file(206, file_path, range_start, content_length)
end

I've learned since about some basic Elixir features like aliasing that would've simplified this, and I'm guessing some other coding choices are a little un-idiomatic, but the above show work as a "drop in" method for your controller.

Wikk
  • 63
  • 1
  • 10
  • Glad it helped, I spend a whole afternoon figuring it out, I hope other can get it done quicker with the answer. I see you changed the math a little. – Cristian Garcia Dec 04 '15 at 19:39
  • Oops, I may have done that accidentally. I'm used to working at higher levels of abstraction than raw bytes, so I was a little uncertain figuring out content length and file size :) I'll have to do some experiments this afternoon. – Wikk Dec 05 '15 at 16:46
1

I have created a plug for this called plug_range, it will catch every range request and serve them. It is still a work in progress to follow the RFC, but it might comes in handy until then.

PlugRange

TheSquad
  • 7,385
  • 8
  • 40
  • 79