feat: add smart audio copy and audio mix presets for configurable 5.1 downmixing
This commit is contained in:
@@ -548,6 +548,20 @@
|
|||||||
<option value="low">Low (Fastest)</option>
|
<option value="low">Low (Fastest)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info" style="flex: 1;">
|
||||||
|
<span class="setting-label">Audio Mix Preset</span>
|
||||||
|
<span class="setting-hint">How multi-channel audio (5.1/7.1) is mixed down to stereo for browser
|
||||||
|
playback.</span>
|
||||||
|
</div>
|
||||||
|
<select id="setting-audio-mix" class="source-select" style="width: auto; min-width: 200px;">
|
||||||
|
<option value="auto">Auto (Smart Copy/ITU)</option>
|
||||||
|
<option value="itu">ITU-R BS.775 (Balanced)</option>
|
||||||
|
<option value="night">Night Mode (Dialogue Boost)</option>
|
||||||
|
<option value="cinematic">Cinematic (Wide)</option>
|
||||||
|
<option value="passthrough">Passthrough (Copy)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
|
|||||||
@@ -806,7 +806,12 @@ class VideoPlayer {
|
|||||||
const statusText = videoMode === 'copy' ? 'Transcoding (Audio)' : 'Transcoding (Video)';
|
const statusText = videoMode === 'copy' ? 'Transcoding (Audio)' : 'Transcoding (Video)';
|
||||||
|
|
||||||
this.updateTranscodeStatus('transcoding', statusText);
|
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.currentUrl = playlistUrl; // Update currentUrl for HLS reload
|
||||||
|
|
||||||
this.playHls(playlistUrl);
|
this.playHls(playlistUrl);
|
||||||
|
|||||||
@@ -149,6 +149,16 @@ class SettingsPage {
|
|||||||
this.app.player.saveSettings();
|
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
|
// Stream processing toggles
|
||||||
forceProxyToggle?.addEventListener('change', () => {
|
forceProxyToggle?.addEventListener('change', () => {
|
||||||
this.app.player.settings.forceProxy = forceProxyToggle.checked;
|
this.app.player.settings.forceProxy = forceProxyToggle.checked;
|
||||||
|
|||||||
@@ -330,7 +330,12 @@ class WatchPage {
|
|||||||
const statusText = videoMode === 'copy' ? 'Transcoding (Audio)' : 'Transcoding (Video)';
|
const statusText = videoMode === 'copy' ? 'Transcoding (Audio)' : 'Transcoding (Video)';
|
||||||
|
|
||||||
this.updateTranscodeStatus('transcoding', statusText);
|
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.playHls(playlistUrl);
|
||||||
this.setVolumeFromStorage();
|
this.setVolumeFromStorage();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ function getDefaultSettings() {
|
|||||||
hwEncoder: 'auto', // auto | nvenc | amf | qsv | vaapi | software
|
hwEncoder: 'auto', // auto | nvenc | amf | qsv | vaapi | software
|
||||||
maxResolution: '1080p', // 4k | 1080p | 720p | 480p
|
maxResolution: '1080p', // 4k | 1080p | 720p | 480p
|
||||||
quality: 'medium', // high | medium | low
|
quality: 'medium', // high | medium | low
|
||||||
|
audioMixPreset: 'auto', // auto | itu | night | cinematic | passthrough
|
||||||
// Probe cache settings
|
// Probe cache settings
|
||||||
probeCacheTTL: 300, // 5 minutes for URL probe cache
|
probeCacheTTL: 300, // 5 minutes for URL probe cache
|
||||||
seriesProbeCacheDays: 7 // 7 days for series episode probe cache
|
seriesProbeCacheDays: 7 // 7 days for series episode probe cache
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ function analyzeProbeResult(probeResult, url) {
|
|||||||
return {
|
return {
|
||||||
video: videoCodec,
|
video: videoCodec,
|
||||||
audio: audioCodec,
|
audio: audioCodec,
|
||||||
|
audioChannels: audioStream?.channels || 0, // For Smart Audio Copy
|
||||||
container: container,
|
container: container,
|
||||||
compatible: compatible,
|
compatible: compatible,
|
||||||
needsRemux: needsRemux,
|
needsRemux: needsRemux,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ transcodeSession.startCleanupInterval();
|
|||||||
* Body: { url: string, seekOffset?: number }
|
* Body: { url: string, seekOffset?: number }
|
||||||
*/
|
*/
|
||||||
router.post('/session', async (req, res) => {
|
router.post('/session', async (req, res) => {
|
||||||
const { url, seekOffset, videoMode, videoCodec } = req.body;
|
const { url, seekOffset, videoMode, videoCodec, audioCodec, audioChannels } = req.body;
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return res.status(400).json({ error: 'URL is required' });
|
return res.status(400).json({ error: 'URL is required' });
|
||||||
@@ -47,8 +47,11 @@ router.post('/session', async (req, res) => {
|
|||||||
hwEncoder: settings.hwEncoder || 'software',
|
hwEncoder: settings.hwEncoder || 'software',
|
||||||
maxResolution: settings.maxResolution || '1080p',
|
maxResolution: settings.maxResolution || '1080p',
|
||||||
quality: settings.quality || 'medium',
|
quality: settings.quality || 'medium',
|
||||||
|
audioMixPreset: settings.audioMixPreset || 'auto', // Audio downmix preset
|
||||||
videoMode: videoMode, // 'copy' or 'encode'
|
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();
|
await session.start();
|
||||||
|
|||||||
@@ -222,15 +222,43 @@ class TranscodeSession extends EventEmitter {
|
|||||||
this.addVideoEncoderArgs(args, encoder);
|
this.addVideoEncoderArgs(args, encoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio: Transcode to AAC
|
// Audio: Apply mix preset
|
||||||
args.push(
|
const audioCodec = this.options.audioCodec?.toLowerCase() || 'unknown';
|
||||||
'-c:a', 'aac',
|
const audioChannels = this.options.audioChannels || 0;
|
||||||
'-ar', '48000',
|
const audioMixPreset = this.options.audioMixPreset || 'auto';
|
||||||
'-b:a', '192k',
|
const isStereoAac = audioCodec.includes('aac') && audioChannels === 2;
|
||||||
// Smart Stereo Downmix (ITU-R BS.775 Standard + LFE):
|
|
||||||
// Balanced mix: FL/FR=100%, FC=70% (Unity), Surrounds=70%, LFE=50%
|
// Define pan filter presets for 5.1 -> Stereo downmix
|
||||||
'-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'
|
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
|
// HLS output options
|
||||||
args.push(
|
args.push(
|
||||||
|
|||||||
Reference in New Issue
Block a user