install.php now takes the videos path (form field / --videos= CLI flag), persists it into config.php, and creates the directory if missing. config.example.php default changed from a dev path to /srv/courses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CourseHub
Self-hosted course video player: PHP 8 + MySQL/MariaDB + Bootstrap 5 + Plyr.
Single-page app: index.php is a static shell; all navigation, login, browsing,
playback, and scanning happen over AJAX against the JSON API in api/
(auth.php, library.php, progress.php, scan.php), with hash-based routes
(#/, #/folder/N, #/watch/N) so views switch without page reloads and
browser back/forward still work. Only stream.php (the mp4 bytes) is a plain
HTTP resource.
- Recursively scans a videos directory for
.mp4files and mirrors the folder hierarchy (courses → sections → videos, any nesting depth). - Per-user resume: log in from any device and continue where you left off.
- Progress tracking at every level — each folder shows completed/total and a percentage bar computed over its whole subtree.
- "Continue watching" strip, auto-play next video, playback speed, keyboard
shortcuts (Plyr), completed checkmarks, natural sorting (
Video2beforeVideo10). - Seekable streaming via HTTP Range requests; files are addressed by DB id only (no path traversal).
Layout on disk
videos/
├── Course 1/
│ ├── Intro/
│ │ ├── Video1.mp4
│ │ └── Video2.mp4
│ └── Module 2/
│ └── Advanced/
│ └── lesson.mp4
└── Course 2/
└── ...
Top-level folders become courses on the home page.
Setup
-
Database — create the schema and an app user:
sudo mysql < schema.sql sudo mysql -e "CREATE USER IF NOT EXISTS 'coursehub'@'localhost' IDENTIFIED BY 'coursehub'; GRANT ALL ON coursehub.* TO 'coursehub'@'localhost';" -
Configure — edit
config.php:VIDEOS_ROOT: absolute path to your course folders (must be readable by the web server / PHP user).- DB credentials (change the password in production).
ALLOW_REGISTRATION: setfalseonce your users have accounts.
-
Run — with the PHP built-in server for testing:
php -S 0.0.0.0:8080 -t /path/to/coursehubor point an Apache/Nginx + PHP-FPM vhost at the directory.
-
Index the library — click Rescan library in the top bar, or:
php scan.phpRescan whenever you add/remove files. Re-scans are idempotent: existing videos keep their IDs (and everyone's progress); files that disappeared are hidden, not deleted, so progress survives if they come back.
-
Open the site, create an account, and watch.
How progress works
- The player POSTs
{video_id, position, duration}toapi/progress.phpevery 5 s while playing, plus on pause/seek, vianavigator.sendBeaconwhen the tab is hidden or closed, and when navigating to another view in the SPA. - A video is marked completed at ≥ 95 % watched (
COMPLETE_THRESHOLD). Completion is sticky — rewatching doesn't un-complete it. - Folder progress is computed over the folder's entire subtree using a path prefix match, with partial credit for videos in progress.
- Video duration is captured from the browser on first play (no ffprobe
dependency) and cached on the
videosrow.
Production notes
- PHP streams the video in 512 KB chunks with Range support, which is fine for
a handful of users. For heavier use, serve files with
X-Accel-Redirect(Nginx) orX-Sendfile(Apache) instead of streaming through PHP. - Put the app behind HTTPS and set
session.cookie_secure=1. - Deny direct web access to
includes/,config.php, andschema.sql(or place the videos and includes outside the docroot).