HTML5视频和部分范围HTTP请求

16

我正在尝试修改一个自定义的Web服务器应用程序以支持HTML5视频。

它提供一个带有基本<video>标签的HTML5页面,然后需要处理实际内容的请求。

到目前为止,我能让它工作的唯一方法是将整个视频文件加载到内存中,然后以单个响应发送回来。这不是一个实际的选项。我想逐块提供它:发送回,比如说100 kb,等待浏览器请求更多。

我看到了一个具有以下头文件的请求:

http_version = 1.1
request_method = GET

Host = ###.###.###.###:##
User-Agent = Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20100101 Firefox/16.0
Accept = video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5
Accept-Language = en-US,en;q=0.5
Connection = keep-alive
Range = bytes=0-

我尝试发送部分内容响应:

HTTP/1.1 206 Partial content
Content-Type: video/mp4
Content-Range: bytes 0-99999 / 232725251
Content-Length: 100000

我收到了几个GET请求,如下所示

Cache-Control = no-cache
Connection = Keep-Alive
Pragma = getIfoFileURI.dlna.org
Accept = */*
User-Agent = NSPlayer/12.00.7601.17514 WMFSDK/12.00.7601.17514
GetContentFeatures.DLNA.ORG = 1
Host = ###.###.###.###:##

无论我向浏览器发送什么内容,视频都不会播放(没有任何指示浏览器想要文件的特定部分)。

如上所述,如果我尝试在同一HTTP数据包中一次性发送整个230 MB文件,则同一视频将��确播放。

是否有办法通过部分内容请求使所有内容正常工作?我正在使用Firefox进行测试,但最终需要在所有浏览器上运行。


到目前为止,我在浏览器端尝试了Firefox(带有H264插件)和IE9,以及服务器端的MP4和WebM。我认为我还缺少一些东西。 - Eugene Smith
为什么要从客户端获取部分范围请求?只需允许他们请求所需内容,并以最方便的方式发送给他们即可。我曾经使用流式音频(从Chrome请求),并使用自定义服务器 - 它实时发送声音。我认为这是默认和可接受的方式。如果您担心网络负载,请使用限速降低到足以流畅播放视频的速度。 - Stan
请不要忘记在回复中提到 @用户名 ;-). 我几乎用音频做了同样的事情 - 实时流式传输,服务器上没有真正的音频文件。我不明白在这种情况下如何请求部分内容?无论如何,我认为您应该遵循大多数用户代理实现HTML5媒体标签支持的方式。虽然我没有为此测试不同的浏览器,但您可以尝试一下。 - Stan
1
@Stan,大多数用户代理如何实现HTML5媒体标签?无法在任何地方找到好的文档。 - Eugene Smith
很遗憾,我不知道实现的内部细节。我们所拥有的只是可观察到的行为。根据HTML5规范,在videosource标签中没有任何属性可以帮助管理部分内容传递。因此,我认为媒体交互的这一部分完全由浏览器的责任承担(至少目前是这样)。如果您想更好地控制内容传递,您应该开发一个自定义机制,利用一些服务器端软件:这样您就可以请求并响应具有偏移量和长度限制的视频。 - Stan
显示剩余2条评论
2个回答

8

我知道这是一个旧问题,但如果有帮助的话,你可以尝试我们在代码库中使用的以下“模型”。

class Model_DownloadableFile {
private $full_path;

function __construct($full_path) {
    $this->full_path = $full_path;
}

public function get_full_path() {
    return $this->full_path;
}

// Function borrowed from (been cleaned up and modified slightly): https://dev59.com/o3VC5IYBdhLWcg3w4VRz#4451376
// Allows for resuming paused downloads etc
public function download_file_in_browser() {
    // 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($this->full_path)) {
        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($this->full_path);
    $length = $filesize;
    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[0]);
        } 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
    }

    // Determine the content type
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $contenttype = finfo_file($finfo, $this->full_path);
    finfo_close($finfo);

    // Send standard headers
    header("Content-Type: $contenttype");
    header("Content-Length: $length");
    header('Content-Disposition: attachment; filename="' . basename($this->full_path) . '"');
    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($this->full_path, '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($this->full_path); // ...otherwise just send the whole file
    }

    // Exit here to avoid accidentally sending extra content on the end of the file
    exit;
}
}

你可以这样使用它:
(new Model_DownloadableFile('FULL/PATH/TO/FILE'))->download_file_in_browser();

它将处理发送文件的部分或完整文件等,在这种情况下对我们非常有效,也适用于许多其他情况。希望它有所帮助。


1
读取的那一行 header("Content-Length: $filesize"); 实际上应该改为 header("Content-Length: $length"); 因为它表示传输的总字节数,而不是文件大小。如果设置不正确,Chrome的视频播放器会出现问题。 - frankieandshadow
好的,我已经更正了原始帖子。只有在$range为真时才设置$length,因此我确保它首先设置为$filesize,并在$range为真时被覆盖。 - Luke Cousins
我的问题是,即使调用了header_remove('Content-Type'),php仍然返回Content-Type: text/html。 - B.F.
@B.F. 不应该是这样的。你怎么知道它是这样的呢?如果你得到了那个头部,你知道它是 PHP 而不是其他东西,比如 Apache 或 Nginx 吗?你为什么要首先删除头部?为什么不使用 header() 函数设置你想要的头部呢? - Luke Cousins
@Luke Cousins 感谢您的回复!我从php.ini中删除了默认标题,但还有很多其他东西。所以我不知道错误在哪里。关于您的脚本:您应该使用header('HTTP/1.1 416 Requested Range Not Satisfiable1')来回答格式错误的范围请求。如果您想给用户第二次机会:header('Content-Range: bytes */'.filesize($file),true); - B.F.

4

我需要部分范围请求,因为我将进行实时转码,不能在请求时完全转码并提供文件。

对于您尚不知道完整正文内容的响应(无法猜测Content-Length,即现场编码),请使用块编码:

HTTP/1.1 200 OK
Content-Type: video/mp4
Transfer-Encoding: chunked
Trailer: Expires

1E; 1st chunk
...binary....data...chunk1..my
24; 2nd chunk
video..binary....data....chunk2..con
22; 3rd chunk
tent...binary....data....chunk3..a
2A; 4th chunk
nd...binary......data......chunk4...etc...
0
Expires: Wed, 21 Oct 2015 07:28:00 GMT

每个数据块在可用时发送:当编码了几帧或输出缓冲区已满时,会生成100kB等大小的块。
22; 3rd chunk
tent...binary....data....chunk3..a

其中,22表示以十六进制表示的块字节长度(0x22 = 34字节),; 3rd chunk是额外的块信息(可选的),tent...binary....data....chunk3..a是块的内容。

然后,在编码完成并发送所有块时,以以下方式结束:

0
Expires: Wed, 21 Oct 2015 07:28:00 GMT

其中,0表示没有更多的数据块,随后是零个或多个拖车(允许在标头中定义的字段),以提供校验和或数字签名等信息(Trailer: ExpiresExpires: Wed, 21 Oct 2015 07:28:00 GMT并非必须)。

如果文件已经生成(没有实时编码),那么下面是服务器响应的相应内容:

HTTP/1.1 200 OK
Content-Type: video/mp4
Content-Length: 142
Expires: Wed, 21 Oct 2015 07:28:00 GMT

...binary....data...chunk1..myvideo..binary....data....chunk2..content...binary....data....chunk3..and...binary......data......chunk4...etc...

更多信息请参考:分块传输编码 - 维基百科Trailer - HTTP | MDN


我认为视频标签不支持分块编码(至少在Chrome中不支持)。 - themihai
1
HTTP/1.1支持需要分块传输编码。此外,我尝试使用分块传输编码在Chrome和Firefox上播放视频效果良好。 Safari(桌面版和iOS版)需要范围支持才能播放视频(对于音频不是必需的)。传输编码和范围并不冲突,但字节范围需要提前知道字节大小。 为此,您可以使用实时流协议(但在编码视频时需要特殊处理)如HLS或APIs如MSE。 请参见hls.js - mems
Chrome会发出部分请求,无论服务器是否支持它,因此我认为仅有分块编码而没有部分/范围支持是不足以提供HTML5视频的。如果服务器不广告内容的总长度(即提供通配符*),播放器在第一个部分请求完成后停止,而不是请求下一部分,所以我猜这是Chrome的一个bug。 - themihai
当您使用分块传输编码时,您不必提供Content-Length头(注意:“*”是此标头的无效值)。 我进行了一个测试用例,在其中视频在Chrome,Firefox和IE上加载(至少是IE10)。请参见https://gist.github.com/mems/b121bba11adc48dfd068bb7a8d113a8c(如果要更快地加载视频,请删除“usleep(200000)”) - mems
如果在Chrome(Mac上)无法正常工作。如果未提供范围标头,则不会播放任何视频。 - themihai

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接