如何在 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。