From c037ef686288c4868dab9a3f075c347c154aa58d Mon Sep 17 00:00:00 2001 From: Trevor Mears Date: Tue, 30 Dec 2025 15:27:19 -0800 Subject: [PATCH] Added experimental force remux setting to remux raw ts streams to mp4 container --- README.md | 43 +++++++++++--- public/index.html | 11 ++++ public/js/components/VideoPlayer.js | 44 ++++++++++++++- public/js/pages/Settings.js | 10 ++++ server/index.js | 1 + server/routes/remux.js | 87 +++++++++++++++++++++++++++++ 6 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 server/routes/remux.js diff --git a/README.md b/README.md index d72d2c6..9331b75 100644 --- a/README.md +++ b/README.md @@ -129,10 +129,38 @@ For streams with Dolby Digital (AC3/EAC3) audio that browsers can't decode nativ NodeCast TV is optimized for **HLS (HTTP Live Streaming)**. - **✅ HLS (`.m3u8`)**: Fully supported and recommended. Best for adaptive bitrate and network resilience. -- **✅ MPEG-TS (`.ts`)**: Supported via the backend proxy. Works well for standard IPTV providers (Xtream Codes). +- **✅ MPEG-TS (`.ts`)**: Supported via Force Remux or Force Audio Transcode settings. - **⚠️ 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. +## Streaming Settings + +All streaming settings are found in **Settings → Player → Streaming**. + +| 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 | + +### Which Setting Do I Need? + +``` +Stream won't play at all? +├── CORS error in console? → Enable "Force Backend Proxy" +├── Raw .ts URL from middleware? → Enable "Force Remux" +└── Still not working? → Check codec support table above + +Video plays but no audio? +├── Stream has Dolby/AC3/EAC3? → Enable "Force Audio Transcode" +└── Using Safari? → Should work natively, check volume + +Buffering issues? +└── Try changing "Stream Output Format" to TS +``` + + ## Troubleshooting ### Video Won't Play (Black Screen or Loading Forever) @@ -198,6 +226,7 @@ If you're using IPTV middleware like **m3u-editor**, **dispatcharr**, **Threadfi | 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) | | **Stream Format: TS** | Settings → Streaming | If HLS streams buffer excessively | @@ -209,8 +238,8 @@ 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 Backend Proxy"** in Settings → Streaming -3. If audio doesn't play, enable **"Force Audio Transcode"** +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). @@ -222,8 +251,8 @@ dispatcharr uses FFmpeg stream profiles to process streams. By default it output **Setup:** 1. In dispatcharr, streams are proxied by default via stream profiles -2. In NodeCast TV, enable **"Force Backend Proxy"** in Settings → Streaming -3. If audio doesn't play, enable **"Force Audio Transcode"** +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: @@ -240,8 +269,8 @@ 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 Backend Proxy"** in Settings → Streaming -3. If needed, enable **"Force Audio Transcode"** +2. Enable **"Force Remux"** in Settings → Streaming (for raw .ts streams) +3. If needed, enable **"Force Audio Transcode"** instead for Dolby audio ### TVHeadend diff --git a/public/index.html b/public/index.html index 6821a27..87425d8 100644 --- a/public/index.html +++ b/public/index.html @@ -365,6 +365,17 @@ but no audio (fixes Dolby/AC3/EAC3). Requires ffmpeg-static. +
+ +
+ Force Remux + Remux streams to MP4 container (no re-encoding). Enable for raw .ts streams + from IPTV middleware like m3u-editor or dispatcharr. Very lightweight. +
+
Stream Output Format diff --git a/public/js/components/VideoPlayer.js b/public/js/components/VideoPlayer.js index f5f6896..b2200ea 100644 --- a/public/js/components/VideoPlayer.js +++ b/public/js/components/VideoPlayer.js @@ -34,7 +34,8 @@ class VideoPlayer { lastVolume: 80, // Last used volume (if rememberVolume is true) autoPlayNextEpisode: false, // Auto-play next episode in series forceProxy: false, // Force all streams through backend proxy - forceTranscode: false // Force audio transcoding (fixes Dolby/AC3 audio) + forceTranscode: false, // Force audio transcoding (fixes Dolby/AC3 audio) + forceRemux: false // Force remux to MP4 (fixes raw .ts streams, lightweight) }; try { const saved = localStorage.getItem('nodecast_tv_player_settings'); @@ -263,9 +264,40 @@ class VideoPlayer { const finalUrl = needsProxy ? this.getProxiedUrl(streamUrl) : streamUrl; // Detect if this is likely an HLS stream (vs direct video file like MP4) + // Note: .ts files are raw MPEG-TS, NOT HLS manifests - exclude them const looksLikeHls = finalUrl.includes('.m3u8') || finalUrl.includes('m3u8') || - (!finalUrl.includes('.mp4') && !finalUrl.includes('.mkv') && !finalUrl.includes('.avi')); + (!finalUrl.includes('.mp4') && !finalUrl.includes('.mkv') && + !finalUrl.includes('.avi') && !finalUrl.includes('.ts')); + + // Check for raw TS streams that browsers can't play directly + const isRawTs = finalUrl.includes('.ts') && !finalUrl.includes('.m3u8'); + if (isRawTs) { + // If Force Remux is enabled, route through remux endpoint + if (this.settings.forceRemux) { + console.log('[Player] Raw TS stream detected. Using remux to convert to MP4...'); + const remuxUrl = this.getRemuxUrl(streamUrl); + this.video.src = remuxUrl; + this.video.play().catch(e => console.log('[Player] Autoplay prevented:', e)); + + // Update UI and dispatch events + this.updateNowPlaying(channel); + this.showNowPlayingOverlay(); + this.fetchEpgData(channel); + window.dispatchEvent(new CustomEvent('channelChanged', { detail: channel })); + return; + } + + console.warn('[Player] Raw MPEG-TS stream detected. Browsers cannot play .ts files directly.'); + this.showError( + 'This stream uses raw MPEG-TS format (.ts) which browsers cannot play directly.

' + + 'To fix this:
' + + '1. Enable "Force Remux" in Settings → Streaming (lightweight)
' + + '2. Or enable "Force Audio Transcode" if you also have audio issues
' + + '3. Or configure your source to output HLS (.m3u8) format' + ); + return; + } // Priority 1: Use HLS.js for HLS streams on browsers that support it if (looksLikeHls && Hls.isSupported()) { @@ -465,6 +497,14 @@ class VideoPlayer { return `/api/transcode?url=${encodeURIComponent(url)}`; } + /** + * Get remuxed URL for a stream (container conversion only, no re-encoding) + * Used for raw .ts streams that browsers can't play directly + */ + getRemuxUrl(url) { + return `/api/remux?url=${encodeURIComponent(url)}`; + } + /** * Decode base64 EPG data */ diff --git a/public/js/pages/Settings.js b/public/js/pages/Settings.js index e3d2208..c4e4bb7 100644 --- a/public/js/pages/Settings.js +++ b/public/js/pages/Settings.js @@ -30,6 +30,7 @@ class SettingsPage { const autoPlayNextToggle = document.getElementById('setting-autoplay-next'); const forceProxyToggle = document.getElementById('setting-force-proxy'); const forceTranscodeToggle = document.getElementById('setting-force-transcode'); + const forceRemuxToggle = document.getElementById('setting-force-remux'); // Load current settings if (this.app.player?.settings) { @@ -45,6 +46,9 @@ class SettingsPage { if (forceTranscodeToggle) { forceTranscodeToggle.checked = this.app.player.settings.forceTranscode || false; } + if (forceRemuxToggle) { + forceRemuxToggle.checked = this.app.player.settings.forceRemux || false; + } } // Arrow keys toggle @@ -93,6 +97,12 @@ class SettingsPage { this.app.player.saveSettings(); }); + // Force remux toggle + forceRemuxToggle?.addEventListener('change', () => { + this.app.player.settings.forceRemux = forceRemuxToggle.checked; + this.app.player.saveSettings(); + }); + // EPG refresh interval const epgRefreshSelect = document.getElementById('epg-refresh-interval'); if (epgRefreshSelect) { diff --git a/server/index.js b/server/index.js index 4d842f8..98555c7 100644 --- a/server/index.js +++ b/server/index.js @@ -37,6 +37,7 @@ app.use('/api/proxy', require('./routes/proxy')); app.use('/api/channels', require('./routes/channels')); app.use('/api/favorites', require('./routes/favorites')); app.use('/api/transcode', require('./routes/transcode')); +app.use('/api/remux', require('./routes/remux')); // SPA fallback - serve index.html for all non-API routes app.get('*', (req, res) => { diff --git a/server/routes/remux.js b/server/routes/remux.js new file mode 100644 index 0000000..476193b --- /dev/null +++ b/server/routes/remux.js @@ -0,0 +1,87 @@ +const express = require('express'); +const router = express.Router(); +const { spawn } = require('child_process'); + +/** + * Remux stream (container conversion only) + * GET /api/remux?url=... + * + * Remuxes MPEG-TS to fragmented MP4 for browser playback. + * This is a lightweight operation - no video/audio re-encoding. + * Use this for raw .ts streams that browsers can't play directly. + * + * Note: This does NOT fix Dolby/AC3 audio issues - use /api/transcode for that. + */ +router.get('/', (req, res) => { + const { url } = req.query; + if (!url) { + return res.status(400).json({ error: 'URL parameter is required' }); + } + + const ffmpegPath = req.app.locals.ffmpegPath || 'ffmpeg'; + console.log(`[Remux] Starting remux for: ${url}`); + + // FFmpeg arguments for pure remux (no encoding) + // Very lightweight - just changes container from TS to fragmented MP4 + const args = [ + '-hide_banner', + '-loglevel', 'warning', + '-fflags', '+genpts+discardcorrupt', + '-i', url, + // Copy ALL streams without re-encoding + '-c', 'copy', + // Fragmented MP4 for streaming (browser-compatible) + '-f', 'mp4', + '-movflags', 'frag_keyframe+empty_moov+default_base_moof', + '-' // Output to stdout + ]; + + console.log(`[Remux] Full command: ${ffmpegPath} ${args.join(' ')}`); + + let ffmpeg; + try { + ffmpeg = spawn(ffmpegPath, args); + } catch (spawnErr) { + console.error('[Remux] Failed to spawn FFmpeg:', spawnErr); + return res.status(500).json({ error: 'FFmpeg spawn failed', details: spawnErr.message }); + } + + // Set headers for fragmented MP4 + res.setHeader('Content-Type', 'video/mp4'); + res.setHeader('Access-Control-Allow-Origin', '*'); + + // Pipe stdout to response + ffmpeg.stdout.pipe(res); + + // Log stderr (useful for debugging) + ffmpeg.stderr.on('data', (data) => { + const msg = data.toString(); + // Only log warnings/errors, not progress + if (msg.includes('Warning') || msg.includes('Error') || msg.includes('error')) { + console.log(`[Remux FFmpeg] ${msg}`); + } + }); + + // Cleanup on client disconnect + req.on('close', () => { + console.log('[Remux] Client disconnected, killing FFmpeg process'); + ffmpeg.kill('SIGKILL'); + }); + + // Handle process exit + ffmpeg.on('exit', (code) => { + if (code !== null && code !== 0 && code !== 255) { + console.error(`[Remux] FFmpeg exited with code ${code}`); + } + }); + + // Handle spawn errors + ffmpeg.on('error', (err) => { + console.error('[Remux] Failed to spawn FFmpeg:', err); + if (!res.headersSent) { + res.status(500).json({ error: 'Remux failed to start' }); + } + }); +}); + +module.exports = router;