feat: add -seekable 0 to fix 405 errors and configurable User-Agent presets for stream requests
This commit is contained in:
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nodecast-tv",
|
"name": "nodecast-tv",
|
||||||
"version": "1.5.0",
|
"version": "1.5.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -540,6 +540,27 @@
|
|||||||
<option value="ts">MPEG-TS (ts) - Better buffer</option>
|
<option value="ts">MPEG-TS (ts) - Better buffer</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info" style="flex: 1;">
|
||||||
|
<span class="setting-label">User-Agent</span>
|
||||||
|
<span class="setting-hint">HTTP User-Agent for stream requests. Try VLC if your provider blocks
|
||||||
|
Chrome.</span>
|
||||||
|
</div>
|
||||||
|
<select id="setting-user-agent" class="source-select" style="width: auto; min-width: 200px;">
|
||||||
|
<option value="chrome">Chrome (Default)</option>
|
||||||
|
<option value="vlc">VLC</option>
|
||||||
|
<option value="tivimate">TiviMate</option>
|
||||||
|
<option value="custom">Custom...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item" id="custom-user-agent-container" style="display: none;">
|
||||||
|
<div class="setting-info" style="flex: 1;">
|
||||||
|
<span class="setting-label">Custom User-Agent</span>
|
||||||
|
<span class="setting-hint">Enter your own User-Agent string.</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="setting-user-agent-custom" class="search-input" style="width: 300px;"
|
||||||
|
placeholder="Custom User-Agent string...">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
|
|||||||
@@ -145,6 +145,39 @@ class SettingsPage {
|
|||||||
this.app.player.saveSettings();
|
this.app.player.saveSettings();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User-Agent preset
|
||||||
|
const userAgentSelect = document.getElementById('setting-user-agent');
|
||||||
|
const userAgentCustomInput = document.getElementById('setting-user-agent-custom');
|
||||||
|
const customUaContainer = document.getElementById('custom-user-agent-container');
|
||||||
|
|
||||||
|
if (userAgentSelect && this.app.player?.settings) {
|
||||||
|
// Load saved values
|
||||||
|
userAgentSelect.value = this.app.player.settings.userAgentPreset || 'chrome';
|
||||||
|
if (userAgentCustomInput) {
|
||||||
|
userAgentCustomInput.value = this.app.player.settings.userAgentCustom || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/hide custom input
|
||||||
|
const toggleCustomInput = () => {
|
||||||
|
if (customUaContainer) {
|
||||||
|
customUaContainer.style.display = userAgentSelect.value === 'custom' ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
toggleCustomInput();
|
||||||
|
|
||||||
|
// Save on change
|
||||||
|
userAgentSelect.addEventListener('change', () => {
|
||||||
|
this.app.player.settings.userAgentPreset = userAgentSelect.value;
|
||||||
|
this.app.player.saveSettings();
|
||||||
|
toggleCustomInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
userAgentCustomInput?.addEventListener('change', () => {
|
||||||
|
this.app.player.settings.userAgentCustom = userAgentCustomInput.value;
|
||||||
|
this.app.player.saveSettings();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initUserManagement() {
|
initUserManagement() {
|
||||||
@@ -297,6 +330,18 @@ class SettingsPage {
|
|||||||
if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode || false;
|
if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode || false;
|
||||||
if (epgRefreshSelect) epgRefreshSelect.value = s.epgRefreshInterval || '24';
|
if (epgRefreshSelect) epgRefreshSelect.value = s.epgRefreshInterval || '24';
|
||||||
if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8';
|
if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8';
|
||||||
|
|
||||||
|
// User-Agent settings
|
||||||
|
const userAgentSelect = document.getElementById('setting-user-agent');
|
||||||
|
const userAgentCustomInput = document.getElementById('setting-user-agent-custom');
|
||||||
|
const customUaContainer = document.getElementById('custom-user-agent-container');
|
||||||
|
if (userAgentSelect) {
|
||||||
|
userAgentSelect.value = s.userAgentPreset || 'chrome';
|
||||||
|
if (customUaContainer) {
|
||||||
|
customUaContainer.style.display = userAgentSelect.value === 'custom' ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userAgentCustomInput) userAgentCustomInput.value = s.userAgentCustom || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update EPG last refreshed display
|
// Update EPG last refreshed display
|
||||||
|
|||||||
+22
-2
@@ -67,10 +67,30 @@ function getDefaultSettings() {
|
|||||||
forceRemux: false,
|
forceRemux: false,
|
||||||
autoTranscode: true,
|
autoTranscode: true,
|
||||||
streamFormat: 'm3u8',
|
streamFormat: 'm3u8',
|
||||||
epgRefreshInterval: '24'
|
epgRefreshInterval: '24',
|
||||||
|
// User-Agent settings
|
||||||
|
userAgentPreset: 'chrome', // chrome | vlc | tivimate | custom
|
||||||
|
userAgentCustom: '', // Custom UA string when preset is 'custom'
|
||||||
|
// Probe cache settings
|
||||||
|
probeCacheTTL: 300, // 5 minutes for URL probe cache
|
||||||
|
seriesProbeCacheDays: 7 // 7 days for series episode probe cache
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User-Agent presets
|
||||||
|
const USER_AGENT_PRESETS = {
|
||||||
|
chrome: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||||
|
vlc: 'VLC/3.0.20 LibVLC/3.0.20',
|
||||||
|
tivimate: 'TiviMate/4.7.0',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getUserAgent(settings) {
|
||||||
|
if (settings.userAgentPreset === 'custom' && settings.userAgentCustom) {
|
||||||
|
return settings.userAgentCustom;
|
||||||
|
}
|
||||||
|
return USER_AGENT_PRESETS[settings.userAgentPreset] || USER_AGENT_PRESETS.chrome;
|
||||||
|
}
|
||||||
|
|
||||||
// Write lock to prevent concurrent writes from corrupting db.json
|
// Write lock to prevent concurrent writes from corrupting db.json
|
||||||
let writeQueue = Promise.resolve();
|
let writeQueue = Promise.resolve();
|
||||||
const tmpPath = dbPath + '.tmp';
|
const tmpPath = dbPath + '.tmp';
|
||||||
@@ -422,4 +442,4 @@ const users = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { loadDb, saveDb, sources, hiddenItems, favorites, settings, users, getDefaultSettings };
|
module.exports = { loadDb, saveDb, sources, hiddenItems, favorites, settings, users, getDefaultSettings, getUserAgent, USER_AGENT_PRESETS };
|
||||||
|
|||||||
+11
-2
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
|
const db = require('../db');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remux stream (container conversion only)
|
* Remux stream (container conversion only)
|
||||||
@@ -12,21 +13,27 @@ const { spawn } = require('child_process');
|
|||||||
*
|
*
|
||||||
* Note: This does NOT fix Dolby/AC3 audio issues - use /api/transcode for that.
|
* Note: This does NOT fix Dolby/AC3 audio issues - use /api/transcode for that.
|
||||||
*/
|
*/
|
||||||
router.get('/', (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const { url } = req.query;
|
const { url } = req.query;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return res.status(400).json({ error: 'URL parameter is required' });
|
return res.status(400).json({ error: 'URL parameter is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ffmpegPath = req.app.locals.ffmpegPath || 'ffmpeg';
|
const ffmpegPath = req.app.locals.ffmpegPath || 'ffmpeg';
|
||||||
|
|
||||||
|
// Get User-Agent from settings
|
||||||
|
const settings = await db.settings.get();
|
||||||
|
const userAgent = db.getUserAgent(settings);
|
||||||
|
|
||||||
console.log(`[Remux] Starting remux for: ${url}`);
|
console.log(`[Remux] Starting remux for: ${url}`);
|
||||||
|
console.log(`[Remux] Using User-Agent: ${settings.userAgentPreset}`);
|
||||||
|
|
||||||
// FFmpeg arguments for pure remux (no encoding)
|
// FFmpeg arguments for pure remux (no encoding)
|
||||||
// Very lightweight - just changes container from TS to fragmented MP4
|
// Very lightweight - just changes container from TS to fragmented MP4
|
||||||
const args = [
|
const args = [
|
||||||
'-hide_banner',
|
'-hide_banner',
|
||||||
'-loglevel', 'warning',
|
'-loglevel', 'warning',
|
||||||
'-user_agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
'-user_agent', userAgent,
|
||||||
// Low-latency startup: reduce probe/analyze time for faster first bytes
|
// Low-latency startup: reduce probe/analyze time for faster first bytes
|
||||||
'-probesize', '32768',
|
'-probesize', '32768',
|
||||||
'-analyzeduration', '500000', // 0.5 seconds - enough to detect audio
|
'-analyzeduration', '500000', // 0.5 seconds - enough to detect audio
|
||||||
@@ -40,6 +47,8 @@ router.get('/', (req, res) => {
|
|||||||
'-reconnect', '1',
|
'-reconnect', '1',
|
||||||
'-reconnect_streamed', '1',
|
'-reconnect_streamed', '1',
|
||||||
'-reconnect_delay_max', '5',
|
'-reconnect_delay_max', '5',
|
||||||
|
// Prevent Range/HEAD requests that some providers reject with 405
|
||||||
|
'-seekable', '0',
|
||||||
'-i', url,
|
'-i', url,
|
||||||
// STRICT MAPPING: Only map video and audio, ignore subtitles/data/attachments
|
// STRICT MAPPING: Only map video and audio, ignore subtitles/data/attachments
|
||||||
// This prevents remux failure when source container has incompatible subtitle tracks (e.g. MKV -> MP4)
|
// This prevents remux failure when source container has incompatible subtitle tracks (e.g. MKV -> MP4)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
|
const db = require('../db');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transcode stream
|
* Transcode stream
|
||||||
@@ -9,14 +10,20 @@ const { spawn } = require('child_process');
|
|||||||
* Transcodes audio to AAC for browser compatibility while passing video through.
|
* Transcodes audio to AAC for browser compatibility while passing video through.
|
||||||
* This fixes playback issues with Dolby/AC3/EAC3 audio that browsers can't decode.
|
* This fixes playback issues with Dolby/AC3/EAC3 audio that browsers can't decode.
|
||||||
*/
|
*/
|
||||||
router.get('/', (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const { url } = req.query;
|
const { url } = req.query;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return res.status(400).json({ error: 'URL parameter is required' });
|
return res.status(400).json({ error: 'URL parameter is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const ffmpegPath = req.app.locals.ffmpegPath || 'ffmpeg';
|
const ffmpegPath = req.app.locals.ffmpegPath || 'ffmpeg';
|
||||||
|
|
||||||
|
// Get User-Agent from settings
|
||||||
|
const settings = await db.settings.get();
|
||||||
|
const userAgent = db.getUserAgent(settings);
|
||||||
|
|
||||||
console.log(`[Transcode] Starting transcoding for: ${url}`);
|
console.log(`[Transcode] Starting transcoding for: ${url}`);
|
||||||
|
console.log(`[Transcode] Using User-Agent: ${settings.userAgentPreset}`);
|
||||||
console.log(`[Transcode] Using binary: ${ffmpegPath}`);
|
console.log(`[Transcode] Using binary: ${ffmpegPath}`);
|
||||||
|
|
||||||
// FFmpeg arguments for transcoding
|
// FFmpeg arguments for transcoding
|
||||||
@@ -25,7 +32,7 @@ router.get('/', (req, res) => {
|
|||||||
const args = [
|
const args = [
|
||||||
'-hide_banner',
|
'-hide_banner',
|
||||||
'-loglevel', 'warning',
|
'-loglevel', 'warning',
|
||||||
'-user_agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
'-user_agent', userAgent,
|
||||||
// Faster startup - reduced probe/analyze for quicker first bytes
|
// Faster startup - reduced probe/analyze for quicker first bytes
|
||||||
'-probesize', '2000000', // 2MB (reduced from 5MB)
|
'-probesize', '2000000', // 2MB (reduced from 5MB)
|
||||||
'-analyzeduration', '3000000', // 3 seconds (reduced from 10s)
|
'-analyzeduration', '3000000', // 3 seconds (reduced from 10s)
|
||||||
@@ -39,6 +46,8 @@ router.get('/', (req, res) => {
|
|||||||
'-reconnect', '1',
|
'-reconnect', '1',
|
||||||
'-reconnect_streamed', '1',
|
'-reconnect_streamed', '1',
|
||||||
'-reconnect_delay_max', '3',
|
'-reconnect_delay_max', '3',
|
||||||
|
// Prevent Range/HEAD requests that some providers reject with 405
|
||||||
|
'-seekable', '0',
|
||||||
'-i', url,
|
'-i', url,
|
||||||
// Map only first video and audio stream (avoid subtitle streams causing issues)
|
// Map only first video and audio stream (avoid subtitle streams causing issues)
|
||||||
'-map', '0:v:0',
|
'-map', '0:v:0',
|
||||||
|
|||||||
Reference in New Issue
Block a user