Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serving HTTP range request with phoenix?

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
like image 593
TheSquad Avatar asked Apr 12 '16 14:04

TheSquad


People also ask

What is HTTP range requests?

An HTTP range request asks the server to send only a portion of an HTTP message back to a client. Range requests are useful for clients like media players that support random access, data tools that know they need only part of a large file, and download managers that let the user pause and resume the download.

How do I send a partial HTTP request?

To initiate a partial range request, the client must initiate a request to the server and will potentially receive two headers: Accept-Ranges and Content-Length . If the server supports partial range requests, the value for Accept-Ranges will be bytes ; otherwise, the value will be none .

What is http range header?

The Range HTTP request header indicates the part of a document that the server should return. Several parts can be requested with one Range header at once, and the server may send back these ranges in a multipart document. If the server sends back ranges, it uses the 206 Partial Content for the response.

What is accept-ranges header?

The Accept-Ranges HTTP response header is a marker used by the server to advertise its support for partial requests from the client for file downloads. The value of this field indicates the unit that can be used to define a range.

How do I know if the server supports range requests?

If an HTTP response includes the Accept-Ranges header and its value is anything other than " none ", then the server supports range requests. You can perform a manual check by issuing a HEAD request with a tool like cURL.

What is a range request in http?

An HTTP range request asks the server to send only a portion of an HTTP message back to a client. Range requests are useful for clients like media players that support random access, data tools that know they need only part of a large file, and download managers that let the user pause and resume the download.

What are the different statuses when working with range requests?

There are three relevant statuses, when working with range requests: A successful range request elicits a 206 Partial Content status from the server. A range request that is out of bounds will result in a 416 Requested Range Not Satisfiable status, meaning that none of the range values overlap the extent of the resource.

What is if-Range HTTP request header?

The If-Range HTTP request header makes a range request conditional: if the condition is fulfilled, the range request will be issued and the server sends back a 206 Partial Content answer with the appropriate body. If the condition is not fulfilled, the full resource is sent back, with a 200 OK status.


1 Answers

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

like image 55
TheSquad Avatar answered Sep 30 '22 08:09

TheSquad