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>
This commit is contained in:
+68
@@ -0,0 +1,68 @@
|
||||
<?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);
|
||||
Reference in New Issue
Block a user