# Universal M3U8 Player

HLS (.m3u8) player with admin toolbox, embed mode, upload, CORS proxy, and local file sharing.

## Ports

| Service | Port | Notes |
|---------|-------|-------|
| Frontend (Vite) | `7594` | Bind to all interfaces via dev script |
| Backend (Express) | `2738` | Express server |

## Quick Start

| Mode | Command | Notes |
|------|---------|-------|
| Full stack (dev) | `./dev.sh` | Auto-installs missing deps; boots both apps |
| Backend only | `cd backend && npm run dev` | Express with nodemon |
| Frontend only | `cd hls-player && npm run dev -- --host 0.0.0.0 --port 7594 --strictPort` | Vite dev server |
| Production | `./prod.sh` | Logs to `logs/prod.log` |

### Build / Test / Lint

```bash
cd hls-player && npm run build          # TypeScript + Vite optimization
cd hls-player && npm run preview       # Preview production build
cd hls-player && npm run lint          # ESLint
cd hls-player && npm test             # Vitest + Testing Library
cd backend && npm test                 # Node built-in test runner
```

## Reverse Proxy Configuration

Set in `hls-player/.env`:

```bash
VITE_BACKEND_URL=https://your-domain.example
FRONTEND_HOST=0.0.0.0
BACKEND_HOST=0.0.0.0
FRONTEND_PORT=7594
BACKEND_PORT=2738
BACKEND_PUBLIC_URL=https://your-domain.example
FRONTEND_ALLOWED_HOSTS=m3u8.thinhkhuat.com,localhost,127.0.0.1,::1
```

For production Docker, bind the backend container to all interfaces:

```yaml
ports:
  - "0.0.0.0:${BACKEND_PORT:-2738}:${BACKEND_PORT:-2738}"
```

## Features

| Feature | Route | Description |
|----------|--------|-------------|
| Admin Toolbox | `/` | Configure/test streams, browse local media, create shares, upload images, issue embed tokens |
| Clean Embed | `/embed?token=...` | Minimal player; token-only config fetched server-side |

### Admin File Browsing & Sharing

- **Requirements:** `ENABLE_LOCAL_MEDIA=1` + `LOCAL_MEDIA_ROOT=<path>`.
- **Login:** Set `ADMIN_USERNAME` and `ADMIN_PASSWORD` env vars.
- **Workflow:** Browse library → select file → create share token → get HLS URL.
- **Security:** UUID tokens, TTL-based expiry (default: 24h), no exposed paths.

### HLS Playback

| Browser | Engine | Notes |
|----------|---------|-------|
| Safari | Native | No polyfill needed |
| Chrome/Firefox/Edge | hls.js | Polyfill via `hls.js` library |
| All | Plyr | Player controls (play, pause, fullscreen, subtitles) |

- **Adaptive Bitrate:** Supports multi-variant playlists.
- **Retry Logic:** Auto-retry on network errors.
- **Config Encoding:** Query parameters encoded via base64 for clean URLs.

## Embed Tokens

Embed playback now requires a server-issued token. Admins generate tokens in the toolbox (after login), which are used like:

```
https://your-domain.com/embed?token=<tokenId>
```

Tokens store the full player config server-side, include TTL, and track usage.

## Query Parameters (Toolbox Preview)

Toolbox still uses config query parameters for previewing the player in `/`.

| Parameter | Type | Example | Notes |
|-----------|------|---------|-------|
| `src` | string | `https://example.com/stream.m3u8` | **Required** - HLS playlist URL |
| `auto` | 1\|0 | `auto=1` | Autoplay |
| `muted` | 1\|0 | `muted=1` | Mute audio |
| `controls` | 1\|0 | `controls=1` | Show player controls |
| `loop` | 1\|0 | `loop=1` | Loop playback |
| `poster` | string | `poster=https://...` | Poster image URL |
| `preload` | string | `preload=metadata` | Strategy: `auto`, `metadata`, `none` |

**Example:**
```
http://localhost:7594/?src=https%3A%2F%2Fexample.com%2Fstream.m3u8&auto=1&muted=1&controls=1
```

## CORS Proxy

For streams without CORS support:

```
GET /proxy?url=https%3A%2F%2Fexample.com%2Fstream.m3u8
```

**Features:**
- Rewrites sub-resource URLs to use proxy.
- Blocks non-http(s) URLs and private hosts.
- Exposes CORS headers for cross-origin playback.

## Responsive Embed (16:9)

```html
<div style="position:relative;width:100%;padding-top:56.25%;">
  <iframe
    src="https://your-domain.com/embed?token=<tokenId>"
    style="position:absolute;top:0;left:0;width:100%;height:100%;"
    allow="autoplay; fullscreen; picture-in-picture"
    loading="lazy"
    frameborder="0"
  ></iframe>
</div>
```

## Configuration

### Frontend (.env)

| Variable | Purpose |
|----------|---------|
| `VITE_BACKEND_URL` | Backend origin (baked into build) |

See `backend/README.md` for complete backend environment variables.

### Runtime Configuration

Frontend resolves backend URL via:
1. Build-time: `VITE_BACKEND_URL` (baked in).
2. Runtime fallback: `window.__HLS_PLAYER_ENV__.backendUrl` (injected by backend).

Reference: `hls-player/src/lib/backendUrl.ts:10-35` and `hls-player/public/runtime-config.js`.

## Architecture

| Component | Role | Reference |
|-----------|------|-----------|
| `src/main.tsx` | Mounts `<App />` under `BrowserRouter` | (hls-player/src/main.tsx:1-9) |
| `src/App.tsx` | Router: `/`, `/embed`, `/admin` | (hls-player/src/App.tsx:1-18) |
| `src/routes/ToolboxPage.tsx` | Admin UI container | (hls-player/src/routes/ToolboxPage.tsx) |
| `src/routes/EmbedPage.tsx` | Embed-mode player | (hls-player/src/routes/EmbedPage.tsx) |
| `src/components/HlsPlayer.tsx` | Player widget (Plyr + hls.js) | (hls-player/src/components/HlsPlayer.tsx:1-214) |
| `src/components/toolbox/FileBrowserDialog.tsx` | File browsing dialog | (hls-player/src/components/toolbox/FileBrowserDialog.tsx:1-200) |
| `src/lib/hlsController.ts` | HLS.js wrapper with callbacks | (hls-player/src/lib/hlsController.ts:1-104) |
| `src/lib/configCodec.ts` | Config encode/decode for URLs | (hls-player/src/lib/configCodec.ts:1-75) |
| `src/lib/backendUrl.ts` | Backend URL resolution | (hls-player/src/lib/backendUrl.ts:10-35) |
| `src/types/playerConfig.ts` | Player config types | (hls-player/src/types/playerConfig.ts:1-23) |

**Tech Stack:**
- React 19 + TypeScript + Vite
- React Router v7
- hls.js + Plyr
- Tailwind CSS
- Vitest + Testing Library

## Design Guidelines

- **Colors:** Vietnam flag palette only (red surfaces, yellow accents). No additional hues.
- **Typography:** System fonts.
- **Responsive:** Mobile-first, adapts to all screen sizes.
- **Contrast:** Adjust luminance within red/yellow pairing if needed; document any tweaks.

Reference: AGENTS.md:73-84

## Troubleshooting

| Symptom | Likely Cause | Fix |
|---------|--------------|-----|
| Playback fails | CORS issues | Use `/proxy?url=...` endpoint |
| Admin login unavailable | Credentials not set | Set `ADMIN_USERNAME` and `ADMIN_PASSWORD` |
| File browser empty | Local media disabled | Set `ENABLE_LOCAL_MEDIA=1` and `LOCAL_MEDIA_ROOT` |
| HLS packaging fails | ffmpeg missing | Install ffmpeg or set `FFMPEG_PATH` |
| Embed not working | URL encoding | URL-encode query params (e.g., `%3A` for `:`) |
