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
Peer Stritzinger
  • 8,232
  • 2
  • 30
  • 43
TheSquad
  • 7,385
  • 8
  • 40
  • 79
  • why serve static content through cowboy? – ardhitama Apr 12 '16 at 16:09
  • @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 Apr 12 '16 at 16:18

2 Answers2

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

TheSquad
  • 7,385
  • 8
  • 40
  • 79
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

Patrick Oscity
  • 53,604
  • 17
  • 144
  • 168
  • You are right, Range is not supported yet, however I found a way to do so with Plugs, see my answer below... – TheSquad Apr 13 '16 at 13:39
  • Me personally, I'd rather have a RFC compliant implementation. But if it works for you, great! Cheers ;-) – Patrick Oscity Apr 13 '16 at 13:42
  • 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 ;-) – TheSquad Apr 13 '16 at 13:44
  • 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 ;-P – Patrick Oscity Apr 13 '16 at 13:53