# CourseHub Self-hosted course video player: PHP 8 + MySQL/MariaDB + Bootstrap 5 + [Plyr](https://github.com/sampotts/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 `.mp4` files 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 (`Video2` before `Video10`). - 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 1. **Database** — create the schema and an app user: ```bash sudo mysql < schema.sql sudo mysql -e "CREATE USER IF NOT EXISTS 'coursehub'@'localhost' IDENTIFIED BY 'coursehub'; GRANT ALL ON coursehub.* TO 'coursehub'@'localhost';" ``` 2. **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`: set `false` once your users have accounts. 3. **Run** — with the PHP built-in server for testing: ```bash php -S 0.0.0.0:8080 -t /path/to/coursehub ``` or point an Apache/Nginx + PHP-FPM vhost at the directory. 4. **Index the library** — click **Rescan library** in the top bar, or: ```bash php scan.php ``` Rescan 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. 5. Open the site, create an account, and watch. ## How progress works - The player POSTs `{video_id, position, duration}` to `api/progress.php` every 5 s while playing, plus on pause/seek, via `navigator.sendBeacon` when 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 `videos` row. ## 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) or `X-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`, and `schema.sql` (or place the videos and includes outside the docroot).