feat: add dedicated transcoding settings tab

This commit is contained in:
Trevor Mears
2026-01-17 01:27:27 -08:00
parent f0c1393ce0
commit 77eb7bff5f
4 changed files with 299 additions and 116 deletions
+47
View File
@@ -4025,4 +4025,51 @@ kbd {
width: 18px;
height: 18px;
flex-shrink: 0;
}
/* Hardware Info Grid (Transcoding Tab) */
.hw-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--space-sm);
}
.hw-info-item {
display: flex;
flex-direction: column;
gap: var(--space-xs);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-sm);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
}
.hw-info-item.hw-available {
border-color: var(--color-success);
background: rgba(16, 185, 129, 0.1);
}
.hw-info-item.hw-unavailable {
opacity: 0.6;
}
.hw-badge {
font-weight: 600;
font-size: 0.875rem;
}
.hw-info-item.hw-available .hw-badge {
color: var(--color-success);
}
.hw-info-item.hw-unavailable .hw-badge {
color: var(--color-text-muted);
}
.hw-name {
font-size: 0.75rem;
color: var(--color-text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
+88 -31
View File
@@ -368,6 +368,7 @@
<div class="tabs">
<button class="tab active" data-tab="sources">Sources</button>
<button class="tab" data-tab="player">Player</button>
<button class="tab" data-tab="transcode">Transcoding</button>
<button class="tab" data-tab="content">Manage Content</button>
<button class="tab" data-tab="users" id="users-tab" style="display:none;">Users</button>
</div>
@@ -484,21 +485,75 @@
</div>
<div class="settings-section">
<h3>Streaming</h3>
<h3>Keyboard Shortcuts</h3>
<div class="shortcuts-grid">
<div class="shortcut"><kbd>↑</kbd> / <kbd>↓</kbd> Channel up/down (or volume)</div>
<div class="shortcut"><kbd>←</kbd> / <kbd>→</kbd> Volume down/up (when arrows = channels)</div>
<div class="shortcut"><kbd>Space</kbd> / <kbd>K</kbd> Play/Pause</div>
<div class="shortcut"><kbd>F</kbd> Fullscreen</div>
<div class="shortcut"><kbd>M</kbd> Mute/Unmute</div>
<div class="shortcut"><kbd>I</kbd> Toggle info overlay</div>
<div class="shortcut"><kbd>PgUp</kbd> / <kbd>PgDn</kbd> Channel up/down</div>
</div>
</div>
</div>
<!-- Transcoding Tab -->
<div id="tab-transcode" class="tab-content">
<div class="settings-section">
<h3>Detected Hardware</h3>
<div id="hw-info-container" class="hw-info-container">
<p class="hint">Loading hardware information...</p>
</div>
</div>
<div class="settings-section">
<h3>Encoder Settings</h3>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-auto-transcode">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Auto Transcode (Smart)</span>
<span class="setting-hint">Automatically detect stream codecs and only transcode/remux when needed.
Adds ~1-3s probe delay on first play.</span>
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Hardware Encoder</span>
<span class="setting-hint">Select which GPU encoder to use for transcoding. Auto will use the best
available.</span>
</div>
<select id="setting-hw-encoder" class="source-select" style="width: auto; min-width: 200px;">
<option value="auto">Auto (Recommended)</option>
<option value="nvenc">NVIDIA NVENC</option>
<option value="amf">AMD AMF</option>
<option value="qsv">Intel QuickSync</option>
<option value="vaapi">VAAPI (Linux)</option>
<option value="software">Software (CPU)</option>
</select>
</div>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Max Resolution</span>
<span class="setting-hint">Limit transcoded output resolution. Lower = faster, more compatible.</span>
</div>
<select id="setting-max-resolution" class="source-select" style="width: auto; min-width: 200px;">
<option value="4k">4K (2160p)</option>
<option value="1080p" selected>1080p (Full HD)</option>
<option value="720p">720p (HD)</option>
<option value="480p">480p (SD)</option>
</select>
</div>
<div class="setting-item">
<div class="setting-info" style="flex: 1;">
<span class="setting-label">Quality Preset</span>
<span class="setting-hint">Higher quality uses more bandwidth and CPU/GPU.</span>
</div>
<select id="setting-quality" class="source-select" style="width: auto; min-width: 200px;">
<option value="high">High</option>
<option value="medium" selected>Medium</option>
<option value="low">Low (Fastest)</option>
</select>
</div>
</div>
<div class="settings-section">
<h3>Stream Processing</h3>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-force-proxy">
<input type="checkbox" id="setting-force-proxy-tc">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
@@ -509,7 +564,18 @@
</div>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-force-transcode">
<input type="checkbox" id="setting-auto-transcode-tc">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
<span class="setting-label">Auto Transcode (Smart)</span>
<span class="setting-hint">Automatically detect stream codecs and only transcode/remux when needed.
Adds ~1-3s probe delay on first play.</span>
</div>
</div>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-force-transcode-tc">
<span class="toggle-slider"></span>
</label>
<div class="setting-info">
@@ -520,13 +586,13 @@
</div>
<div class="setting-item">
<label class="setting-toggle">
<input type="checkbox" id="setting-force-remux">
<input type="checkbox" id="setting-force-remux-tc">
<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. Enable for raw .ts streams
from IPTV middleware like m3u-editor or dispatcharr.</span>
<span class="setting-hint">Remux streams to MP4 container. Enable for raw .ts streams from IPTV
middleware like m3u-editor or dispatcharr.</span>
</div>
</div>
<div class="setting-item">
@@ -535,46 +601,37 @@
<span class="setting-hint">Container format for Xtream streams. Try TS if you experience buffering
issues.</span>
</div>
<select id="setting-stream-format" class="source-select" style="width: auto; min-width: 200px;">
<select id="setting-stream-format-tc" class="source-select" style="width: auto; min-width: 200px;">
<option value="m3u8">HLS (m3u8) - Recommended</option>
<option value="ts">MPEG-TS (ts) - Better buffer</option>
</select>
</div>
</div>
<div class="settings-section">
<h3>HTTP Settings</h3>
<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;">
<select id="setting-user-agent-tc" 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-item" id="custom-user-agent-container-tc" 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;"
<input type="text" id="setting-user-agent-custom-tc" class="search-input" style="width: 300px;"
placeholder="Custom User-Agent string...">
</div>
</div>
<div class="settings-section">
<h3>Keyboard Shortcuts</h3>
<div class="shortcuts-grid">
<div class="shortcut"><kbd>↑</kbd> / <kbd>↓</kbd> Channel up/down (or volume)</div>
<div class="shortcut"><kbd>←</kbd> / <kbd>→</kbd> Volume down/up (when arrows = channels)</div>
<div class="shortcut"><kbd>Space</kbd> / <kbd>K</kbd> Play/Pause</div>
<div class="shortcut"><kbd>F</kbd> Fullscreen</div>
<div class="shortcut"><kbd>M</kbd> Mute/Unmute</div>
<div class="shortcut"><kbd>I</kbd> Toggle info overlay</div>
<div class="shortcut"><kbd>PgUp</kbd> / <kbd>PgDn</kbd> Channel up/down</div>
</div>
</div>
</div>
<!-- Manage Content Tab -->
+160 -85
View File
@@ -20,6 +20,9 @@ class SettingsPage {
// Player settings
this.initPlayerSettings();
// Transcoding settings
this.initTranscodingSettings();
// User management (admin only)
this.initUserManagement();
}
@@ -31,10 +34,6 @@ class SettingsPage {
const volumeValueDisplay = document.getElementById('volume-value');
const rememberVolumeToggle = document.getElementById('setting-remember-volume');
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');
const autoTranscodeToggle = document.getElementById('setting-auto-transcode');
// Load current settings
if (this.app.player?.settings) {
@@ -44,18 +43,6 @@ class SettingsPage {
volumeValueDisplay.textContent = this.app.player.settings.defaultVolume + '%';
rememberVolumeToggle.checked = this.app.player.settings.rememberVolume;
autoPlayNextToggle.checked = this.app.player.settings.autoPlayNextEpisode;
if (forceProxyToggle) {
forceProxyToggle.checked = this.app.player.settings.forceProxy || false;
}
if (forceTranscodeToggle) {
forceTranscodeToggle.checked = this.app.player.settings.forceTranscode || false;
}
if (forceRemuxToggle) {
forceRemuxToggle.checked = this.app.player.settings.forceRemux || false;
}
if (autoTranscodeToggle) {
autoTranscodeToggle.checked = this.app.player.settings.autoTranscode || false;
}
}
// Arrow keys toggle
@@ -66,56 +53,30 @@ class SettingsPage {
// Overlay duration
overlayDurationInput.addEventListener('change', () => {
const value = Math.min(30, Math.max(1, parseInt(overlayDurationInput.value) || 5));
overlayDurationInput.value = value;
this.app.player.settings.overlayDuration = value;
this.app.player.settings.overlayDuration = parseInt(overlayDurationInput.value) || 5;
this.app.player.saveSettings();
});
// Default volume slider
defaultVolumeSlider?.addEventListener('input', () => {
const value = parseInt(defaultVolumeSlider.value);
defaultVolumeSlider.addEventListener('input', () => {
const value = defaultVolumeSlider.value;
volumeValueDisplay.textContent = value + '%';
this.app.player.settings.defaultVolume = value;
this.app.player.settings.defaultVolume = parseInt(value);
this.app.player.saveSettings();
});
// Remember volume toggle
rememberVolumeToggle?.addEventListener('change', () => {
rememberVolumeToggle.addEventListener('change', () => {
this.app.player.settings.rememberVolume = rememberVolumeToggle.checked;
this.app.player.saveSettings();
});
// Auto-play next episode toggle
autoPlayNextToggle?.addEventListener('change', () => {
autoPlayNextToggle.addEventListener('change', () => {
this.app.player.settings.autoPlayNextEpisode = autoPlayNextToggle.checked;
this.app.player.saveSettings();
});
// Force proxy toggle
forceProxyToggle?.addEventListener('change', () => {
this.app.player.settings.forceProxy = forceProxyToggle.checked;
this.app.player.saveSettings();
});
// Force transcode toggle
forceTranscodeToggle?.addEventListener('change', () => {
this.app.player.settings.forceTranscode = forceTranscodeToggle.checked;
this.app.player.saveSettings();
});
// Force remux toggle
forceRemuxToggle?.addEventListener('change', () => {
this.app.player.settings.forceRemux = forceRemuxToggle.checked;
this.app.player.saveSettings();
});
// Auto transcode (smart) toggle
autoTranscodeToggle?.addEventListener('change', () => {
this.app.player.settings.autoTranscode = autoTranscodeToggle.checked;
this.app.player.saveSettings();
});
// EPG refresh interval
const epgRefreshSelect = document.getElementById('epg-refresh-interval');
if (epgRefreshSelect && this.app.player?.settings) {
@@ -126,57 +87,166 @@ class SettingsPage {
epgRefreshSelect.addEventListener('change', () => {
this.app.player.settings.epgRefreshInterval = epgRefreshSelect.value;
this.app.player.saveSettings();
// Server-side sync timer is restarted automatically via settings API
});
}
// Update last refreshed display
this.updateEpgLastRefreshed();
}
// Stream output format
const streamFormatSelect = document.getElementById('setting-stream-format');
if (streamFormatSelect && this.app.player?.settings) {
// Load saved value from player settings
streamFormatSelect.value = this.app.player.settings.streamFormat || 'm3u8';
initTranscodingSettings() {
// Encoder settings
const hwEncoderSelect = document.getElementById('setting-hw-encoder');
const maxResolutionSelect = document.getElementById('setting-max-resolution');
const qualitySelect = document.getElementById('setting-quality');
// Save on change
streamFormatSelect.addEventListener('change', () => {
this.app.player.settings.streamFormat = streamFormatSelect.value;
this.app.player.saveSettings();
});
// Stream processing (use -tc suffix IDs from Transcoding tab)
const forceProxyToggle = document.getElementById('setting-force-proxy-tc');
const autoTranscodeToggle = document.getElementById('setting-auto-transcode-tc');
const forceTranscodeToggle = document.getElementById('setting-force-transcode-tc');
const forceRemuxToggle = document.getElementById('setting-force-remux-tc');
const streamFormatSelect = document.getElementById('setting-stream-format-tc');
// User-Agent (Transcoding tab versions)
const userAgentSelect = document.getElementById('setting-user-agent-tc');
const userAgentCustomInput = document.getElementById('setting-user-agent-custom-tc');
const customUaContainer = document.getElementById('custom-user-agent-container-tc');
// Load current settings
if (this.app.player?.settings) {
const s = this.app.player.settings;
if (hwEncoderSelect) hwEncoderSelect.value = s.hwEncoder || 'auto';
if (maxResolutionSelect) maxResolutionSelect.value = s.maxResolution || '1080p';
if (qualitySelect) qualitySelect.value = s.quality || 'medium';
if (forceProxyToggle) forceProxyToggle.checked = s.forceProxy === true;
if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode !== false;
if (forceTranscodeToggle) forceTranscodeToggle.checked = s.forceTranscode === true;
if (forceRemuxToggle) forceRemuxToggle.checked = s.forceRemux || false;
if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8';
if (userAgentSelect) userAgentSelect.value = s.userAgentPreset || 'chrome';
if (userAgentCustomInput) userAgentCustomInput.value = s.userAgentCustom || '';
if (customUaContainer) {
customUaContainer.style.display = userAgentSelect?.value === 'custom' ? 'flex' : 'none';
}
}
// 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');
// Event listeners for encoder settings
hwEncoderSelect?.addEventListener('change', () => {
this.app.player.settings.hwEncoder = hwEncoderSelect.value;
this.app.player.saveSettings();
});
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 || '';
maxResolutionSelect?.addEventListener('change', () => {
this.app.player.settings.maxResolution = maxResolutionSelect.value;
this.app.player.saveSettings();
});
qualitySelect?.addEventListener('change', () => {
this.app.player.settings.quality = qualitySelect.value;
this.app.player.saveSettings();
});
// Stream processing toggles
forceProxyToggle?.addEventListener('change', () => {
this.app.player.settings.forceProxy = forceProxyToggle.checked;
this.app.player.saveSettings();
});
autoTranscodeToggle?.addEventListener('change', () => {
this.app.player.settings.autoTranscode = autoTranscodeToggle.checked;
this.app.player.saveSettings();
});
forceTranscodeToggle?.addEventListener('change', () => {
this.app.player.settings.forceTranscode = forceTranscodeToggle.checked;
this.app.player.saveSettings();
});
forceRemuxToggle?.addEventListener('change', () => {
this.app.player.settings.forceRemux = forceRemuxToggle.checked;
this.app.player.saveSettings();
});
streamFormatSelect?.addEventListener('change', () => {
this.app.player.settings.streamFormat = streamFormatSelect.value;
this.app.player.saveSettings();
});
// User-Agent handlers
const toggleCustomInput = () => {
if (customUaContainer) {
customUaContainer.style.display = userAgentSelect?.value === 'custom' ? 'flex' : 'none';
}
};
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();
});
}
/**
* Load and display hardware info in Transcoding tab
*/
async loadHardwareInfo() {
const container = document.getElementById('hw-info-container');
if (!container) return;
try {
const response = await fetch('/api/settings/hw-info');
if (!response.ok) throw new Error('Failed to fetch hardware info');
const hwInfo = await response.json();
const detected = [];
// Only show detected hardware
if (hwInfo.nvidia?.available) {
detected.push(`<div class="hw-info-item hw-available">
<span class="hw-badge">✓ NVIDIA</span>
<span class="hw-name">${hwInfo.nvidia.name}</span>
</div>`);
}
// Show/hide custom input
const toggleCustomInput = () => {
if (customUaContainer) {
customUaContainer.style.display = userAgentSelect.value === 'custom' ? 'flex' : 'none';
}
};
toggleCustomInput();
if (hwInfo.amf?.available) {
detected.push(`<div class="hw-info-item hw-available">
<span class="hw-badge">✓ AMD</span>
<span class="hw-name">${hwInfo.amf.name || 'Available'}</span>
</div>`);
}
// Save on change
userAgentSelect.addEventListener('change', () => {
this.app.player.settings.userAgentPreset = userAgentSelect.value;
this.app.player.saveSettings();
toggleCustomInput();
});
if (hwInfo.qsv?.available) {
detected.push(`<div class="hw-info-item hw-available">
<span class="hw-badge">✓ Intel QSV</span>
<span class="hw-name">Available</span>
</div>`);
}
userAgentCustomInput?.addEventListener('change', () => {
this.app.player.settings.userAgentCustom = userAgentCustomInput.value;
this.app.player.saveSettings();
});
if (hwInfo.vaapi?.available) {
detected.push(`<div class="hw-info-item hw-available">
<span class="hw-badge">✓ VAAPI</span>
<span class="hw-name">${hwInfo.vaapi.device || 'Available'}</span>
</div>`);
}
let html;
if (detected.length > 0) {
html = `<div class="hw-info-grid">${detected.join('')}</div>`;
html += `<p class="hint" style="margin-top: var(--space-sm);">Recommended encoder: <strong>${hwInfo.recommended}</strong></p>`;
} else {
html = `<p class="hint">No GPU acceleration detected. Using software encoding.</p>`;
}
container.innerHTML = html;
} catch (err) {
console.error('Error loading hardware info:', err);
container.innerHTML = '<p class="hint error">Failed to load hardware info</p>';
}
}
@@ -286,6 +356,11 @@ class SettingsPage {
if (tabName === 'users') {
this.loadUsers();
}
// Load hardware info when switching to transcode tab
if (tabName === 'transcode') {
this.loadHardwareInfo();
}
}
async show() {
+4
View File
@@ -71,6 +71,10 @@ function getDefaultSettings() {
// User-Agent settings
userAgentPreset: 'chrome', // chrome | vlc | tivimate | custom
userAgentCustom: '', // Custom UA string when preset is 'custom'
// Transcoding settings
hwEncoder: 'auto', // auto | nvenc | amf | qsv | vaapi | software
maxResolution: '1080p', // 4k | 1080p | 720p | 480p
quality: 'medium', // high | medium | low
// Probe cache settings
probeCacheTTL: 300, // 5 minutes for URL probe cache
seriesProbeCacheDays: 7 // 7 days for series episode probe cache