如何在 html 文件夹之外无错误地流式传输视频?
How to stream video, with no errors, when outside the html folder?
On ubuntu 16 这是在 /html 文件夹外的 var/www/uploads 中,目前为 chmod 777(测试)。如果您在下载视频时尝试暂停,它将播放然后出现错误:
image.php
<?php
$filename = $_GET['filename'];
header('Content-Type: video/mp4');
readfile("../uploads/" . $filename);
?>
html
<video id="my_video_1" class="video-js vjs-default-skin" width="100%" height="100%"
controls preload="none" poster='img.png'
data-setup='{ "playbackRates": [1, 1.5, 2] }'>
<source src="image.php?filename=myfile.mp4" type='video/mp4' />
</video>
有效,但在 www/html/uploads 内,chmod 777。完全没有错误。这是不好的做法:
<video id="my_video_1" class="video-js vjs-default-skin" width="100%" height="100%"
controls preload="none" poster='img.png'
data-setup='{ "playbackRates": [1, 1.5, 2] }'>
<source src="uploads/myfile.mp4" type='video/mp4' />
</video>
您还想用 mp4 做什么来阻止这种情况发生?
您需要将视频放到 Web 服务器
可以访问的 public 目录(例如在 symfony 框架中它是目录 web/uploads/
),
之后,您将能够在视频标签的 src 参数中使用视频(就像您在第二个示例中所做的那样)。
如果您已经有将视频放到目录 var/www/uploads
的上传系统 - 您必须将此类视频移动到 public 目录,可能您使用了一些工作人员或其他东西。
你这样做是因为从 php 流式传输视频内容 - 这是错误的方式...
而且你不能使用 777
mod 作为你的视频目录,你必须使用 755
.
最好的方法是使用 "byte range" headers - 这个 returns 只是你需要的文件块。维基百科有一个非常简短的介绍 (https://en.wikipedia.org/wiki/Byte_serving),但您可以 google 了解更多。
这是我为我的项目编写的函数 - 您可能需要对其进行调整才能满足您的具体需求,但它非常通用,可能开箱即用。
function serve_file_resumable ($file, $contenttype = 'application/octet-stream') {
// Avoid sending unexpected errors to the client - we should be serving a file,
// we don't want to corrupt the data we send
@error_reporting(0);
// Make sure the files exists, otherwise we are wasting our time
if (!file_exists($file)) {
header("HTTP/1.1 404 Not Found");
exit;
}
// Get the 'Range' header if one was sent
if (isset($_SERVER['HTTP_RANGE'])) {
$range = $_SERVER['HTTP_RANGE']; // IIS/Some Apache versions
} else if ($apache = apache_request_headers()) { // Try Apache again
$headers = array();
foreach ($apache as $header => $val) {
$headers[strtolower($header)] = $val;
}
if (isset($headers['range'])) {
$range = $headers['range'];
} else {
$range = false; // We can't get the header/there isn't one set
}
} else {
$range = false; // We can't get the header/there isn't one set
}
// Get the data range requested (if any)
$filesize = filesize($file);
if ($range) {
$partial = true;
list($param,$range) = explode('=',$range);
if (strtolower(trim($param)) != 'bytes') { // Bad request - range unit is not 'bytes'
header("HTTP/1.1 400 Invalid Request");
exit;
}
$range = explode(',',$range);
$range = explode('-',$range[0]); // We only deal with the first requested range
if (count($range) != 2) { // Bad request - 'bytes' parameter is not valid
header("HTTP/1.1 400 Invalid Request");
exit;
}
if ($range[0] === '') { // First number missing, return last $range[1] bytes
$end = $filesize - 1;
$start = $end - intval($range[1]);
} else if ($range[1] === '') { // Second number missing, return from byte $range[0] to end
$start = intval($range[0]);
$end = $filesize - 1;
} else { // Both numbers present, return specific range
$start = intval($range[0]);
$end = intval($range[1]);
if ($end >= $filesize || (!$start && (!$end || $end == ($filesize - 1)))) {
$partial = false; // Invalid range/whole file specified, return whole file
}
}
$length = $end - $start + 1;
} else {
$partial = false; // No range requested
$length = $filesize;
}
// Send standard headers
header("Content-Type: $contenttype");
header("Content-Length: $length"); // was $filesize
header('Content-Disposition: attachment; filename="'.basename($file).'"');
header('Accept-Ranges: bytes');
// if requested, send extra headers and part of file...
if ($partial) {
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$filesize");
if (!$fp = fopen($file, 'r')) { // Error out if we can't read the file
header("HTTP/1.1 500 Internal Server Error");
exit;
}
if ($start) {
fseek($fp,$start);
}
while ($length) { // Read in blocks of 8KB so we don't chew up memory on the server
$read = ($length > 8192) ? 8192 : $length;
$length -= $read;
print(fread($fp,$read));
}
fclose($fp);
} else {
readfile($file); // ...otherwise just send the whole file
}
// Exit here to avoid accidentally sending extra content on the end of the file
exit;
}
serve_file_resumable ("../uploads/" . $filename, 'video/mp4');
如果您使用的是 Apache,则可以使用 X-Sendfile
header 来提供任何文件,包括不在可公开访问的目录中的文件。
示例:
$filename = $_GET['filename'];
header('Content-Type: video/mp4');
header('X-Sendfile: ../uploads/'.$filename);
通常默认不启用,所以需要在httpd.conf
中添加:
LoadModule xsendfile_module path/to/mod_xsendfile.so
根据需要调整路径并将其添加到您的 .htaccess
:
XSendFile on
重新启动 Apache,一切顺利。
此功能并非 Apache 独有。
事实上,这个想法来自于Lighttpd。
On ubuntu 16 这是在 /html 文件夹外的 var/www/uploads 中,目前为 chmod 777(测试)。如果您在下载视频时尝试暂停,它将播放然后出现错误:
image.php
<?php
$filename = $_GET['filename'];
header('Content-Type: video/mp4');
readfile("../uploads/" . $filename);
?>
html
<video id="my_video_1" class="video-js vjs-default-skin" width="100%" height="100%"
controls preload="none" poster='img.png'
data-setup='{ "playbackRates": [1, 1.5, 2] }'>
<source src="image.php?filename=myfile.mp4" type='video/mp4' />
</video>
有效,但在 www/html/uploads 内,chmod 777。完全没有错误。这是不好的做法:
<video id="my_video_1" class="video-js vjs-default-skin" width="100%" height="100%"
controls preload="none" poster='img.png'
data-setup='{ "playbackRates": [1, 1.5, 2] }'>
<source src="uploads/myfile.mp4" type='video/mp4' />
</video>
您还想用 mp4 做什么来阻止这种情况发生?
您需要将视频放到 Web 服务器
可以访问的 public 目录(例如在 symfony 框架中它是目录 web/uploads/
),
之后,您将能够在视频标签的 src 参数中使用视频(就像您在第二个示例中所做的那样)。
如果您已经有将视频放到目录 var/www/uploads
的上传系统 - 您必须将此类视频移动到 public 目录,可能您使用了一些工作人员或其他东西。
你这样做是因为从 php 流式传输视频内容 - 这是错误的方式...
而且你不能使用 777
mod 作为你的视频目录,你必须使用 755
.
最好的方法是使用 "byte range" headers - 这个 returns 只是你需要的文件块。维基百科有一个非常简短的介绍 (https://en.wikipedia.org/wiki/Byte_serving),但您可以 google 了解更多。
这是我为我的项目编写的函数 - 您可能需要对其进行调整才能满足您的具体需求,但它非常通用,可能开箱即用。
function serve_file_resumable ($file, $contenttype = 'application/octet-stream') {
// Avoid sending unexpected errors to the client - we should be serving a file,
// we don't want to corrupt the data we send
@error_reporting(0);
// Make sure the files exists, otherwise we are wasting our time
if (!file_exists($file)) {
header("HTTP/1.1 404 Not Found");
exit;
}
// Get the 'Range' header if one was sent
if (isset($_SERVER['HTTP_RANGE'])) {
$range = $_SERVER['HTTP_RANGE']; // IIS/Some Apache versions
} else if ($apache = apache_request_headers()) { // Try Apache again
$headers = array();
foreach ($apache as $header => $val) {
$headers[strtolower($header)] = $val;
}
if (isset($headers['range'])) {
$range = $headers['range'];
} else {
$range = false; // We can't get the header/there isn't one set
}
} else {
$range = false; // We can't get the header/there isn't one set
}
// Get the data range requested (if any)
$filesize = filesize($file);
if ($range) {
$partial = true;
list($param,$range) = explode('=',$range);
if (strtolower(trim($param)) != 'bytes') { // Bad request - range unit is not 'bytes'
header("HTTP/1.1 400 Invalid Request");
exit;
}
$range = explode(',',$range);
$range = explode('-',$range[0]); // We only deal with the first requested range
if (count($range) != 2) { // Bad request - 'bytes' parameter is not valid
header("HTTP/1.1 400 Invalid Request");
exit;
}
if ($range[0] === '') { // First number missing, return last $range[1] bytes
$end = $filesize - 1;
$start = $end - intval($range[1]);
} else if ($range[1] === '') { // Second number missing, return from byte $range[0] to end
$start = intval($range[0]);
$end = $filesize - 1;
} else { // Both numbers present, return specific range
$start = intval($range[0]);
$end = intval($range[1]);
if ($end >= $filesize || (!$start && (!$end || $end == ($filesize - 1)))) {
$partial = false; // Invalid range/whole file specified, return whole file
}
}
$length = $end - $start + 1;
} else {
$partial = false; // No range requested
$length = $filesize;
}
// Send standard headers
header("Content-Type: $contenttype");
header("Content-Length: $length"); // was $filesize
header('Content-Disposition: attachment; filename="'.basename($file).'"');
header('Accept-Ranges: bytes');
// if requested, send extra headers and part of file...
if ($partial) {
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$filesize");
if (!$fp = fopen($file, 'r')) { // Error out if we can't read the file
header("HTTP/1.1 500 Internal Server Error");
exit;
}
if ($start) {
fseek($fp,$start);
}
while ($length) { // Read in blocks of 8KB so we don't chew up memory on the server
$read = ($length > 8192) ? 8192 : $length;
$length -= $read;
print(fread($fp,$read));
}
fclose($fp);
} else {
readfile($file); // ...otherwise just send the whole file
}
// Exit here to avoid accidentally sending extra content on the end of the file
exit;
}
serve_file_resumable ("../uploads/" . $filename, 'video/mp4');
如果您使用的是 Apache,则可以使用 X-Sendfile
header 来提供任何文件,包括不在可公开访问的目录中的文件。
示例:
$filename = $_GET['filename'];
header('Content-Type: video/mp4');
header('X-Sendfile: ../uploads/'.$filename);
通常默认不启用,所以需要在httpd.conf
中添加:
LoadModule xsendfile_module path/to/mod_xsendfile.so
根据需要调整路径并将其添加到您的 .htaccess
:
XSendFile on
重新启动 Apache,一切顺利。
此功能并非 Apache 独有。 事实上,这个想法来自于Lighttpd。