6
votes

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
2
why serve static content through cowboy?ardhitama
@ardhitama : 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.TheSquad

2 Answers

3
votes

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

0
votes

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