diff --git a/public/css/main.css b/public/css/main.css index cef9814..c361bf1 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -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; } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 4b9bca2..07841b2 100644 --- a/public/index.html +++ b/public/index.html @@ -368,6 +368,7 @@
+
@@ -484,21 +485,75 @@
-

Streaming

+

Keyboard Shortcuts

+
+
↑ / ↓ Channel up/down (or volume)
+
← / → Volume down/up (when arrows = channels)
+
Space / K Play/Pause
+
F Fullscreen
+
M Mute/Unmute
+
I Toggle info overlay
+
PgUp / PgDn Channel up/down
+
+
+ + + +
+
+

Detected Hardware

+
+

Loading hardware information...

+
+
+ +
+

Encoder Settings

- -
- Auto Transcode (Smart) - Automatically detect stream codecs and only transcode/remux when needed. - Adds ~1-3s probe delay on first play. +
+ Hardware Encoder + Select which GPU encoder to use for transcoding. Auto will use the best + available.
+
+
+
+ Max Resolution + Limit transcoded output resolution. Lower = faster, more compatible. +
+ +
+
+
+ Quality Preset + Higher quality uses more bandwidth and CPU/GPU. +
+ +
+
+ +
+

Stream Processing

@@ -509,7 +564,18 @@
+
+ Auto Transcode (Smart) + Automatically detect stream codecs and only transcode/remux when needed. + Adds ~1-3s probe delay on first play. +
+
+
+
@@ -520,13 +586,13 @@
Force Remux - Remux streams to MP4 container. Enable for raw .ts streams - from IPTV middleware like m3u-editor or dispatcharr. + Remux streams to MP4 container. Enable for raw .ts streams from IPTV + middleware like m3u-editor or dispatcharr.
@@ -535,46 +601,37 @@ Container format for Xtream streams. Try TS if you experience buffering issues.
-
+
+ +
+

HTTP Settings

User-Agent HTTP User-Agent for stream requests. Try VLC if your provider blocks Chrome.
-
- - -
-

Keyboard Shortcuts

-
-
↑ / ↓ Channel up/down (or volume)
-
← / → Volume down/up (when arrows = channels)
-
Space / K Play/Pause
-
F Fullscreen
-
M Mute/Unmute
-
I Toggle info overlay
-
PgUp / PgDn Channel up/down
-
-
diff --git a/public/js/pages/Settings.js b/public/js/pages/Settings.js index 084dbe6..9b2e1e1 100644 --- a/public/js/pages/Settings.js +++ b/public/js/pages/Settings.js @@ -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(`
+ ✓ NVIDIA + ${hwInfo.nvidia.name} +
`); } - // Show/hide custom input - const toggleCustomInput = () => { - if (customUaContainer) { - customUaContainer.style.display = userAgentSelect.value === 'custom' ? 'flex' : 'none'; - } - }; - toggleCustomInput(); + if (hwInfo.amf?.available) { + detected.push(`
+ ✓ AMD + ${hwInfo.amf.name || 'Available'} +
`); + } - // Save on change - userAgentSelect.addEventListener('change', () => { - this.app.player.settings.userAgentPreset = userAgentSelect.value; - this.app.player.saveSettings(); - toggleCustomInput(); - }); + if (hwInfo.qsv?.available) { + detected.push(`
+ ✓ Intel QSV + Available +
`); + } - userAgentCustomInput?.addEventListener('change', () => { - this.app.player.settings.userAgentCustom = userAgentCustomInput.value; - this.app.player.saveSettings(); - }); + if (hwInfo.vaapi?.available) { + detected.push(`
+ ✓ VAAPI + ${hwInfo.vaapi.device || 'Available'} +
`); + } + + let html; + if (detected.length > 0) { + html = `
${detected.join('')}
`; + html += `

Recommended encoder: ${hwInfo.recommended}

`; + } else { + html = `

No GPU acceleration detected. Using software encoding.

`; + } + + container.innerHTML = html; + } catch (err) { + console.error('Error loading hardware info:', err); + container.innerHTML = '

Failed to load hardware info

'; } } @@ -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() { diff --git a/server/db.js b/server/db.js index ef38102..1b1bc95 100644 --- a/server/db.js +++ b/server/db.js @@ -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