使用rails,nginx和send_file在Chrome中流式传输mp4

时间:2015-01-18 10:01:46

标签: ruby-on-rails apache google-chrome nginx html5-video

我不能用我的生命将mp4流式传输到带有html5 <video>标签的Chrome。如果我将文件放在public中,那么一切都很好,并按预期工作。但是,如果我尝试使用send_file来提供它,几乎所有可以想象的都会出错。我正在使用由nginx代理的rails应用程序,其Video模型具有location属性,该属性是磁盘上的绝对路径。

起初我试过了:

def show
  send_file Video.find(params[:id]).location
end

我相信我会沉浸在现代网络发展的荣耀中。哈。这可以在Chrome和Firefox中播放,但既不会寻求,也不会知道视频有多长。我查看了响应标头,发现Content-Type正在application/octet-stream发送,并且没有设置Content-Length。嗯......好吗?

好的,我想我可以在rails中设置这些:

def show
  video = Video.find(params[:id])
  response.headers['Content-Length'] = File.stat(video.location).size
  send_file(video.location, type: 'video/mp4')
end

此时一切都与Firefox中预期的一样。它知道视频有多长,并且寻求按预期工作。 Chrome似乎知道视频的持续时间(不显示时间戳,但搜索栏看起来合适)但是搜索不起作用。

显然Chrome比Firefox更挑剔。它要求服务器使用值为Accept-Ranges的{​​{1}}标头进行响应,并使用bytes和文件的相应部分响应后续请求(在用户搜索时发生)。

好的,所以我从here借了一些代码然后我就有了这个:

206

现在Chrome会尝试搜索,但是当您执行此操作时,视频会停止播放,并且在页面重新加载之前不会再次运行。哎呀。所以我决定玩弄卷曲来看看发生了什么,我发现了这个:

  

$ curl --header“Range:bytes = 200-400”http://localhost:8080/videos/1/001.mp4    ftypisomisomiso2avc1mp41 moovlmvhd @ trak\ttkh

     

$ curl --header“Range:bytes = 1200-1400”http://localhost:8080/videos/1/001.mp4    ftypisomisomiso2avc1mp41 moovlmvhd @ trak\ttkh

无论字节范围请求如何,数据始终从文件的开头开始。返回适当的字节数(在这种情况下为201字节),但始终是从文件的开头。显然nginx尊重video = Video.find(params[:id]) file_begin = 0 file_size = File.stat(video.location).size file_end = file_size - 1 if !request.headers["Range"] status_code = :ok else status_code = :partial_content match = request.headers['Range'].match(/bytes=(\d+)-(\d*)/) if match file_begin = match[1] file_end = match[2] if match[2] && !match[2].empty? end response.header["Content-Range"] = "bytes " + file_begin.to_s + "-" + file_end.to_s + "/" + file_size.to_s end response.header["Content-Length"] = (file_end.to_i - file_begin.to_i + 1).to_s response.header["Accept-Ranges"]= "bytes" response.header["Content-Transfer-Encoding"] = "binary" send_file(video.location, :filename => File.basename(video.location), :type => 'video/mp4', :disposition => "inline", :status => status_code, :stream => 'true', :buffer_size => 4096) 标题,但忽略了Content-Length标题。

我的Content-Range未触及默认值:

nginx.conf

我的app.conf很基本:

user www-data;
worker_processes 4;
pid /run/nginx.pid;

events {
        worker_connections 768;
}

http {
        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        gzip on;
        gzip_disable "msie6";

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}

首先我尝试了Ubuntu 14.04附带的nginx 1.4.x,然后从ppa尝试了1.7.x - 结果相同。我甚至尝试过apache2并得到完全相同的结果。

我想重申视频文件问题。如果我将其放入upstream unicorn { server unix:/tmp/unicorn.app.sock fail_timeout=0; } server { listen 80 default deferred; root /vagrant/public; try_files $uri/index.html $uri @unicorn; location @unicorn { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header HOST $http_host; proxy_redirect off; proxy_pass http://unicorn; } error_page 500 502 503 504 /500.html; client_max_body_size 4G; keepalive_timeout 5; } ,那么nginx会使用相应的mime类型,标题以及Chrome正常工作所需的一切来为其提供服务。

所以我的问题是两个人:

  • 为什么nginx / apache不能像public send_file / X-Accel-Redirect一样自动处理所有这些内容,就像从{{1}静态提供文件一样}}?在铁轨中处理这些东西是如此倒退。

  • 我怎样才能真正使用带有nginx(或apache)的send_file,这样Chrome会很高兴并且允许搜索?


  

更新1

好吧,所以我想我会尝试将rails的复杂性从图片中删除,然后看看我是否可以让nginx正确地代理文件。所以我开了一个简单的nodjs服务器:

X-Sendfile

镀铬很高兴作为一个蛤蜊​​。 = / public甚至表明nginx正在自动插入var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, { 'X-Accel-Redirect': '/path/to/file.mp4' }); res.end(); }).listen(3000, '127.0.0.1'); console.log('Server running at http://127.0.0.1:3000/'); curl -I - 因为应该。有什么能阻止nginx做这件事吗?


  

更新2

我可能会越走越近......

如果我有:

Accept-Ranges: bytes

然后我得到:

Content-Type: video/mp4

我有上述所有问题。

但如果我有:

def show
  video = Video.find(params[:id])
  send_file video.location
end

然后我得到:

$ curl -I localhost:8080/videos/1/001.mp4
HTTP/1.1 200 OK
Server: nginx/1.7.9
Date: Sun, 18 Jan 2015 12:06:38 GMT
Content-Type: application/octet-stream
Connection: keep-alive
Status: 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Disposition: attachment; filename="001.mp4"
Content-Transfer-Encoding: binary
Cache-Control: private
Set-Cookie: request_method=HEAD; path=/
X-Meta-Request-Version: 0.3.4
X-Request-Id: cd80b6e8-2eaa-4575-8241-d86067527094
X-Runtime: 0.041953

一切都很完美。

但为什么呢?那些应该做同样的事情。为什么nginx在这里没有自动设置def show video = Video.find(params[:id]) response.headers['X-Accel-Redirect'] = video.location head :ok end ,就像它对简单的nodejs示例一样?我设置了$ curl -I localhost:8080/videos/1/001.mp4 HTTP/1.1 200 OK Server: nginx/1.7.9 Date: Sun, 18 Jan 2015 12:06:02 GMT Content-Type: text/html Content-Length: 186884698 Last-Modified: Sun, 18 Jan 2015 03:49:30 GMT Connection: keep-alive Cache-Control: max-age=0, private, must-revalidate Set-Cookie: request_method=HEAD; path=/ ETag: "54bb2d4a-b23a25a" Accept-Ranges: bytes 。我在Content-Typeconfig.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'之间来回移动了相同的结果。我想我从来没有提过......这是rails 4.2.0。


  

更新3

现在我已经将我的独角兽服务器改为侦听端口3000(因为我已经更改了nginx以在3000上侦听nodejs示例)。现在我可以直接向独角兽发出请求(因为它正在侦听端口而不是套接字)所以我发现application.rb直接向unicorn显示没有发送development.rb标头而只是{{1}直接实际发送unicorn文件。就像curl -I没有按照预期做的那样。

1 个答案:

答案 0 :(得分:6)

终于得到了原始问题的答案。我没想到我会来这里。我所有的研究都导致了死胡同,黑客的非解决方案和#34;它只是开箱即用&#34; (好吧,不适合我)。

  

为什么nginx / apache不会像使用send_file(X-Accel-Redirect / X-Sendfile)一样自动处理所有这些内容,就像从公共场所静态提供文件一样?在轨道中处理这些东西是如此倒退。

他们这样做,但必须正确配置它们以取悦Rack :: Sendfile(见下文)。试图在rails中处理这个问题是一个非常糟糕的解决方案。

  

我怎样才能真正使用带有nginx(或apache)的send_file,这样Chrome会很高兴并且允许搜索?

Rack::Sendfile的评论中,我非常绝望地开始讨论机架源代码以及我找到答案的地方。它们的结构为文档,您可以在rubydoc找到它。

无论出于何种原因,Rack::Sendfile都要求前端代理发送X-Sendfile-Type标头。在nginx的情况下,它还需要X-Accel-Mapping标头。该文档还提供了apache和lighttpd的示例。

有人会认为rails文档可以链接到Rack :: Sendfile文档,因为如果没有其他配置,send_file就无法开箱即用。也许我会提交拉取请求。

最后我只需要在app.conf中添加几行:

upstream unicorn {
  server unix:/tmp/unicorn.app.sock fail_timeout=0;
}

server {
  listen 80 default deferred;
  root /vagrant/public;
  try_files $uri/index.html $uri @unicorn;
  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header HOST $http_host;
    proxy_set_header X-Sendfile-Type X-Accel-Redirect; # ADDITION
    proxy_set_header X-Accel-Mapping /=/; # ADDITION
    proxy_redirect off;
    proxy_pass http://localhost:3000;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 4G;
  keepalive_timeout 5;
}

现在我的原始代码按预期工作:

def show
  send_file(Video.find(params[:id]).location)
end

编辑:

虽然最初有效,但在重新启动我的流浪盒之后它停止了工作,我不得不做出进一步的改变:

upstream unicorn {
  server unix:/tmp/unicorn.app.sock fail_timeout=0;
}

server {
  listen 80 default deferred;
  root /vagrant/public;
  try_files $uri/index.html $uri @unicorn;

  location ~ /files(.*) { # NEW
    internal;             # NEW
    alias $1;             # NEW
  }                       # NEW

  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header HOST $http_host;
    proxy_set_header X-Sendfile-Type X-Accel-Redirect;
    proxy_set_header X-Accel-Mapping /=/files/; # CHANGED
    proxy_redirect off;
    proxy_pass http://localhost:3000;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 4G;
  keepalive_timeout 5;
}

我发现将一个URI映射到另一个URI然后将该URI映射到磁盘上的位置完全没必要。它对我的用例毫无用处,我只是将一个映射到另一个并再次映射。 Apache和lighttpd并不需要它。但至少它有效。

我还将Mime::Type.register('video/mp4', :mp4)添加到config/initializers/mime_types.rb,以便使用正确的mime类型提供文件。