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;