diff --git a/app/i18n/en-US/settings.json b/app/i18n/en-US/settings.json index 1189b2913dba..a5836ea08cb8 100644 --- a/app/i18n/en-US/settings.json +++ b/app/i18n/en-US/settings.json @@ -226,6 +226,7 @@ "Rate Control": "Rate Control", "Enforce streaming service encoder settings": "Enforce streaming service encoder settings", "Twitch VOD": "Twitch VOD", + "Twitch VOD Track (Uses Track 2)": "Twitch VOD Track (Uses Track 2)", "Use authentication": "Use authentication", "Username": "Username", "Password": "Password", diff --git a/app/services/audio/audio.ts b/app/services/audio/audio.ts index b55342252604..d579bf50ea61 100644 --- a/app/services/audio/audio.ts +++ b/app/services/audio/audio.ts @@ -246,10 +246,16 @@ export class AudioService extends StatefulService { }); } - setSimpleTracks() { - this.views - .getSources() - .forEach(audioSource => this.setSettings(audioSource.sourceId, { audioMixers: 1 })); + setSimpleTracks(includeVodTrack = false) { + this.views.getSources().forEach(audioSource => + this.setSettings(audioSource.sourceId, { + // Initialize the VOD mix from the live mix when it is first enabled. + // Retain other authored track flags, including Advanced track 6. + audioMixers: includeVodTrack + ? audioSource.audioMixers | ((audioSource.audioMixers & 1) << 1) + : 1, + }), + ); } setSettings(sourceId: string, patch: Partial) { diff --git a/app/services/settings/settings.ts b/app/services/settings/settings.ts index ab67a81f279e..bc699e5f1f7b 100644 --- a/app/services/settings/settings.ts +++ b/app/services/settings/settings.ts @@ -710,6 +710,7 @@ export class SettingsService extends StatefulService { if (categoryName === 'Audio') this.setAudioSettings([settingsData.pop()]); if (categoryName === 'Video' && forceApplyCategory && forceApplyCategory !== 'Video') return; + const wasSimpleVodEnabled = this.views.isSimpleOutputMode && this.views.vodTrackEnabled; const dataToSave = []; for (const subGroup of settingsData) { @@ -719,20 +720,16 @@ export class SettingsService extends StatefulService { valueToCurrentValue: true, }), }); - - if ( - categoryName === 'Output' && - subGroup.nameSubCategory === 'Untitled' && - subGroup.parameters[0].value === 'Simple' - ) { - this.audioService.setSimpleTracks(); - } } obs.NodeObs.OBS_settings_saveSettings(categoryName, dataToSave); this.isSaving = true; try { this.loadSettingsIntoStore(); + if (categoryName === 'Output' && this.views.isSimpleOutputMode) { + const vodEnabled = !!this.views.vodTrackEnabled; + if (!vodEnabled || !wasSimpleVodEnabled) this.audioService.setSimpleTracks(vodEnabled); + } } finally { this.isSaving = false; } diff --git a/scripts/repositories.json b/scripts/repositories.json index 6ec6f29831c8..049b071ad3cd 100644 --- a/scripts/repositories.json +++ b/scripts/repositories.json @@ -4,7 +4,7 @@ "name": "obs-studio-node", "url": "https://s3-us-west-2.amazonaws.com/obsstudionodes3.streamlabs.com/", "archive": "osn-[VERSION]-release-[OS][ARCH].tar.gz", - "version": "0.27.19", + "version": "0.27.20", "win64": true, "osx": true }, diff --git a/test/regular/simple-vod-audio-settings.ts b/test/regular/simple-vod-audio-settings.ts new file mode 100644 index 000000000000..ac5a318823ba --- /dev/null +++ b/test/regular/simple-vod-audio-settings.ts @@ -0,0 +1,77 @@ +import { test, useWebdriver } from '../helpers/webdriver'; +import { focusWindow } from '../helpers/modules/core'; + +// eslint-disable-next-line react-hooks/rules-of-hooks +useWebdriver(); + +test('Simple VOD initializes track 2 once and preserves source exclusions on later settings saves', async t => { + t.true(await focusWindow('worker')); + const result = (await t.context.app.client.execute(() => { + const get = (name: string) => (window as any).servicesManager.getResource(name); + const osn = require('obs-studio-node'); + const originalSave = osn.NodeObs.OBS_settings_saveSettings; + const settings = Object.create(get('SettingsService')); + const audio = Object.create(get('AudioService')); + const sources = [ + { sourceId: 'live-and-track-6', audioMixers: 33 }, + { sourceId: 'track-6-only', audioMixers: 32 }, + { sourceId: 'live-only', audioMixers: 1 }, + ]; + let views = { isSimpleOutputMode: true, vodTrackEnabled: false }; + let nextViews = views; + const calls: boolean[] = []; + Object.defineProperty(audio, 'views', { value: { getSources: () => sources } }); + audio.setSettings = (sourceId: string, patch: { audioMixers: number }) => + Object.assign( + sources.find(source => source.sourceId === sourceId), + patch, + ); + Object.defineProperty(settings, 'views', { get: () => views }); + Object.defineProperty(settings, 'audioService', { + value: { + setSimpleTracks: (includeVod: boolean) => { + calls.push(!!includeVod); + audio.setSimpleTracks(includeVod); + }, + }, + }); + settings.loadSettingsIntoStore = () => (views = nextViews); + const form = [{ nameSubCategory: 'Untitled', parameters: [{ name: 'Mode', value: 'Simple' }] }]; + try { + osn.NodeObs.OBS_settings_saveSettings = () => {}; + // Reloaded metadata is authoritative, including a saved VOD flag that was + // hidden while UseAdvanced was disabled in the submitted form. + nextViews = { isSimpleOutputMode: true, vodTrackEnabled: true }; + settings.setSettings('Output', form); + const initialized = sources.map(source => source.audioMixers); + sources[0].audioMixers &= ~2; + settings.setSettings('Output', form); + const afterBitrateSave = sources.map(source => source.audioMixers); + views = nextViews = { isSimpleOutputMode: false, vodTrackEnabled: true }; + settings.setSettings('Output', []); + const afterAdvancedSave = sources.map(source => source.audioMixers); + nextViews = { isSimpleOutputMode: true, vodTrackEnabled: false }; + settings.setSettings('Output', form); + return { + initialized, + afterBitrateSave, + afterAdvancedSave, + afterDisabled: sources.map(source => source.audioMixers), + calls, + }; + } finally { + osn.NodeObs.OBS_settings_saveSettings = originalSave; + } + })) as { + initialized: number[]; + afterBitrateSave: number[]; + afterAdvancedSave: number[]; + afterDisabled: number[]; + calls: boolean[]; + }; + t.deepEqual(result.initialized, [35, 32, 3]); + t.deepEqual(result.afterBitrateSave, [33, 32, 3]); + t.deepEqual(result.afterAdvancedSave, [33, 32, 3]); + t.deepEqual(result.afterDisabled, [1, 1, 1]); + t.deepEqual(result.calls, [true, false]); +});