Files
coursehub/stream.php
T
Harwinder Sekhon b9c92540d4 CourseHub: AJAX video course player (PHP + MySQL + Bootstrap + Plyr)
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>
2026-07-11 10:25:35 -04:00

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);