13856e35bc
registerUser() declared `: string|true`; the bare `true` type requires PHP 8.2, so including auth.php was a fatal error on PHP 8.0/8.1 — every login/API call returned 500 while install.php (which doesn't include auth.php) worked. Use string|bool instead. Also install a JSON exception handler so future server errors surface a message to the SPA instead of a blank 500.
96 lines
3.6 KiB
Markdown
96 lines
3.6 KiB
Markdown
# CourseHub
|
|
|
|
Self-hosted course video player: PHP 8.0+ + 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).
|