diff --git a/.gitignore b/.gitignore
index 2c1d47f..e625555 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,5 @@ coverage/
.DS_Store
dist/
release/
+
transcode-cache/
diff --git a/Dockerfile b/Dockerfile
index cae87a8..27997b0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,41 @@
-FROM node:20-alpine
+# NodeCast TV Docker Image
+#
+# Uses system FFmpeg with hardware acceleration drivers installed.
+#
+# Hardware acceleration:
+# - VAAPI (Intel/AMD): Mount /dev/dri and add video/render groups
+# - NVIDIA NVENC: Requires nvidia-container-toolkit on host + --gpus flag
+# - Intel QSV: Mount /dev/dri
+#
+# Build: docker compose build
+# Run with VAAPI: docker run --device /dev/dri:/dev/dri --group-add video ...
+# Run with NVENC: docker run --gpus all ...
-# Install build dependencies for better-sqlite3 and FFmpeg for streaming
-# Using system FFmpeg instead of ffmpeg-static for better Docker DNS compatibility
-RUN apk add --no-cache python3 make g++ ffmpeg
+FROM node:20-slim
+
+# Install FFmpeg, build dependencies, and hardware acceleration drivers
+ENV DEBIAN_FRONTEND=noninteractive
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ # FFmpeg with full codec support
+ ffmpeg \
+ # Build dependencies for better-sqlite3
+ python3 \
+ make \
+ g++ \
+ # VAAPI runtime libraries (AMD/Intel)
+ libva2 \
+ libva-drm2 \
+ libvdpau1 \
+ mesa-va-drivers \
+ # Intel VAAPI/QSV drivers
+ intel-media-va-driver \
+ i965-va-driver \
+ # VA-API utilities for debugging
+ vainfo \
+ && rm -rf /var/lib/apt/lists/*
+
+# Verify FFmpeg installed and show version
+RUN ffmpeg -version && ffmpeg -encoders 2>/dev/null | grep -E "vaapi|nvenc|qsv|libx264" | head -10
WORKDIR /app
@@ -15,8 +48,8 @@ RUN npm ci --only=production
# Copy application files
COPY . .
-# Create data directory
-RUN mkdir -p /app/data
+# Create data and cache directories
+RUN mkdir -p /app/data /app/transcode-cache && chmod 777 /app/transcode-cache
# Expose port
EXPOSE 3000
diff --git a/README.md b/README.md
index d13548b..b6010c1 100644
--- a/README.md
+++ b/README.md
@@ -13,22 +13,26 @@ nodecast-tv is a modern, web-based IPTV player featuring Live TV, EPG, Movies (V
- **π¬ VOD Support**: Dedicated sections for Movies and TV Series with rich metadata, posters, and seasonal episode lists.
- **β€οΈ Favorites System**: Unified favorites for channels, movies, and series with instant synchronization.
- **π Authentication**: User login system with admin and viewer roles ([details](https://github.com/technomancer702/nodecast-tv/pull/23)).
+- **π OIDC SSO**: Support for Single Sign-On via OIDC providers (Authentik, Keycloak, etc.).
- **β‘ High Performance**: Optimized for large playlists (7000+ channels) using virtual scrolling and batch rendering.
- **βοΈ Management**:
- Support for Xtream Codes and M3U playlists.
- Manage hidden content categories.
- Playback preferences (volume memory, auto-play).
-- **π Audio Transcoding**: Optional FFmpeg-based audio transcoding for Dolby/AC3/EAC3 compatibility.
-- **π¦ Stream Remux**: Lightweight FFmpeg remux for raw MPEG-TS streams from IPTV middleware.
+- **ποΈ Hardware Transcoding**: GPU-accelerated transcoding with NVIDIA NVENC, AMD AMF, Intel QuickSync, and VAAPI support.
+- **π Smart Audio**: Configurable 5.1βStereo downmix presets (ITU, Night Mode, Cinematic) with automatic passthrough for compatible sources.
+- **π¦ Stream Processing**: Auto-detection of stream codecs with smart remux/transcode decisions.
- **π³ Docker Ready**: Easy deployment containerization.
## Screenshots
## Getting Started
@@ -86,6 +90,49 @@ You can run nodecast-tv easily using Docker.
The application will be available at `http://localhost:3000`.
+
+### Hardware Acceleration Setup
+
+To enable hardware transcoding (NVENC, QSV, VAAPI), you must expose your host's GPU to the container.
+
+**1. Intel (QSV) & AMD (VAAPI)**
+Update your `docker-compose.yml` to map the DRI devices and add necessary groups (often required for permission):
+```yaml
+ devices:
+ - /dev/dri:/dev/dri # Required for VAAPI/QuickSync/AMF (Linux)
+ # group_add: # Optional: Needed mainly if you run as non-root
+ # - "video" # Run on host: getent group video
+ # - "render" # Run on host: getent group render
+```
+
+**2. NVIDIA (NVENC)**
+Ensure you have the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) installed on your host, then update your `docker-compose.yml`:
+```yaml
+ deploy:
+ resources:
+ reservations:
+ devices:
+ - driver: nvidia
+ count: 1
+ capabilities: [gpu]
+```
+
+**Verify:**
+After restarting the container, go to **Settings -> Transcoding**. The **Hardware Detection** status should list your GPU (e.g., "NVIDIA GPU Detected" or "VAAPI Available").
+
+### SSO / OIDC Setup
+
+Enable Single Sign-On (SSO) with your preferred OIDC provider (Authentik, Keycloak, etc.) by configuring these variables in your `.env` file or Docker environment:
+
+```env
+OIDC_ISSUER_URL=https://your-idp.com/application/o/nodecast/
+OIDC_CLIENT_ID=your_client_id
+OIDC_CLIENT_SECRET=your_client_secret
+OIDC_CALLBACK_URL=http://localhost:3000/api/auth/oidc/callback # Adjust for your domain
+```
+
+**Note:** New users signing in via SSO are automatically assigned the **Viewer** role. You must manually promote them to Admin if desired.
+
### Usage
1. Go to **Settings** -> **Content Sources**.
@@ -94,42 +141,24 @@ The application will be available at `http://localhost:3000`.
4. Navigate to **Live TV**, **Movies**, or **Series** to browse your content.
-## Browser Codec Support
+## Browser Codec Support & Transcoding
-nodecast-tv is a web-based application, which means **video decoding is handled by your browser**, not by the server. The server simply proxies the stream data - it does not transcode or re-encode video.
+nodecast-tv is a web-based application. By default, **video decoding is handled by your browser**. However, the built-in **smart transcoding system** automatically converts incompatible media (e.g., HEVC video, Dolby audio) into browser-friendly formats using FFmpeg.
-This means codec support depends entirely on what your browser can decode natively:
+**Codec Compatibility Table:**
| Codec | Chrome | Firefox | Safari | Edge |
|-------|--------|---------|--------|------|
| **H.264 (AVC)** | β
| β
| β
| β
|
-| **H.265 (HEVC)** | βΒΉ | β | β
| β οΈΒ² |
-| **VP9** | β
| β
| β οΈΒ³ | β
|
-| **AV1** | β
| β
| β | β
|
+| **H.265 (HEVC)** | Auto-Transcode | Auto-Transcode | β
| β οΈ |
+| **AV1** | β
| β
| Auto-Transcode | β
|
| **AAC Audio** | β
| β
| β
| β
|
-| **AC3/EAC3 (Dolby)** | β | β | β
| β |
-| **MP3 Audio** | β
| β
| β
| β
|
+| **AC3/EAC3 (Dolby)** | Auto-Transcode | Auto-Transcode | β
| Auto-Transcode |
-**Notes:**
-1. Chrome may support HEVC on macOS with hardware decoder
-2. Edge requires the paid "HEVC Video Extensions" from Microsoft Store ($0.99)
-3. Safari VP9 support varies by device/version
+> **β οΈ Note:** Edge requires the [HEVC Video Extensions](https://apps.microsoft.com/store/detail/hevc-video-extensions/9NMZLZ57R3T7) from the Microsoft Store to play H.265 (HEVC) natively.
+> **βΉοΈ Note:** Safari plays AV1 natively on supported hardware (iPhone 15 Pro, M3 Macs). On older devices, Auto-Transcode handles it.
-**If a stream doesn't play:**
-- The stream codec may not be supported by your browser
-- Try a different browser (Safari for HEVC/Dolby, Chrome/Edge for VP9/AV1)
-- Check if your IPTV provider offers alternative stream formats
-- For Dolby audio issues, enable **"Force Audio Transcode"** in Settings β Streaming
-### Audio Transcoding
-
-For streams with Dolby Digital (AC3/EAC3) audio that browsers can't decode natively:
-
-1. Install FFmpeg support: `npm install ffmpeg-static` (included as optional dependency)
-2. Enable **"Force Audio Transcode"** in Settings β Streaming
-3. Audio will be transcoded to AAC while video passes through unchanged
-
-**Note:** For ad-stitched live streams (like Pluto TV), transcoding may struggle to keep up. These streams typically use AAC audio already, so the built-in HLS discontinuity handling manages audio transitions without transcoding.
## Supported Stream Types
@@ -140,18 +169,45 @@ nodecast-tv is optimized for **HLS (HTTP Live Streaming)**.
- **β οΈ High Latency/P2P**: For sources like Acestream, prefer HLS output (`.m3u8`) over raw TS streams to avoid timeouts during buffering.
- **β RTMP/RTSP**: Not supported natively by browsers.
+## Transcoding Settings
+All transcoding and stream processing settings are found in **Settings β Transcoding**.
-## Streaming Settings
+### Hardware Encoder
-All streaming settings are found in **Settings β Player β Streaming**.
+| Setting | Options | Description |
+|---------|---------|-------------|
+| **Hardware Encoder** | Auto, NVENC, AMF, QSV, VAAPI, Software | GPU-accelerated encoding. Auto detects best available. |
+| **Max Resolution** | 4K, 1080p, 720p, 480p | Limit output resolution (lower = faster). |
+| **Quality Preset** | High, Medium, Low | Encoding quality/speed tradeoff. |
+| **Audio Mix Preset** | Auto, ITU, Night Mode, Cinematic, Passthrough | 5.1βStereo downmix mode (see below). |
+
+### Audio Mix Presets
+
+| Preset | Description |
+|--------|-------------|
+| **Auto (Smart)** | Copies stereo AAC as-is, uses ITU downmix for 5.1+ |
+| **ITU-R BS.775** | Industry-standard balanced downmix |
+| **Night Mode** | Boosted dialogue, reduced bass for quiet viewing |
+| **Cinematic** | Wide soundstage, immersive surround feel |
+| **Passthrough** | No processing (may cause errors on 5.1/Dolby sources) |
+
+### Stream Processing
| Setting | What It Does | When to Enable |
|---------|--------------|----------------|
-| **Force Backend Proxy** | Routes streams through the nodecast-tv server, adding proper CORS headers | When streams fail with "Access-Control-Allow-Origin" errors, or when using IPTV middleware |
-| **Force Audio Transcode** | Transcodes audio to AAC using FFmpeg (video passes through unchanged) | When you have video but no audio (Dolby/AC3/EAC3 streams) |
-| **Force Remux** | Remuxes MPEG-TS to MP4 container using FFmpeg (no re-encoding, very lightweight) | When using raw `.ts` streams from m3u-editor, dispatcharr, or similar middleware |
-| **Stream Output Format** | Controls whether Xtream API requests use HLS (.m3u8) or TS format | Try TS if you experience buffering issues with HLS |
+| **Auto Transcode (Smart)** | Probes streams and only transcodes/remuxes when needed | Recommended for most users (default ON) |
+| **Force Audio Transcode** | Transcodes audio to AAC (video passes through) | When you have video but no audio (Dolby/AC3/EAC3) |
+| **Force Video Transcode** | Full transcode of both audio and video | For HEVC/VP9 sources on unsupported browsers |
+| **Force Remux** | Remuxes MPEG-TS to MP4 (no re-encoding) | For raw `.ts` streams from middleware |
+| **Stream Output Format** | HLS or TS for Xtream API requests | Try TS if HLS causes buffering |
+
+### Network
+
+| Setting | What It Does | When to Enable |
+|---------|--------------|----------------|
+| **Force Backend Proxy** | Routes streams through the server for CORS headers | When streams fail with CORS errors, or using middleware |
+
## Troubleshooting
@@ -159,23 +215,22 @@ All streaming settings are found in **Settings β Player β Streaming**.
| Symptom | Likely Cause | Solution |
|---------|--------------|----------|
-| Black screen, `Access-Control-Allow-Origin` error | CORS blocked | Enable **"Force Backend Proxy"** in Settings β Streaming |
-| Black screen with `MEDIA_ERR_DECODE` or `fragParsingError` | Unsupported codec (likely HEVC) | Try a different browser (see Codec Support table) |
-| Loading forever (no error) | Decoder hung on unsupported codec | Try Safari or Edge; stream likely uses HEVC |
+| Black screen, `Access-Control-Allow-Origin` error | CORS blocked | Enable **"Force Backend Proxy"** in Settings β Transcoding |
+| Black screen with `MEDIA_ERR_DECODE` | Unsupported codec (HEVC/VP9) | Ensure **"Auto Transcode"** is enabled |
+| Loading forever (no error) | Browser decoder stuck | Enable **"Force Video Transcode"** (overrides Auto detection) |
### No Audio (Video Plays Fine)
| Symptom | Likely Cause | Solution |
|---------|--------------|----------|
-| No audio at all | Dolby/AC3/EAC3 audio | Enable **"Force Audio Transcode"** in Settings β Streaming |
-| No audio on some channels | Codec mismatch | Try Safari (best Dolby support) or enable transcoding |
+| No audio at all | Dolby/AC3/EAC3 audio | Enable **"Force Audio Transcode"** (overrides Auto detection) |
| Audio out of sync | Stream encoding issue | Try changing stream format to TS in Settings |
### Buffering Issues
| Symptom | Likely Cause | Solution |
|---------|--------------|----------|
-| Constant buffering | Slow network or provider | Try TS format instead of HLS in Settings |
+| Constant buffering | Slow network or weak GPU | 1. Lower **Max Resolution** (e.g. to 720p)
2. Try **TS** format instead of HLS |
### HTTPS / Reverse Proxy Issues
@@ -183,7 +238,7 @@ If you're running nodecast-tv behind a reverse proxy (Nginx, Caddy, Traefik) wit
| Symptom | Likely Cause | Solution |
|---------|--------------|----------|
-| Streams fail with `fragLoadError` | Mixed content (HTTPS page loading HTTP streams) | Enable **"Force Backend Proxy"** in Settings β Streaming |
+| Streams fail with `fragLoadError` | Mixed content (HTTPS page loading HTTP streams) | Enable **"Force Backend Proxy"** in Settings β Transcoding |
| Streams work on HTTP but not HTTPS | Reverse proxy not passing headers correctly | Ensure `X-Forwarded-Proto` header is set (see examples below) |
**Caddy example:**
@@ -214,77 +269,21 @@ location / {
}
```
-### IPTV Middleware (m3u-editor, dispatcharr, Threadfin, etc.)
+### IPTV Middleware (m3u-editor, dispatcharr, Threadfin, xTeVe)
+If you manage your streams with middleware tools, you may encounter CORS issues or raw MPEG-TS streams that browsers can't play directly.
-If you're using IPTV middleware like **m3u-editor**, **dispatcharr**, **Threadfin**, or **xTeVe** to manage your streams, you may need to adjust nodecast-tv settings for optimal playback. These tools typically use passthrough mode, which preserves original codecs (like HEVC/Dolby) that most browsers cannot decode natively, and may also trigger CORS restrictions.
+**Recommended Setup:**
+1. **Force Backend Proxy:** Enable this in **Settings β Transcoding β Network**. This routes middleware streams through NodeCast TV, bypassing CORS restrictions.
+2. **Auto Transcode:** Keep this enabled (default). It will automatically detect if the middleware stream (e.g., MPEG-TS) needs to be remuxed or transcoded for the browser.
-**Output Format Compatibility:**
-
-| Middleware Output | Works? | Notes |
-|-------------------|--------|-------|
-| **HLS (`.m3u8`)** | β
Yes | Best option. NodeCast plays HLS natively. |
-| **Raw TS (`.ts`)** | β οΈ Maybe | Enable **"Force Remux"**. May fail if middleware proxies the stream. |
-| **Proxied TS** | β No | If middleware proxies/rewrites the stream, NodeCast's FFmpeg can't read it. Use HLS output instead. |
-
-> **Why Proxied TS Fails:** When middleware is set to "Proxy" mode, it wraps the stream in its own layer. NodeCast's Remux/FFmpeg can't parse this format (error: `Invalid data found when processing input`). Configure your middleware to output **HLS (m3u8)** instead.
-
-**Recommended Settings in nodecast-tv:**
-
-| Setting | Location | When to Enable |
-|---------|----------|----------------|
-| **Force Backend Proxy** | Settings β Player β Streaming | Always recommended when using middleware |
-| **Force Remux** | Settings β Player β Streaming | For raw `.ts` streams (lightweight, no re-encoding) |
-| **Force Audio Transcode** | Settings β Player β Streaming | If you have no audio (Dolby/AC3/EAC3 streams) |
-
-
----
-
-#### m3u-editor (sparkison/m3u-editor)
-
-m3u-editor includes an internal proxy that remuxes streams to MPEG-TS.
-
-**Setup:**
-1. In m3u-editor, configure your playlist and enable the proxy if needed
-2. In nodecast-tv, enable **"Force Remux"** in Settings β Streaming (for raw .ts streams)
-3. If audio doesn't play, enable **"Force Audio Transcode"** instead
-
-**Note:** m3u-editor's proxy preserves original codecs. If your source has HEVC or Dolby, you'll need transcoding or a compatible browser (Safari).
-
----
-
-#### dispatcharr
-
-dispatcharr uses FFmpeg stream profiles to process streams. By default it outputs MPEG-TS with passthrough codecs.
-
-**Setup:**
-1. In dispatcharr, streams are proxied by default via stream profiles
-2. In nodecast-tv, enable **"Force Remux"** in Settings β Streaming (for raw .ts streams)
-3. If audio doesn't play, enable **"Force Audio Transcode"** instead
-
-**Custom dispatcharr profile for browser compatibility:**
-If you want dispatcharr to transcode audio for you instead of nodecast-tv:
-```
--user_agent {userAgent} -i {streamUrl} -c:v copy -c:a aac -f mpegts pipe:1
-```
-This keeps video passthrough but converts audio to AAC.
-
----
-
-#### Threadfin / xTeVe
-
-These HDHomeRun emulators work similarly to other middleware.
-
-**Setup:**
-1. Add your Threadfin/xTeVe M3U URL as an M3U source in nodecast-tv
-2. Enable **"Force Remux"** in Settings β Streaming (for raw .ts streams)
-3. If needed, enable **"Force Audio Transcode"** instead for Dolby audio
+There is rarely a need to configure specific "Force Remux" settings manually anymore; the system detects stream types automatically.
### TVHeadend
If you're using TVHeadend as your source, you may need to configure a few settings for streams to play correctly in nodecast-tv:
**Option 1: Enable Force Backend Proxy (Easiest)**
-- In nodecast-tv, go to **Settings β Player β Streaming**
+- In nodecast-tv, go to **Settings β Transcoding β Network**
- Enable **"Force Backend Proxy"**
- This routes streams through the server, bypassing browser CORS restrictions
@@ -299,10 +298,10 @@ If you're using TVHeadend as your source, you may need to configure a few settin
### Acestream / P2P Streaming
-If you are using `acestream-docker-home` or similar tools, you **MUST** use the HLS output format.
+If you are using `acestream-docker-home` or similar tools, it is **recommended** to use the HLS output format to reduce server load, though NodeCast TV can remux raw streams if needed.
-- **Don't use**: `http://proxy:6878/ace/getstream?id=...` (This is a raw MPEG-TS stream)
-- **Do use**: `http://proxy:6878/ace/manifest.m3u8?id=...` (This wraps the stream in a browser-friendly HLS playlist)
+- **Recommended:** `http://proxy:6878/ace/manifest.m3u8?id=...` (HLS Playlist - Direct Play)
+- **Supported:** `http://proxy:6878/ace/getstream?id=...` (MPEG-TS - Requires Server Remuxing)
## Technology Stack
@@ -310,7 +309,7 @@ If you are using `acestream-docker-home` or similar tools, you **MUST** use the
- **Frontend**: Vanilla JavaScript (ES6+), CSS3
- **Database**: SQLite (via better-sqlite3) for high-performance data storage
- **Streaming**: HLS.js for stream playback
-- **Audio Transcoding**: FFmpeg (optional, via ffmpeg-static)
+- **Transcoding**: FFmpeg (integrated for hardware/software transcoding)
## Project Structure
diff --git a/package-lock.json b/package-lock.json
index 015d8b2..1839780 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,23 +1,25 @@
{
"name": "nodecast-tv",
- "version": "1.5.0",
+ "version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nodecast-tv",
- "version": "1.0.0",
+ "version": "2.0.0",
"license": "GPL-3.0-only",
"dependencies": {
"@ffprobe-installer/ffprobe": "^2.1.2",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.5.0",
+ "dotenv": "^17.2.3",
"express": "^4.18.2",
"express-session": "^1.18.2",
"jsonwebtoken": "^9.0.3",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
+ "passport-openidconnect": "^0.1.2",
"sax": "^1.4.3",
"xml2js": "^0.6.2"
},
@@ -525,6 +527,18 @@
"node": ">=8"
}
},
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1196,6 +1210,12 @@
"node": ">=10"
}
},
+ "node_modules/oauth": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
+ "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
+ "license": "MIT"
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -1292,6 +1312,23 @@
"node": ">= 0.4.0"
}
},
+ "node_modules/passport-openidconnect": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.2.tgz",
+ "integrity": "sha512-JX3rTyW+KFZ/E9OF/IpXJPbyLO9vGzcmXB5FgSP2jfL3LGKJPdV7zUE8rWeKeeI/iueQggOeFa3onrCmhxXZTg==",
+ "license": "MIT",
+ "dependencies": {
+ "oauth": "0.10.x",
+ "passport-strategy": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
@@ -1829,4 +1866,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 829b267..22359fd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nodecast-tv",
- "version": "1.5.1",
+ "version": "2.0.0",
"description": "Modern IPTV Player",
"license": "GPL-3.0-only",
"main": "server/index.js",
@@ -12,12 +12,14 @@
"@ffprobe-installer/ffprobe": "^2.1.2",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^12.5.0",
+ "dotenv": "^17.2.3",
"express": "^4.18.2",
"express-session": "^1.18.2",
"jsonwebtoken": "^9.0.3",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
+ "passport-openidconnect": "^0.1.2",
"sax": "^1.4.3",
"xml2js": "^0.6.2"
},
@@ -27,4 +29,4 @@
"engines": {
"node": ">=18.0.0"
}
-}
\ No newline at end of file
+}
diff --git a/public/css/main.css b/public/css/main.css
index cef9814..5af3e93 100644
--- a/public/css/main.css
+++ b/public/css/main.css
@@ -980,7 +980,8 @@ video::cue {
.epg-grid {
height: calc(100vh - var(--navbar-height) - 60px);
- overflow: auto;
+ overflow: hidden;
+ overflow-y: auto;
padding: var(--space-md);
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
@@ -988,10 +989,22 @@ video::cue {
.epg-time-header {
display: flex;
+ position: sticky;
top: 0;
background: var(--color-bg-secondary);
z-index: 10;
- margin-left: var(--epg-sidebar-width);
+}
+
+.epg-header-corner {
+ width: var(--epg-sidebar-width);
+ flex-shrink: 0;
+ background: var(--color-bg-secondary);
+ border-right: 1px solid var(--color-border);
+}
+
+.epg-time-slots {
+ display: flex;
+ flex: 1;
}
.epg-time-slot {
@@ -1103,7 +1116,7 @@ video::cue {
bottom: 0;
width: 2px;
background: var(--color-error);
- z-index: 15;
+ z-index: 2;
}
/* =====================================================
@@ -1853,6 +1866,21 @@ kbd {
animation: spin 0.8s linear infinite;
}
+.loading-state {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-sm);
+ padding: var(--space-xl);
+ color: var(--color-text-muted);
+ width: 100%;
+}
+
+.horizontal-scroll .empty-state {
+ padding: var(--space-xl);
+ min-height: 150px;
+}
+
@keyframes spin {
to {
transform: rotate(360deg);
@@ -1887,6 +1915,7 @@ kbd {
--sidebar-width: 320px;
--space-lg: 16px;
--space-xl: 24px;
+ --epg-sidebar-width: 180px;
}
/* Navigation - Hide text labels, show only icons */
@@ -1900,7 +1929,8 @@ kbd {
gap: 0;
}
- .navbar-menu span:not(.nav-icon) {
+ /* Hide nav link text on mobile, but keep Now Playing visible */
+ .navbar-menu .nav-link span:not(.nav-icon) {
display: none;
}
@@ -2039,11 +2069,7 @@ kbd {
}
.epg-channel-info {
- width: 180px;
- }
-
- .epg-time-slot {
- min-width: 160px;
+ width: var(--epg-sidebar-width);
}
/* Movies & Series */
@@ -3314,6 +3340,24 @@ kbd {
gap: var(--space-md);
padding: var(--space-lg);
padding-top: calc(var(--space-lg) + var(--safe-area-inset-top));
+ pointer-events: none;
+ z-index: 20;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.watch-top-bar>* {
+ pointer-events: auto;
+}
+
+/* Live TV Header: Left Title & Right Status */
+.watch-top-bar.live-header {
+ justify-content: space-between;
+}
+
+/* VOD Header: Spaced (Back Btn - Title - Status) */
+.watch-top-bar.vod-header {
+ justify-content: space-between;
}
.watch-back-btn {
@@ -4025,4 +4069,670 @@ kbd {
width: 18px;
height: 18px;
flex-shrink: 0;
+}
+
+/* Hardware Info Grid (Transcoding Tab) */
+.hw-info-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: var(--space-sm);
+}
+
+.hw-info-item {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xs);
+ padding: var(--space-sm) var(--space-md);
+ border-radius: var(--radius-sm);
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+}
+
+.hw-info-item.hw-available {
+ border-color: var(--color-success);
+ background: rgba(16, 185, 129, 0.1);
+}
+
+.hw-info-item.hw-unavailable {
+ opacity: 0.6;
+}
+
+.hw-badge {
+ font-weight: 600;
+ font-size: 0.875rem;
+}
+
+.hw-info-item.hw-available .hw-badge {
+ color: var(--color-success);
+}
+
+.hw-info-item.hw-unavailable .hw-badge {
+ color: var(--color-text-muted);
+}
+
+.hw-name {
+ font-size: 0.75rem;
+ color: var(--color-text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Transcode Status Indicator */
+.transcode-status {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ background: rgba(0, 0, 0, 0.5);
+ color: white;
+ display: inline-flex;
+ align-items: center;
+ backdrop-filter: blur(4px);
+ transition: opacity 0.3s ease;
+}
+
+.transcode-status.transcoding {
+ background: rgba(255, 68, 68, 0.8);
+ /* Red for Transcoding */
+ box-shadow: 0 0 10px rgba(255, 68, 68, 0.3);
+}
+
+.transcode-status.remuxing {
+ background: rgba(255, 153, 0, 0.8);
+ /* Orange for Remux */
+}
+
+.transcode-status.direct {
+ background: rgba(76, 175, 80, 0.8);
+ /* Green for Direct */
+}
+
+.transcode-status.hidden {
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* Badge Container for right alignment */
+.player-badges {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-left: auto;
+ /* Fallback for cases without flex-grow middle element */
+}
+
+/* Quality Badge */
+.quality-badge {
+ display: inline-block;
+ background: rgba(158, 158, 158, 0.8);
+ color: white;
+ padding: 4px 10px;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 600;
+}
+
+.quality-badge.hidden {
+ display: none;
+}
+
+/* Watch Header Bar (Title + Transcode Status) */
+/* Moved to original definition around line 3311 */
+
+.watch-title-overlay {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: white;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 60%;
+}
+
+/* =====================================================
+ Edit User Modal
+ ===================================================== */
+.modal-overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(4px);
+ z-index: 10000;
+ align-items: center;
+ justify-content: center;
+}
+
+.modal-overlay.active {
+ display: flex;
+}
+
+.modal-overlay .modal {
+ display: block;
+ position: relative;
+ inset: auto;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ width: 90%;
+ max-width: 500px;
+ max-height: 90vh;
+ overflow: hidden;
+ box-shadow: var(--shadow-lg);
+ animation: modalFadeIn 0.2s ease;
+}
+
+@keyframes modalFadeIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.modal-overlay .modal-header {
+ padding: var(--space-md);
+ border-bottom: 1px solid var(--color-border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.modal-overlay .modal-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--color-text-primary);
+ margin: 0;
+}
+
+.modal-overlay .modal-close {
+ background: none;
+ border: none;
+ color: var(--color-text-muted);
+ font-size: 1.5rem;
+ line-height: 1;
+ cursor: pointer;
+ padding: 0;
+ transition: color 0.2s;
+}
+
+.modal-overlay .modal-close:hover {
+ color: var(--color-text-primary);
+}
+
+.modal-overlay .modal-body {
+ padding: var(--space-lg);
+}
+
+.modal-overlay .modal-footer {
+ padding: var(--space-md);
+ border-top: 1px solid var(--color-border);
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--space-sm);
+ background: var(--color-bg-tertiary);
+}
+
+/* Modal Form Styles */
+.modal-overlay .modal-form-group {
+ margin-bottom: var(--space-md);
+}
+
+.modal-overlay .modal-form-group label {
+ display: block;
+ margin-bottom: var(--space-xs);
+ color: var(--color-text-secondary);
+ font-size: 0.875rem;
+}
+
+.modal-overlay .modal-form-group .form-input {
+ width: 100%;
+ padding: var(--space-sm);
+ background: var(--color-bg-primary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-sm);
+ color: var(--color-text-primary);
+}
+
+.modal-overlay .modal-form-group .form-input:focus {
+ outline: none;
+ border-color: var(--color-accent);
+}
+
+.modal-overlay .modal-form-group .form-input:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background: var(--color-bg-tertiary);
+}
+
+/* =====================================================
+ User Management Badges
+ ===================================================== */
+.user-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 2px 10px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border-radius: 12px;
+ white-space: nowrap;
+}
+
+.user-badge-sso {
+ background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ color: #fff;
+ box-shadow: 0 2px 4px rgba(99, 102, 241, 0.3);
+}
+
+.user-badge-local {
+ background: rgba(255, 255, 255, 0.08);
+ color: var(--color-text-secondary);
+ border: 1px solid var(--color-border);
+}
+
+.user-badge-admin {
+ background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);
+ color: #000;
+ box-shadow: 0 2px 4px rgba(245, 158, 11, 0.3);
+}
+
+.user-badge-viewer {
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--color-text-muted);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+/* =====================================================
+ Now Playing Indicator (Header)
+ ===================================================== */
+
+.now-playing-indicator {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 14px;
+ margin-right: var(--space-md);
+ background: linear-gradient(135deg, var(--color-accent) 0%, #8b5cf6 100%);
+ border-radius: var(--radius-full);
+ color: white;
+ text-decoration: none;
+ font-size: 13px;
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 200px;
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
+ transition: transform var(--transition-fast), box-shadow var(--transition-fast);
+ animation: now-playing-pulse 2s ease-in-out infinite;
+}
+
+.now-playing-indicator:hover {
+ transform: scale(1.05);
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.5);
+}
+
+.now-playing-indicator.hidden {
+ display: none;
+}
+
+.now-playing-icon {
+ font-size: 10px;
+}
+
+.now-playing-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+@keyframes now-playing-pulse {
+
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.85;
+ }
+}
+
+/* Mobile responsive - show in dropdown menu */
+@media (max-width: 768px) {
+ .now-playing-indicator {
+ width: calc(100% - 32px);
+ max-width: none;
+ margin: 0 16px var(--space-sm) 16px;
+ padding: 10px 16px;
+ font-size: 13px;
+ border-radius: var(--radius-md);
+ justify-content: center;
+ }
+}
+
+/* =====================================================
+ Home Dashboard Styles
+ ===================================================== */
+
+#page-home {
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+.dashboard-content {
+ padding: 24px;
+ max-width: 1400px;
+ margin: 0 auto;
+}
+
+.dashboard-section {
+ margin-bottom: 40px;
+}
+
+.dashboard-section.hidden {
+ display: none;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 20px;
+}
+
+.section-header h2 {
+ font-family: 'Outfit', sans-serif;
+ font-size: 1.5rem;
+ color: var(--color-text-bright);
+}
+
+/* Scroll Wrapper with Navigation Arrows */
+.scroll-wrapper {
+ position: relative;
+}
+
+.scroll-arrow {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 10;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.7);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ color: white;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all var(--transition-fast);
+ backdrop-filter: blur(8px);
+}
+
+.scroll-arrow:hover {
+ background: rgba(0, 0, 0, 0.9);
+ border-color: var(--color-accent);
+ transform: translateY(-50%) scale(1.1);
+}
+
+.scroll-arrow svg {
+ width: 24px;
+ height: 24px;
+}
+
+.scroll-arrow.scroll-left {
+ left: -8px;
+}
+
+.scroll-arrow.scroll-right {
+ right: -8px;
+}
+
+.scroll-arrow.hidden {
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* Hide arrows on touch devices (they can swipe) */
+@media (hover: none) {
+ .scroll-arrow {
+ display: none;
+ }
+}
+
+.horizontal-scroll {
+
+ display: flex;
+ gap: 20px;
+ overflow-x: auto;
+ padding-bottom: 12px;
+ scroll-snap-type: x mandatory;
+ -webkit-overflow-scrolling: touch;
+}
+
+/* Hide scrollbar by default, show on hover */
+.horizontal-scroll::-webkit-scrollbar {
+ height: 6px;
+}
+
+.horizontal-scroll::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 3px;
+}
+
+.horizontal-scroll:hover::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.dashboard-card {
+ flex: 0 0 calc(50% - 10px);
+ /* 2 per view on small screens */
+ max-width: 240px;
+ background: var(--color-surface);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ cursor: pointer;
+ transition: transform var(--transition-fast), box-shadow var(--transition-fast);
+ scroll-snap-align: start;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+/* 4 per view on medium screens */
+@media (min-width: 768px) {
+ .dashboard-card {
+ flex: 0 0 calc(25% - 15px);
+ }
+}
+
+/* 6 per view on large screens */
+@media (min-width: 1200px) {
+ .dashboard-card {
+ flex: 0 0 calc(16.66% - 17px);
+ }
+}
+
+.dashboard-card:hover {
+ transform: translateY(-5px);
+ box-shadow: var(--shadow-lg);
+ border-color: var(--color-accent-dim);
+}
+
+.card-image {
+ position: relative;
+ aspect-ratio: 2/3;
+ overflow: hidden;
+}
+
+.card-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.5s ease;
+}
+
+.dashboard-card:hover .card-image img {
+ transform: scale(1.05);
+}
+
+.progress-bar-container {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 4px;
+ background: rgba(0, 0, 0, 0.5);
+}
+
+.progress-bar {
+ height: 100%;
+ background: var(--color-accent);
+ box-shadow: 0 0 10px var(--color-accent);
+}
+
+.play-icon-overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, 0.4);
+ opacity: 0;
+ transition: opacity var(--transition-fast);
+}
+
+.play-icon-overlay svg {
+ width: 48px;
+ height: 48px;
+ color: white;
+ filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5));
+}
+
+.dashboard-card:hover .play-icon-overlay {
+ opacity: 1;
+}
+
+.card-info {
+ padding: 12px;
+}
+
+.card-title {
+ font-weight: 600;
+ font-size: 0.95rem;
+ color: var(--color-text-bright);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 4px;
+}
+
+.card-subtitle {
+ font-size: 0.8rem;
+ color: var(--color-text-muted);
+}
+
+.loading-state,
+.empty-state {
+ padding: 40px;
+ text-align: center;
+ color: var(--color-text-muted);
+ width: 100%;
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: var(--radius-lg);
+ border: 1px dashed rgba(255, 255, 255, 0.1);
+}
+
+/* =====================================================
+ Channel Tile Styles (Favorite Channels)
+ ===================================================== */
+
+.channel-tiles {
+ gap: 16px;
+}
+
+.channel-tile {
+ flex: 0 0 100px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: pointer;
+ padding: 12px 8px;
+ border-radius: var(--radius-md);
+ transition: transform var(--transition-fast), background var(--transition-fast);
+ scroll-snap-align: start;
+}
+
+.channel-tile:hover {
+ transform: translateY(-4px);
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.tile-logo {
+ width: 64px;
+ height: 64px;
+ border-radius: 50%;
+ overflow: hidden;
+ background: var(--color-surface);
+ border: 2px solid rgba(255, 255, 255, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 8px;
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
+}
+
+.channel-tile:hover .tile-logo {
+ border-color: var(--color-accent);
+ box-shadow: 0 0 16px rgba(var(--color-accent-rgb), 0.3);
+}
+
+.tile-logo img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.tile-name {
+ font-size: 0.75rem;
+ font-weight: 500;
+ color: var(--color-text-secondary);
+ text-align: center;
+ max-width: 90px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.channel-tile:hover .tile-name {
+ color: var(--color-text-bright);
+}
+
+/* Responsive: smaller tiles on mobile */
+@media (max-width: 480px) {
+ .channel-tile {
+ flex: 0 0 80px;
+ padding: 8px 4px;
+ }
+
+ .tile-logo {
+ width: 52px;
+ height: 52px;
+ }
+
+ .tile-name {
+ font-size: 0.7rem;
+ max-width: 70px;
+ }
}
\ No newline at end of file
diff --git a/public/img/placeholder.png b/public/img/placeholder.png
index 3f17944..c33035c 100644
Binary files a/public/img/placeholder.png and b/public/img/placeholder.png differ
diff --git a/public/img/poster-placeholder.jpg b/public/img/poster-placeholder.jpg
new file mode 100644
index 0000000..c3283e9
Binary files /dev/null and b/public/img/poster-placeholder.jpg differ
diff --git a/public/img/screenshots/screenshot-1.png b/public/img/screenshots/screenshot-1.png
index 9a677ba..fbe612d 100644
Binary files a/public/img/screenshots/screenshot-1.png and b/public/img/screenshots/screenshot-1.png differ
diff --git a/public/img/screenshots/screenshot-2.png b/public/img/screenshots/screenshot-2.png
index b91d0f6..f049627 100644
Binary files a/public/img/screenshots/screenshot-2.png and b/public/img/screenshots/screenshot-2.png differ
diff --git a/public/img/screenshots/screenshot-3.png b/public/img/screenshots/screenshot-3.png
index 4a30fb6..563fb2e 100644
Binary files a/public/img/screenshots/screenshot-3.png and b/public/img/screenshots/screenshot-3.png differ
diff --git a/public/img/screenshots/screenshot-4.png b/public/img/screenshots/screenshot-4.png
index e766d7b..5575c2a 100644
Binary files a/public/img/screenshots/screenshot-4.png and b/public/img/screenshots/screenshot-4.png differ
diff --git a/public/img/screenshots/screenshot-dashboard.png b/public/img/screenshots/screenshot-dashboard.png
new file mode 100644
index 0000000..25cfd56
Binary files /dev/null and b/public/img/screenshots/screenshot-dashboard.png differ
diff --git a/public/img/screenshots/screenshot-settings.png b/public/img/screenshots/screenshot-settings.png
new file mode 100644
index 0000000..db1e75b
Binary files /dev/null and b/public/img/screenshots/screenshot-settings.png differ
diff --git a/public/index.html b/public/index.html
index 01450cc..cfb728c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,7 +8,7 @@
- NodeCast TV | IPTV Player
+ nodecast-tv | IPTV Player
@@ -39,7 +39,19 @@