72 lines
2.4 KiB
PHP
72 lines
2.4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
const WEB_MEDIA_STREAM_ROOT = '/opt/im-server/media';
|
|
|
|
function failMediaStream(int $status, string $message): never
|
|
{
|
|
http_response_code($status);
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
header('Cache-Control: no-store');
|
|
echo $message;
|
|
exit;
|
|
}
|
|
|
|
$url = trim((string)($_GET['url'] ?? ''));
|
|
if ($url === '') {
|
|
failMediaStream(400, '缺少视频地址');
|
|
}
|
|
|
|
$path = rawurldecode((string)(parse_url($url, PHP_URL_PATH) ?: ''));
|
|
if (!preg_match('#^/fs/[0-9]+/[A-Za-z0-9_./ -]+$#', $path) || str_contains($path, '..')) {
|
|
failMediaStream(400, '视频地址不合法');
|
|
}
|
|
|
|
$root = realpath(WEB_MEDIA_STREAM_ROOT);
|
|
$fullPath = realpath(rtrim(WEB_MEDIA_STREAM_ROOT, '/') . $path);
|
|
if ($root === false || $fullPath === false || !str_starts_with($fullPath, $root . DIRECTORY_SEPARATOR) || !is_file($fullPath)) {
|
|
failMediaStream(404, '视频文件不存在');
|
|
}
|
|
|
|
$size = (int)filesize($fullPath);
|
|
$mime = (string)(mime_content_type($fullPath) ?: 'video/mp4');
|
|
if (!str_starts_with($mime, 'video/')) {
|
|
$extension = strtolower((string)pathinfo($fullPath, PATHINFO_EXTENSION));
|
|
$mime = $extension === 'webm' ? 'video/webm' : ($extension === 'mov' ? 'video/quicktime' : 'video/mp4');
|
|
}
|
|
|
|
$start = 0;
|
|
$end = max(0, $size - 1);
|
|
$range = trim((string)($_SERVER['HTTP_RANGE'] ?? ''));
|
|
if ($range !== '' && preg_match('/^bytes=(\d*)-(\d*)$/', $range, $matches)) {
|
|
if ($matches[1] !== '') $start = (int)$matches[1];
|
|
if ($matches[2] !== '') $end = min($end, (int)$matches[2]);
|
|
if ($start < 0 || $start > $end || $start >= $size) {
|
|
header('Content-Range: bytes */' . $size);
|
|
failMediaStream(416, '请求范围无效');
|
|
}
|
|
http_response_code(206);
|
|
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size);
|
|
}
|
|
|
|
$length = $end - $start + 1;
|
|
header('Content-Type: ' . $mime);
|
|
header('Content-Length: ' . $length);
|
|
header('Accept-Ranges: bytes');
|
|
header('Content-Disposition: inline');
|
|
header('Cache-Control: public, max-age=86400');
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
$stream = fopen($fullPath, 'rb');
|
|
if ($stream === false) failMediaStream(500, '视频文件无法读取');
|
|
fseek($stream, $start);
|
|
$remaining = $length;
|
|
while ($remaining > 0 && !feof($stream)) {
|
|
$chunk = fread($stream, min(1024 * 1024, $remaining));
|
|
if ($chunk === false || $chunk === '') break;
|
|
echo $chunk;
|
|
$remaining -= strlen($chunk);
|
|
flush();
|
|
}
|
|
fclose($stream);
|