190 lines
4.4 KiB
PHP
190 lines
4.4 KiB
PHP
<?php
|
|
/**
|
|
* serve_media.php - Serves media files with proper headers for streaming + Range
|
|
*/
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/auth.php';
|
|
require_auth(false); // plain text is fine for media endpoint
|
|
set_time_limit(0);
|
|
|
|
// ---- Config ----
|
|
$mediaRoot = getenv('MEDIA_ROOT');
|
|
if ($mediaRoot === false || trim($mediaRoot) === '') {
|
|
http_response_code(500);
|
|
echo 'MEDIA_ROOT is not set';
|
|
exit;
|
|
}
|
|
|
|
$baseDir = realpath(trim($mediaRoot));
|
|
if ($baseDir === false || !is_dir($baseDir)) {
|
|
http_response_code(500);
|
|
echo 'MEDIA_ROOT base directory not found';
|
|
exit;
|
|
}
|
|
|
|
// ---- Input ----
|
|
$requested = $_GET['file'] ?? '';
|
|
if (!is_string($requested) || $requested === '') {
|
|
http_response_code(400);
|
|
echo 'No file specified';
|
|
exit;
|
|
}
|
|
|
|
// Normalize separators (avoid backslash weirdness)
|
|
$requested = str_replace(['\\'], '/', $requested);
|
|
|
|
// Compute real paths and enforce base directory containment
|
|
$realBase = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
|
$realFile = realpath($baseDir . DIRECTORY_SEPARATOR . ltrim($requested, "/"));
|
|
|
|
if ($realFile === false) {
|
|
http_response_code(404);
|
|
echo 'File not found';
|
|
exit;
|
|
}
|
|
|
|
if (strpos($realFile, $realBase) !== 0) {
|
|
http_response_code(403);
|
|
echo 'Access denied';
|
|
exit;
|
|
}
|
|
|
|
if (!is_file($realFile) || !is_readable($realFile)) {
|
|
http_response_code(404);
|
|
echo 'File not found';
|
|
exit;
|
|
}
|
|
|
|
// Disable output buffering to prevent Content-Length mismatch
|
|
while (ob_get_level() > 0) {
|
|
ob_end_clean();
|
|
}
|
|
|
|
// Mime types (extend as needed)
|
|
$ext = strtolower(pathinfo($realFile, PATHINFO_EXTENSION));
|
|
$mime = [
|
|
'mp4' => 'video/mp4',
|
|
'mkv' => 'video/x-matroska',
|
|
'avi' => 'video/x-msvideo',
|
|
'mov' => 'video/quicktime',
|
|
'wmv' => 'video/x-ms-wmv',
|
|
'flv' => 'video/x-flv',
|
|
'webm' => 'video/webm',
|
|
'm4v' => 'video/x-m4v',
|
|
'mp3' => 'audio/mpeg',
|
|
'wav' => 'audio/wav',
|
|
'ogg' => 'audio/ogg',
|
|
'flac' => 'audio/flac',
|
|
'm4a' => 'audio/mp4',
|
|
'aac' => 'audio/aac',
|
|
'wma' => 'audio/x-ms-wma',
|
|
'opus' => 'audio/opus',
|
|
][$ext] ?? 'application/octet-stream';
|
|
|
|
$size = filesize($realFile);
|
|
if ($size === false) {
|
|
http_response_code(500);
|
|
echo 'Failed to stat file';
|
|
exit;
|
|
}
|
|
|
|
// Always advertise range support
|
|
header('Accept-Ranges: bytes');
|
|
header('Content-Type: ' . $mime);
|
|
header('Cache-Control: public, max-age=3600');
|
|
|
|
$rangeHeader = $_SERVER['HTTP_RANGE'] ?? '';
|
|
if ($rangeHeader === '') {
|
|
// Full response
|
|
header('Content-Length: ' . $size);
|
|
http_response_code(200);
|
|
|
|
$fp = fopen($realFile, 'rb');
|
|
if ($fp === false) {
|
|
http_response_code(500);
|
|
echo 'Failed to open file';
|
|
exit;
|
|
}
|
|
|
|
$buf = 1024 * 256; // 256KB chunks
|
|
while (!feof($fp) && !connection_aborted()) {
|
|
$data = fread($fp, $buf);
|
|
if ($data === false) break;
|
|
echo $data;
|
|
}
|
|
fclose($fp);
|
|
exit;
|
|
}
|
|
|
|
// ---- Range response (single-range only) ----
|
|
if (!preg_match('/^bytes=(\d*)-(\d*)$/', trim($rangeHeader), $m)) {
|
|
http_response_code(416);
|
|
header("Content-Range: bytes */{$size}");
|
|
exit;
|
|
}
|
|
|
|
$startRaw = $m[1];
|
|
$endRaw = $m[2];
|
|
|
|
if ($startRaw === '' && $endRaw === '') {
|
|
http_response_code(416);
|
|
header("Content-Range: bytes */{$size}");
|
|
exit;
|
|
}
|
|
|
|
if ($startRaw === '') {
|
|
// Suffix range: last N bytes
|
|
$suffixLen = (int)$endRaw;
|
|
if ($suffixLen <= 0) {
|
|
http_response_code(416);
|
|
header("Content-Range: bytes */{$size}");
|
|
exit;
|
|
}
|
|
$start = max(0, $size - $suffixLen);
|
|
$end = $size - 1;
|
|
} else {
|
|
$start = (int)$startRaw;
|
|
$end = ($endRaw === '') ? ($size - 1) : (int)$endRaw;
|
|
}
|
|
|
|
if ($start < 0 || $end < $start || $end >= $size) {
|
|
http_response_code(416);
|
|
header("Content-Range: bytes */{$size}");
|
|
exit;
|
|
}
|
|
|
|
$length = $end - $start + 1;
|
|
|
|
http_response_code(206);
|
|
header("Content-Range: bytes {$start}-{$end}/{$size}");
|
|
header("Content-Length: {$length}");
|
|
|
|
$fp = fopen($realFile, 'rb');
|
|
if ($fp === false) {
|
|
http_response_code(500);
|
|
echo 'Failed to open file';
|
|
exit;
|
|
}
|
|
|
|
if (fseek($fp, $start) !== 0) {
|
|
fclose($fp);
|
|
http_response_code(500);
|
|
echo 'Failed to seek';
|
|
exit;
|
|
}
|
|
|
|
$buf = 1024 * 256;
|
|
$remaining = $length;
|
|
|
|
while ($remaining > 0 && !feof($fp) && !connection_aborted()) {
|
|
$read = min($buf, $remaining);
|
|
$data = fread($fp, $read);
|
|
if ($data === false || $data === '') break;
|
|
echo $data;
|
|
$remaining -= strlen($data);
|
|
}
|
|
|
|
fclose($fp);
|
|
exit;
|
|
|