diff --git a/public/index.html b/public/index.html
index b8abd83..4d9ce0f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -548,6 +548,20 @@
+
+
+ Audio Mix Preset
+ How multi-channel audio (5.1/7.1) is mixed down to stereo for browser
+ playback.
+
+
+
diff --git a/public/js/components/VideoPlayer.js b/public/js/components/VideoPlayer.js
index 9a52a56..b9aac7c 100644
--- a/public/js/components/VideoPlayer.js
+++ b/public/js/components/VideoPlayer.js
@@ -806,7 +806,12 @@ class VideoPlayer {
const statusText = videoMode === 'copy' ? 'Transcoding (Audio)' : 'Transcoding (Video)';
this.updateTranscodeStatus('transcoding', statusText);
- const playlistUrl = await this.startTranscodeSession(streamUrl, { videoMode, videoCodec: info.video });
+ const playlistUrl = await this.startTranscodeSession(streamUrl, {
+ videoMode,
+ videoCodec: info.video,
+ audioCodec: info.audio,
+ audioChannels: info.audioChannels
+ });
this.currentUrl = playlistUrl; // Update currentUrl for HLS reload
this.playHls(playlistUrl);
diff --git a/public/js/pages/Settings.js b/public/js/pages/Settings.js
index f4b4472..a8ff0b2 100644
--- a/public/js/pages/Settings.js
+++ b/public/js/pages/Settings.js
@@ -149,6 +149,16 @@ class SettingsPage {
this.app.player.saveSettings();
});
+ // Audio Mix Preset
+ const audioMixSelect = document.getElementById('setting-audio-mix');
+ if (audioMixSelect) {
+ audioMixSelect.value = this.app.player?.settings?.audioMixPreset || 'auto';
+ audioMixSelect.addEventListener('change', () => {
+ this.app.player.settings.audioMixPreset = audioMixSelect.value;
+ this.app.player.saveSettings();
+ });
+ }
+
// Stream processing toggles
forceProxyToggle?.addEventListener('change', () => {
this.app.player.settings.forceProxy = forceProxyToggle.checked;
diff --git a/public/js/pages/WatchPage.js b/public/js/pages/WatchPage.js
index 2f5087c..630c826 100644
--- a/public/js/pages/WatchPage.js
+++ b/public/js/pages/WatchPage.js
@@ -330,7 +330,12 @@ class WatchPage {
const statusText = videoMode === 'copy' ? 'Transcoding (Audio)' : 'Transcoding (Video)';
this.updateTranscodeStatus('transcoding', statusText);
- const playlistUrl = await this.startTranscodeSession(url, { videoMode, videoCodec: info.video });
+ const playlistUrl = await this.startTranscodeSession(url, {
+ videoMode,
+ videoCodec: info.video,
+ audioCodec: info.audio,
+ audioChannels: info.audioChannels
+ });
this.playHls(playlistUrl);
this.setVolumeFromStorage();
return;
diff --git a/server/db.js b/server/db.js
index 5ad5392..d012ca8 100644
--- a/server/db.js
+++ b/server/db.js
@@ -76,6 +76,7 @@ function getDefaultSettings() {
hwEncoder: 'auto', // auto | nvenc | amf | qsv | vaapi | software
maxResolution: '1080p', // 4k | 1080p | 720p | 480p
quality: 'medium', // high | medium | low
+ audioMixPreset: 'auto', // auto | itu | night | cinematic | passthrough
// Probe cache settings
probeCacheTTL: 300, // 5 minutes for URL probe cache
seriesProbeCacheDays: 7 // 7 days for series episode probe cache
diff --git a/server/routes/probe.js b/server/routes/probe.js
index 367d33d..ac4b874 100644
--- a/server/routes/probe.js
+++ b/server/routes/probe.js
@@ -128,6 +128,7 @@ function analyzeProbeResult(probeResult, url) {
return {
video: videoCodec,
audio: audioCodec,
+ audioChannels: audioStream?.channels || 0, // For Smart Audio Copy
container: container,
compatible: compatible,
needsRemux: needsRemux,
diff --git a/server/routes/transcode.js b/server/routes/transcode.js
index ce79a12..08c736e 100644
--- a/server/routes/transcode.js
+++ b/server/routes/transcode.js
@@ -29,7 +29,7 @@ transcodeSession.startCleanupInterval();
* Body: { url: string, seekOffset?: number }
*/
router.post('/session', async (req, res) => {
- const { url, seekOffset, videoMode, videoCodec } = req.body;
+ const { url, seekOffset, videoMode, videoCodec, audioCodec, audioChannels } = req.body;
if (!url) {
return res.status(400).json({ error: 'URL is required' });
@@ -47,8 +47,11 @@ router.post('/session', async (req, res) => {
hwEncoder: settings.hwEncoder || 'software',
maxResolution: settings.maxResolution || '1080p',
quality: settings.quality || 'medium',
+ audioMixPreset: settings.audioMixPreset || 'auto', // Audio downmix preset
videoMode: videoMode, // 'copy' or 'encode'
- videoCodec: videoCodec // 'h264', 'hevc', etc.
+ videoCodec: videoCodec, // 'h264', 'hevc', etc.
+ audioCodec: audioCodec, // 'aac', 'ac3', etc.
+ audioChannels: audioChannels // number of channels (2=stereo)
});
await session.start();
diff --git a/server/services/transcodeSession.js b/server/services/transcodeSession.js
index 09ece40..c43ef35 100644
--- a/server/services/transcodeSession.js
+++ b/server/services/transcodeSession.js
@@ -222,15 +222,43 @@ class TranscodeSession extends EventEmitter {
this.addVideoEncoderArgs(args, encoder);
}
- // Audio: Transcode to AAC
- args.push(
- '-c:a', 'aac',
- '-ar', '48000',
- '-b:a', '192k',
- // Smart Stereo Downmix (ITU-R BS.775 Standard + LFE):
- // Balanced mix: FL/FR=100%, FC=70% (Unity), Surrounds=70%, LFE=50%
- '-af', 'pan=stereo|FL=FL+0.707*FC+0.707*BL+0.5*LFE|FR=FR+0.707*FC+0.707*BR+0.5*LFE,aresample=async=1'
- );
+ // Audio: Apply mix preset
+ const audioCodec = this.options.audioCodec?.toLowerCase() || 'unknown';
+ const audioChannels = this.options.audioChannels || 0;
+ const audioMixPreset = this.options.audioMixPreset || 'auto';
+ const isStereoAac = audioCodec.includes('aac') && audioChannels === 2;
+
+ // Define pan filter presets for 5.1 -> Stereo downmix
+ const AUDIO_MIX_FILTERS = {
+ // ITU-R BS.775 Standard: Mathematically balanced, transparent
+ itu: 'pan=stereo|FL=FL+0.707*FC+0.707*BL+0.5*LFE|FR=FR+0.707*FC+0.707*BR+0.5*LFE',
+ // Night Mode: Heavy dialogue boost, reduced bass/surrounds for quiet viewing
+ night: 'pan=stereo|FL=0.5*FL+1.2*FC+0.3*BL+0.1*LFE|FR=0.5*FR+1.2*FC+0.3*BR+0.1*LFE',
+ // Cinematic: Wide soundstage, immersive (original "dialogue boost" mix)
+ cinematic: 'pan=stereo|FL=FC+0.80*FL+0.60*BL+0.5*LFE|FR=FC+0.80*FR+0.60*BR+0.5*LFE'
+ };
+
+ if (audioMixPreset === 'passthrough') {
+ // Passthrough: Always copy audio, no processing
+ console.log(`[TranscodeSession ${this.id}] Audio: Passthrough (copy)`);
+ args.push('-c:a', 'copy');
+ } else if (audioMixPreset === 'auto' && isStereoAac) {
+ // Auto + Stereo AAC source: Smart copy
+ console.log(`[TranscodeSession ${this.id}] Audio: Auto (Smart Copy) - Source is Stereo AAC`);
+ args.push('-c:a', 'copy');
+ } else {
+ // Transcode to AAC with selected mix preset (default to ITU for 'auto')
+ const mixPreset = (audioMixPreset === 'auto') ? 'itu' : audioMixPreset;
+ const panFilter = AUDIO_MIX_FILTERS[mixPreset] || AUDIO_MIX_FILTERS.itu;
+
+ console.log(`[TranscodeSession ${this.id}] Audio: ${mixPreset.toUpperCase()} mix (${audioCodec} ${audioChannels}ch -> Stereo AAC)`);
+ args.push(
+ '-c:a', 'aac',
+ '-ar', '48000',
+ '-b:a', '192k',
+ '-af', `${panFilter},aresample=async=1`
+ );
+ }
// HLS output options
args.push(