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",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -540,6 +540,27 @@
|
||||
<option value="ts">MPEG-TS (ts) - Better buffer</option>
|
||||
</select>
|
||||
</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 class="settings-section">
|
||||
|
||||
@@ -145,6 +145,39 @@ class SettingsPage {
|
||||
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() {
|
||||
@@ -297,6 +330,18 @@ class SettingsPage {
|
||||
if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode || false;
|
||||
if (epgRefreshSelect) epgRefreshSelect.value = s.epgRefreshInterval || '24';
|
||||
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
|
||||
|
||||
+22
-2
@@ -67,10 +67,30 @@ function getDefaultSettings() {
|
||||
forceRemux: false,
|
||||
autoTranscode: true,
|
||||
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
|
||||
let writeQueue = Promise.resolve();
|
||||
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 router = express.Router();
|
||||
const { spawn } = require('child_process');
|
||||
const db = require('../db');
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
router.get('/', async (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';
|
||||
|
||||
// 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] Using User-Agent: ${settings.userAgentPreset}`);
|
||||
|
||||
// FFmpeg arguments for pure remux (no encoding)
|
||||
// Very lightweight - just changes container from TS to fragmented MP4
|
||||
const args = [
|
||||
'-hide_banner',
|
||||
'-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
|
||||
'-probesize', '32768',
|
||||
'-analyzeduration', '500000', // 0.5 seconds - enough to detect audio
|
||||
@@ -40,6 +47,8 @@ router.get('/', (req, res) => {
|
||||
'-reconnect', '1',
|
||||
'-reconnect_streamed', '1',
|
||||
'-reconnect_delay_max', '5',
|
||||
// Prevent Range/HEAD requests that some providers reject with 405
|
||||
'-seekable', '0',
|
||||
'-i', url,
|
||||
// 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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { spawn } = require('child_process');
|
||||
const db = require('../db');
|
||||
|
||||
/**
|
||||
* Transcode stream
|
||||
@@ -9,14 +10,20 @@ const { spawn } = require('child_process');
|
||||
* 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.
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
router.get('/', async (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';
|
||||
|
||||
// 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] Using User-Agent: ${settings.userAgentPreset}`);
|
||||
console.log(`[Transcode] Using binary: ${ffmpegPath}`);
|
||||
|
||||
// FFmpeg arguments for transcoding
|
||||
@@ -25,7 +32,7 @@ router.get('/', (req, res) => {
|
||||
const args = [
|
||||
'-hide_banner',
|
||||
'-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
|
||||
'-probesize', '2000000', // 2MB (reduced from 5MB)
|
||||
'-analyzeduration', '3000000', // 3 seconds (reduced from 10s)
|
||||
@@ -39,6 +46,8 @@ router.get('/', (req, res) => {
|
||||
'-reconnect', '1',
|
||||
'-reconnect_streamed', '1',
|
||||
'-reconnect_delay_max', '3',
|
||||
// Prevent Range/HEAD requests that some providers reject with 405
|
||||
'-seekable', '0',
|
||||
'-i', url,
|
||||
// Map only first video and audio stream (avoid subtitle streams causing issues)
|
||||
'-map', '0:v:0',
|
||||
|
||||
Reference in New Issue
Block a user