Added experimental force remux setting to remux raw ts streams to mp4 container
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user