本文目录导读:

在 PHP 中处理和生成视频流(Video Streaming),核心在于服务器端控制 HTTP 响应头,以支持范围请求(Range Requests),这使得用户可以拖动进度条、从中间开始播放,而不是一次性下载整个文件。
根据你的具体场景(是处理上传/转码后的流,还是直接提供本地视频文件),有以下几种主要方式:
直接提供本地视频文件(最常用)
如果你的视频已经存储在服务器上,并且用户想看,应该使用分块流式传输并支持 Range 请求。
<?php
// stream.php
$file = '/path/to/your/video.mp4'; // 确保文件路径正确
if (!file_exists($file)) {
header('HTTP/1.1 404 Not Found');
exit;
}
// 获取文件大小
$size = filesize($file);
$mimeType = mime_content_type($file); // 或手动指定 'video/mp4'
// 定义响应头
header('Content-Type: ' . $mimeType);
header('Accept-Ranges: bytes'); // 告知客户端支持范围请求
header('Content-Length: ' . $size); // 初始长度(可能被后面的代码修改)
// --- 处理 Range (分块请求) ---
$start = 0;
$end = $size - 1;
$isPartial = false;
if (isset($_SERVER['HTTP_RANGE'])) {
// 解析 Range 头, bytes=100-599
preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches);
$start = intval($matches[1]);
$end = $matches[2] === '' ? $size - 1 : intval($matches[2]);
// 验证范围合法性
if ($start > $end || $start >= $size) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header('Content-Range: bytes */' . $size);
exit;
}
$isPartial = true;
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
header('Content-Length: ' . ($end - $start + 1));
}
// --- 输出文件内容 ---
$fp = fopen($file, 'rb');
if ($fp === false) {
header('HTTP/1.1 500 Internal Server Error');
exit;
}
// 移动指针到开始位置
fseek($fp, $start);
$bufferSize = 8192; // 8KB 块大小
$remaining = $end - $start + 1;
set_time_limit(0); // 防止大文件超时
while (!feof($fp) && $remaining > 0) {
$readSize = min($bufferSize, $remaining);
echo fread($fp, $readSize);
$remaining -= $readSize;
flush(); // 立刻发送到客户端(关键)
ob_flush(); // 如果开启了输出缓冲
}
fclose($fp);
exit;
?>
使用方式: 在 HTML5 <video> 标签中直接引用:
<video controls> <source src="stream.php" type="video/mp4"> </video>
处理实时/动态生成的视频流(如摄像头、转码输出)
如果你需要从外部进程实时获取数据并推送给用户(ffmpeg 实时转码),需要使用分块传输编码(Chunked Transfer Encoding)。
<?php
// realtime_stream.php
header('Content-Type: video/mp4'); // 或其他类型
header('Transfer-Encoding: chunked');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
set_time_limit(0);
ob_implicit_flush(true);
// 示例:调用 ffmpeg 推送实时视频
$cmd = 'ffmpeg -i rtmp://your_source -f mp4 pipe:1 2>/dev/null';
$handle = popen($cmd, 'r');
if ($handle === false) {
header('HTTP/1.1 500 Internal Server Error');
exit;
}
while (!feof($handle)) {
$buffer = fread($handle, 8192);
if ($buffer !== false && strlen($buffer) > 0) {
// 发送分块
echo dechex(strlen($buffer)) . "\r\n";
echo $buffer . "\r\n";
flush();
}
}
// 发送结束块
echo "0\r\n\r\n";
pclose($handle);
?>
注意: 此方法不支持进度条跳跃(因为没有 Range 支持),仅适用于直播或无法确定内容总长度的场景。
使用 PHP 框架或专用方案(推荐)
如果不想手写上述逻辑,可以使用现成方案:
-
Symfony HTTP Foundation:提供了
BinaryFileResponse,支持Range请求。use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\Request; $response = new BinaryFileResponse('/path/to/video.mp4'); $response->setContentDisposition('inline'); return $response; -
Laravel:返回文件流。
return response()->file('/path/to/video.mp4'); -
Nginx 内部流(性能最优):让 Nginx 直接处理静态文件,PHP 只负责权限验证。
# nginx 配置 location /videos/ { internal; # 只允许内部重定向访问 alias /var/www/storage/videos/; }// PHP 代码 header('Content-Type: video/mp4'); header('X-Accel-Redirect: /videos/' . $filename); // 由 nginx 直接发送 exit;
| 特性 | 普通文件流(支持拖动) | 实时流(不支持拖动) |
|---|---|---|
| HTTP协议 | Range 头 + 206 Partial Content |
Transfer-Encoding: chunked |
| 内存占用 | 低(分块读取) | 较低 |
| 进度条跳跃 | ✅ 支持 | ❌ 不支持 |
| 典型应用 | 点播视频、课程视频 | 摄像头直播、动态转码 |
| 性能瓶颈 | fread/fwrite |
外部进程(ffmpeg)I/O |
避坑指南
- 不要用
readfile()或file_get_contents()直接返回大视频:它们会把整个文件载入内存,导致服务器 OOM(内存溢出),且不支持Range。 flush()非常重要:如果不调用flush(),PHP 会缓冲到脚本结束才发送,失去流式传输的意义。- 浏览器缓存:对于点播视频,可以配置
Cache-Control: public和ETag让浏览器减少请求。 - 安全:如果视频需要权限控制,务必在 PHP 脚本中进行身份验证(如 Session、JWT),不要将视频目录直接暴露给公网。