Serving HTTP range request with phoenix?
Asked Answered
Y

2

6

I'm currently developing a website with phoenix and have a video section that should play in the background.

Although it works fine on Chrome & Firefox, it does not work on Safari.

I suspect it is because cowboy is not serving HTTP range-request correctly.

Is there a way to enable (if disable by default) ?

$ curl -H Range:bytes=16- -I http://localhost:4000/videos/vid_home_1.mp4
HTTP/1.1 200 OK
server: Cowboy
date: Tue, 12 Apr 2016 14:41:20 GMT
content-length: 633787
cache-control: public
etag: 480A03F
content-type: video/mp4

When it should be a 206 as shown with a nginx server :

$ curl -H Range:bytes=16- -I http://localhost/videos/vid_home_1.mp4
HTTP/1.1 206 Partial Content
Server: nginx/1.8.0
Date: Tue, 12 Apr 2016 14:46:17 GMT
Content-Type: video/mp4
Content-Length: 633771
Last-Modified: Mon, 11 Apr 2016 12:26:26 GMT
Connection: keep-alive
ETag: "570b97f2-9abbb"
Content-Range: bytes 16-633786/633787
Yorgos answered 12/4, 2016 at 14:48 Comment(2)
why serve static content through cowboy?Orbiculate
@Orbiculate : well mostly because it is what uses phoenix, and I'm still at early development... and also, cowboy serves static content pretty well as far as I know. However, if I can't serve range-request, I might go through nginx and reverse proxy to my phoenix server, but I would prefer to fix this.Yorgos
Y
3

I found a way to do it myself with Plugs... So if anyone want to serve Range Request with Phoenix / Elixir Here's what you have to do (This is pretty basic and does not take into account rfc)

defmodule Plug.Range do                                                                                       
  @behaviour Plug                                                                                             
  @allowed_methods ~w(GET HEAD)                                                                               
  import Plug.Conn                                                                                            

  def init(options) do                                                                                        
    options                                                                                                   
  end                                                                                                         

  def call(conn, _opts) do                                                                                    
    if (Enum.empty?(Plug.Conn.get_req_header(conn, "range"))) do                                              
      conn                                                                                                    
    else                                                                                                      
      file_path = "priv/static" <> conn.request_path                                                          
      if File.exists? file_path do                                                                            

        stats = File.stat! file_path                                                                          
        filesize = stats.size                                                                                 

        req = Regex.run(~r/bytes=([0-9]+)-([0-9]+)?/, conn |> Plug.Conn.get_req_header("range") |> List.first)

        {req_start, _} = req |> Enum.at(1) |> Integer.parse                                                   
        {req_end, _} = req |> Enum.at(2, filesize |> to_string) |> Integer.parse                              

        file_end = ( filesize - 2) |> to_string                                                               

        length = req_end - req_start + 1                                                                      

        conn                                                                                                  
        |> Plug.Conn.put_resp_header("Content-Type", "video/mp4")                                             
        |> Plug.Conn.put_resp_header("Accept-Ranges", "bytes")                                                
        |> Plug.Conn.put_resp_header("Content-Range", "bytes #{req_start}-#{req_end}/#{filesize}")            
        |> Plug.Conn.send_file(206, file_path, req_start, length)                                             
        |> Plug.Conn.halt                                                                                     
      else                                                                                                    
        conn                                                                                                  
      end                                                                                                     
    end                                                                                                       
  end                                                                                                         
end           

As you can see, right now, it will only send "video/mp4" content-Type, but you can easily make something work for everything...

Finally for the Plug to work, you need to place it just before Plug.static in your Project Endpoint file.

Hope it helps someone...

EDIT : For those who are interested, I have created a github/hex.pm package for this: Hex link
github link

Yorgos answered 13/4, 2016 at 13:38 Comment(0)
S
0

It looks like Cowboy does not (yet) have support for the Range header, so you will need to use a different web server for this.

Source: https://github.com/ninenines/cowboy/issues/306

Scientific answered 13/4, 2016 at 8:9 Comment(4)
You are right, Range is not supported yet, however I found a way to do so with Plugs, see my answer below...Yorgos
Me personally, I'd rather have a RFC compliant implementation. But if it works for you, great! Cheers ;-)Scientific
This is for development purpose only... Right now I unfortunately don't have time to create a RFC compliant implementation... I'll probably create a github in a month with the RFC compliant Plug, but for now it will work for me ;-)Yorgos
Hey I didn't mean to challenge you to do the RFC compliant implementation ;-) Just saying, you shouldn't probably run a high traffic video streaming service in production based on this ;-PScientific

© 2022 - 2024 — McMap. All rights reserved.