45e7465f7b
Parse the mp4 mvhd box in pure PHP (ffprobe fallback) for videos with no stored duration, so folder listings show durations before first play.
122 lines
4.6 KiB
Markdown
122 lines
4.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.
|
|
|
|
## Quizzes
|
|
|
|
Course folders may contain quiz text files next to the videos. Files named
|
|
`*_quiz_solved.txt` are indexed (a `*_quiz_notsolved.txt` twin is ignored —
|
|
it's the same content without answers). Format:
|
|
|
|
```
|
|
[&] Which two are supported CLI connectivity methods? (Select two)
|
|
- RDP
|
|
- rlogin
|
|
- GUI widget -- Correct
|
|
- FortiExplorer -- Correct
|
|
```
|
|
|
|
Questions start with `[&]`, options with `- `, and correct options end with
|
|
`-- Correct`. Questions with more than one correct option automatically render
|
|
as checkboxes instead of radios.
|
|
|
|
Quizzes are graded server-side (answers are never sent to the browser before
|
|
submission). A question counts only if the selected set exactly matches the
|
|
correct set. Best score, last score, and attempt count are stored per user;
|
|
a quiz is *completed* once every question is answered correctly in one
|
|
attempt, and quiz scores count toward folder progress alongside videos.
|
|
|
|
## 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 read at scan time by parsing the mp4's `mvhd` header in
|
|
pure PHP (ffprobe fallback if the parse fails), so listings show durations
|
|
before anyone plays the video. The browser still reports the duration on
|
|
first play as a last-resort fallback.
|
|
|
|
## 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).
|