b9c92540d4
Scans nested course folders for mp4s, tracks per-user resume position and per-folder progress. SPA frontend with hash routing over a JSON API; range-request streaming via stream.php. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
69 lines
2.0 KiB
PHP
69 lines
2.0 KiB
PHP
<?php
|
|
// Streams an mp4 with HTTP Range support so the player can seek.
|
|
// The file is looked up by database ID only — the client never supplies a
|
|
// filesystem path, which rules out path traversal.
|
|
require_once __DIR__ . '/includes/auth.php';
|
|
require_once __DIR__ . '/includes/functions.php';
|
|
|
|
$userId = currentUserId();
|
|
if ($userId === null) {
|
|
http_response_code(403);
|
|
exit('Forbidden');
|
|
}
|
|
|
|
$video = getVideo((int)($_GET['id'] ?? 0));
|
|
if (!$video) {
|
|
http_response_code(404);
|
|
exit('Not found');
|
|
}
|
|
|
|
$file = realpath(rtrim(VIDEOS_ROOT, '/') . '/' . $video['path']);
|
|
$root = realpath(VIDEOS_ROOT);
|
|
if ($file === false || $root === false || !str_starts_with($file, $root . DIRECTORY_SEPARATOR) || !is_file($file)) {
|
|
http_response_code(404);
|
|
exit('File missing — rescan the library');
|
|
}
|
|
|
|
$size = filesize($file);
|
|
$start = 0;
|
|
$end = $size - 1;
|
|
|
|
if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) {
|
|
if ($m[1] !== '') {
|
|
$start = (int)$m[1];
|
|
if ($m[2] !== '') $end = min((int)$m[2], $size - 1);
|
|
} elseif ($m[2] !== '') { // suffix range: last N bytes
|
|
$start = max(0, $size - (int)$m[2]);
|
|
}
|
|
if ($start > $end || $start >= $size) {
|
|
http_response_code(416);
|
|
header("Content-Range: bytes */$size");
|
|
exit;
|
|
}
|
|
http_response_code(206);
|
|
header("Content-Range: bytes $start-$end/$size");
|
|
} else {
|
|
http_response_code(200);
|
|
}
|
|
|
|
session_write_close(); // don't block other requests from this user
|
|
ignore_user_abort(false);
|
|
set_time_limit(0);
|
|
|
|
header('Content-Type: video/mp4');
|
|
header('Accept-Ranges: bytes');
|
|
header('Content-Length: ' . ($end - $start + 1));
|
|
header('Cache-Control: private, max-age=3600');
|
|
|
|
$fp = fopen($file, 'rb');
|
|
fseek($fp, $start);
|
|
$remaining = $end - $start + 1;
|
|
while ($remaining > 0 && !feof($fp) && !connection_aborted()) {
|
|
$chunk = fread($fp, min(1024 * 512, $remaining));
|
|
if ($chunk === false) break;
|
|
echo $chunk;
|
|
flush();
|
|
$remaining -= strlen($chunk);
|
|
}
|
|
fclose($fp);
|