Files
coursehub/README.md
T
Harwinder Sekhon aba6ef4734 Fix HTTP 500 on PHP < 8.2: drop standalone true union type
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.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:11:30 -04:00

3.6 KiB

CourseHub

Self-hosted course video player: PHP 8.0+ + 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 .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:

    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:

    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:

    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).