我不能用我的生命将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-Type
和config.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
没有按照预期做的那样。
答案 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类型提供文件。