Added experimental force remux setting to remux raw ts streams to mp4 container

This commit is contained in:
Trevor Mears
2025-12-30 15:27:19 -08:00
parent 6daa3071be
commit c037ef6862
6 changed files with 187 additions and 9 deletions
+36 -7
View File
@@ -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)**. NodeCast TV is optimized for **HLS (HTTP Live Streaming)**.
- **✅ HLS (`.m3u8`)**: Fully supported and recommended. Best for adaptive bitrate and network resilience. - **✅ 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. - **⚠️ 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. - **❌ 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 ## Troubleshooting
### Video Won't Play (Black Screen or Loading Forever) ### 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 | | Setting | Location | When to Enable |
|---------|----------|----------------| |---------|----------|----------------|
| **Force Backend Proxy** | Settings → Player → Streaming | Always recommended when using middleware | | **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) | | **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 | | **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:** **Setup:**
1. In m3u-editor, configure your playlist and enable the proxy if needed 1. In m3u-editor, configure your playlist and enable the proxy if needed
2. In NodeCast TV, enable **"Force Backend Proxy"** in Settings → Streaming 2. In NodeCast TV, enable **"Force Remux"** in Settings → Streaming (for raw .ts streams)
3. If audio doesn't play, enable **"Force Audio Transcode"** 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). **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:** **Setup:**
1. In dispatcharr, streams are proxied by default via stream profiles 1. In dispatcharr, streams are proxied by default via stream profiles
2. In NodeCast TV, enable **"Force Backend Proxy"** in Settings → Streaming 2. In NodeCast TV, enable **"Force Remux"** in Settings → Streaming (for raw .ts streams)
3. If audio doesn't play, enable **"Force Audio Transcode"** 3. If audio doesn't play, enable **"Force Audio Transcode"** instead
**Custom dispatcharr profile for browser compatibility:** **Custom dispatcharr profile for browser compatibility:**
If you want dispatcharr to transcode audio for you instead of NodeCast TV: 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:** **Setup:**
1. Add your Threadfin/xTeVe M3U URL as an M3U source in NodeCast TV 1. Add your Threadfin/xTeVe M3U URL as an M3U source in NodeCast TV
2. Enable **"Force Backend Proxy"** in Settings → Streaming 2. Enable **"Force Remux"** in Settings → Streaming (for raw .ts streams)
3. If needed, enable **"Force Audio Transcode"** 3. If needed, enable **"Force Audio Transcode"** instead for Dolby audio
### TVHeadend ### TVHeadend
+11
View File
@@ -365,6 +365,17 @@
but no audio (fixes Dolby/AC3/EAC3). Requires ffmpeg-static.</span> but no audio (fixes Dolby/AC3/EAC3). Requires ffmpeg-static.</span>
</div> </div>
</div> </div>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-force-remux">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Force Remux</span>
<span class="setting-hint">Remux streams to MP4 container (no re-encoding). Enable for raw .ts streams
from IPTV middleware like m3u-editor or dispatcharr. Very lightweight.</span>
</div>
</div>
<div class="setting-item"> <div class="setting-item">
<div class="setting-info" style="flex: 1;"> <div class="setting-info" style="flex: 1;">
<span class="setting-label">Stream Output Format</span> <span class="setting-label">Stream Output Format</span>
+42 -2
View File
@@ -34,7 +34,8 @@ class VideoPlayer {
lastVolume: 80, // Last used volume (if rememberVolume is true) lastVolume: 80, // Last used volume (if rememberVolume is true)
autoPlayNextEpisode: false, // Auto-play next episode in series autoPlayNextEpisode: false, // Auto-play next episode in series
forceProxy: false, // Force all streams through backend proxy 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 { try {
const saved = localStorage.getItem('nodecast_tv_player_settings'); const saved = localStorage.getItem('nodecast_tv_player_settings');
@@ -263,9 +264,40 @@ class VideoPlayer {
const finalUrl = needsProxy ? this.getProxiedUrl(streamUrl) : streamUrl; const finalUrl = needsProxy ? this.getProxiedUrl(streamUrl) : streamUrl;
// Detect if this is likely an HLS stream (vs direct video file like MP4) // 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') || const looksLikeHls = finalUrl.includes('.m3u8') ||
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.<br><br>' +
'<strong>To fix this:</strong><br>' +
'1. Enable <strong>"Force Remux"</strong> in Settings → Streaming (lightweight)<br>' +
'2. Or enable <strong>"Force Audio Transcode"</strong> if you also have audio issues<br>' +
'3. Or configure your source to output HLS (.m3u8) format'
);
return;
}
// Priority 1: Use HLS.js for HLS streams on browsers that support it // Priority 1: Use HLS.js for HLS streams on browsers that support it
if (looksLikeHls && Hls.isSupported()) { if (looksLikeHls && Hls.isSupported()) {
@@ -465,6 +497,14 @@ class VideoPlayer {
return `/api/transcode?url=${encodeURIComponent(url)}`; 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 * Decode base64 EPG data
*/ */
+10
View File
@@ -30,6 +30,7 @@ class SettingsPage {
const autoPlayNextToggle = document.getElementById('setting-autoplay-next'); const autoPlayNextToggle = document.getElementById('setting-autoplay-next');
const forceProxyToggle = document.getElementById('setting-force-proxy'); const forceProxyToggle = document.getElementById('setting-force-proxy');
const forceTranscodeToggle = document.getElementById('setting-force-transcode'); const forceTranscodeToggle = document.getElementById('setting-force-transcode');
const forceRemuxToggle = document.getElementById('setting-force-remux');
// Load current settings // Load current settings
if (this.app.player?.settings) { if (this.app.player?.settings) {
@@ -45,6 +46,9 @@ class SettingsPage {
if (forceTranscodeToggle) { if (forceTranscodeToggle) {
forceTranscodeToggle.checked = this.app.player.settings.forceTranscode || false; forceTranscodeToggle.checked = this.app.player.settings.forceTranscode || false;
} }
if (forceRemuxToggle) {
forceRemuxToggle.checked = this.app.player.settings.forceRemux || false;
}
} }
// Arrow keys toggle // Arrow keys toggle
@@ -93,6 +97,12 @@ class SettingsPage {
this.app.player.saveSettings(); this.app.player.saveSettings();
}); });
// Force remux toggle
forceRemuxToggle?.addEventListener('change', () => {
this.app.player.settings.forceRemux = forceRemuxToggle.checked;
this.app.player.saveSettings();
});
// EPG refresh interval // EPG refresh interval
const epgRefreshSelect = document.getElementById('epg-refresh-interval'); const epgRefreshSelect = document.getElementById('epg-refresh-interval');
if (epgRefreshSelect) { if (epgRefreshSelect) {
+1
View File
@@ -37,6 +37,7 @@ app.use('/api/proxy', require('./routes/proxy'));
app.use('/api/channels', require('./routes/channels')); app.use('/api/channels', require('./routes/channels'));
app.use('/api/favorites', require('./routes/favorites')); app.use('/api/favorites', require('./routes/favorites'));
app.use('/api/transcode', require('./routes/transcode')); app.use('/api/transcode', require('./routes/transcode'));
app.use('/api/remux', require('./routes/remux'));
// SPA fallback - serve index.html for all non-API routes // SPA fallback - serve index.html for all non-API routes
app.get('*', (req, res) => { app.get('*', (req, res) => {
+87
View File
@@ -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;