Skip to content

Commit d58a781

Browse files
authored
fix(youtube): extra outputs can persist when disabled (#5395)
* fix(youtube): extra outputs can persist when disabled Refactor all the handling of `extraOutputs` to live mostly in the Dual Output service as single source of truth. Should fix: * Multiple streams going live when disabling YouTube while it was previously set to "Both" outputs. * Double horizontal/vertical validation in Go Live window. * Display should reset back to horizontal when disabling a platform. * fix(youtube): visual bug when display is not set * fix(diag): extra output platforms
1 parent 6073f97 commit d58a781

File tree

8 files changed

+134
-22
lines changed

8 files changed

+134
-22
lines changed

app/components-react/shared/DisplaySelector.tsx

+19-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { RadioInput } from './inputs';
44
import { TDisplayType } from 'services/settings-v2';
55
import { TPlatform } from 'services/platforms';
66
import { useGoLiveSettings } from 'components-react/windows/go-live/useGoLiveSettings';
7+
import { useVuex } from 'components-react/hooks';
8+
import { Services } from 'components-react/service-provider';
79

810
interface IDisplaySelectorProps {
911
title: string;
@@ -21,14 +23,21 @@ export default function DisplaySelector(p: IDisplaySelectorProps) {
2123
updatePlatform,
2224
isPrime,
2325
enabledPlatforms,
26+
updateShouldUseExtraOutput,
2427
} = useGoLiveSettings();
2528

29+
// TODO: find a way to integrate into goLiveSettings that's reactive (currently not working that way)
30+
const { hasExtraOutput } = useVuex(() => ({
31+
hasExtraOutput: Services.DualOutputService.views.hasExtraOutput(p.platform!),
32+
}));
33+
2634
const setting = p.platform ? platforms[p.platform] : customDestinations[p.index];
2735

2836
// If the user has Ultra, add extra output for YT, if not, check that we only have
2937
// a single platform enabled, hopefully YouTube.
3038
// Might need better validation.
31-
const hasExtraOutputs = p.platform === 'youtube' && (isPrime || enabledPlatforms.length === 1);
39+
const supportsExtraOutputs =
40+
p.platform === 'youtube' && (isPrime || enabledPlatforms.length === 1);
3241

3342
const displays = [
3443
{
@@ -41,7 +50,7 @@ export default function DisplaySelector(p: IDisplaySelectorProps) {
4150
},
4251
];
4352

44-
if (hasExtraOutputs) {
53+
if (supportsExtraOutputs) {
4554
// TODO: TS doesn't infer types on filter(id) so we're mutating array here
4655
displays.push({
4756
label: $t('Both'),
@@ -54,18 +63,20 @@ export default function DisplaySelector(p: IDisplaySelectorProps) {
5463
if (p.platform) {
5564
const display: TDisplayType =
5665
// Use horizontal display, vertical stream will be created separately
57-
hasExtraOutputs && val === 'both' ? 'horizontal' : (val as TDisplayType);
58-
updatePlatform(p.platform, { display, hasExtraOutputs: val === 'both' });
66+
supportsExtraOutputs && val === 'both' ? 'horizontal' : (val as TDisplayType);
67+
updatePlatform(p.platform, { display });
68+
69+
// Add or remove the platform from the Dual Output's extra output platforms list
70+
updateShouldUseExtraOutput(p.platform, val);
5971
} else {
6072
updateCustomDestinationDisplay(p.index, val as TDisplayType);
6173
}
6274
};
6375

6476
// TODO: Fake accessor, improve, if nothing else, fix type
65-
const value =
66-
setting?.display === 'horizontal' && (setting as any)?.hasExtraOutputs
67-
? 'both'
68-
: setting?.display;
77+
// display can be undefined on first window load
78+
const isDefaultDisplay = setting?.display === 'horizontal' || setting?.display === undefined;
79+
const value = isDefaultDisplay && hasExtraOutput ? 'both' : setting?.display;
6980

7081
return (
7182
<RadioInput

app/components-react/windows/go-live/useGoLiveSettings.ts

+48-2
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,44 @@ class GoLiveSettingsState extends StreamInfoView<IGoLiveSettingsState> {
6262
Object.assign(this.state, { ...newSettings, platforms, customDestinations });
6363
}
6464
/**
65-
* Update settings for a specific platforms
65+
* Update settings for a specific platform
6666
*/
6767
updatePlatform(platform: TPlatform, patch: Partial<IGoLiveSettings['platforms'][TPlatform]>) {
68+
// TODO: find or create an observer for platform enabling/disabling behavior
69+
const isDisablingPlatform =
70+
Object.prototype.hasOwnProperty.call(patch, 'enabled') && patch?.enabled === false;
71+
72+
const hasExtraOutputs = Services.DualOutputService.views.hasExtraOutput(platform);
73+
6874
const updated = {
6975
platforms: {
7076
...this.state.platforms,
71-
[platform]: { ...this.state.platforms[platform], ...patch },
77+
[platform]: {
78+
...this.state.platforms[platform],
79+
...this.updateDisplayIfNeeded(patch, isDisablingPlatform, hasExtraOutputs),
80+
},
7281
},
7382
};
7483
this.updateSettings(updated);
84+
85+
/*
86+
* Reset display and extra outputs when disabling a platform, go live checks aren't enough.
87+
* When disabling a platform, the extra output state remains true since its display
88+
* `onChange` selector isn't triggered.
89+
* Coupled with some bugs we've seen with go live settings persistence, this
90+
* is the most practical place we've found to handle.
91+
*/
92+
if (isDisablingPlatform) {
93+
Services.DualOutputService.actions.removeExtraOutputPlatform(platform);
94+
}
95+
}
96+
97+
private updateDisplayIfNeeded(
98+
patch: Partial<IGoLiveSettings['platforms'][TPlatform]>,
99+
isDisablingPlatform: boolean,
100+
hasExtraOutputs: boolean,
101+
) {
102+
return isDisablingPlatform && hasExtraOutputs ? { ...patch, display: 'horizontal' } : patch;
75103
}
76104

77105
switchPlatforms(enabledPlatforms: TPlatform[]) {
@@ -398,6 +426,24 @@ export class GoLiveSettingsModule {
398426
get recommendedColorSpaceWarnings() {
399427
return Services.SettingsService.views.recommendedColorSpaceWarnings;
400428
}
429+
430+
/**
431+
* Add or remove a platform from Dual Output's extra output list
432+
* according to display.
433+
* If display is set to `both` it would add it, otherwise would remove it
434+
* from the list if present.
435+
*/
436+
updateShouldUseExtraOutput(platform: TPlatform, display: TDisplayType | 'both') {
437+
if (display === 'both') {
438+
Services.DualOutputService.actions.return.addExtraOutputPlatform(platform);
439+
} else {
440+
Services.DualOutputService.actions.return.removeExtraOutputPlatform(platform);
441+
}
442+
}
443+
444+
hasExtraOutput(platform: TPlatform) {
445+
return Services.DualOutputService.views.hasExtraOutput(platform);
446+
}
401447
}
402448

403449
export function useGoLiveSettings() {

app/services/diagnostics.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1062,9 +1062,7 @@ export class DiagnosticsService extends PersistentStatefulService<IDiagnosticsSe
10621062
* don't feel too happy about hacking it
10631063
*/
10641064
const streamingPlatforms = (this.streamingService as any)?.views?.settings?.platforms || [];
1065-
const platformsUsingExtraOutputs = Object.keys(streamingPlatforms).filter(p => {
1066-
return streamingPlatforms[p].hasExtraOutputs;
1067-
});
1065+
const platformsUsingExtraOutputs = this.dualOutputService.views.extraOutputPlatforms;
10681066

10691067
return new Section('Dual Output', {
10701068
'Dual Output Active': this.dualOutputService.views.dualOutputMode,

app/services/dual-output/dual-output.ts

+57
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ interface IDualOutputServiceState {
3838
dualOutputMode: boolean;
3939
videoSettings: IDisplayVideoSettings;
4040
isLoading: boolean;
41+
// TODO: use Set
42+
extraOutputPlatforms: TPlatform[];
4143
}
4244

4345
enum EOutputDisplayType {
@@ -278,6 +280,27 @@ class DualOutputViews extends ViewHandler<IDualOutputServiceState> {
278280
const nodeMap = sceneId ? this.sceneNodeMaps[sceneId] : this.activeSceneNodeMap;
279281
return !!nodeMap && Object.keys(nodeMap).length > 0;
280282
}
283+
284+
/**
285+
* List of platforms that use extra outputs via the new streaming API
286+
*/
287+
get extraOutputPlatforms() {
288+
return this.state.extraOutputPlatforms;
289+
}
290+
291+
/**
292+
* Check if a given platform is using extra outputs with the new streaming API
293+
*/
294+
hasExtraOutput(platform: TPlatform) {
295+
return this.state.extraOutputPlatforms.includes(platform);
296+
}
297+
298+
/**
299+
* Check if there are any platforms using extra outputs
300+
*/
301+
get hasExtraOutputs() {
302+
return !!this.state.extraOutputPlatforms.length;
303+
}
281304
}
282305

283306
@InitAfter('ScenesService')
@@ -303,6 +326,8 @@ export class DualOutputService extends PersistentStatefulService<IDualOutputServ
303326
},
304327
},
305328
isLoading: false,
329+
// TODO: I would like to use `Set` but seem to be having issues with it
330+
extraOutputPlatforms: [] as TPlatform[],
306331
};
307332

308333
sceneNodeHandled = new Subject<number>();
@@ -820,6 +845,22 @@ export class DualOutputService extends PersistentStatefulService<IDualOutputServ
820845
this.SET_IS_LOADING(status);
821846
}
822847

848+
/**
849+
* Add a platform to the list of platforms that use extra outputs
850+
* (in addition to horizontal/vertical).
851+
*/
852+
addExtraOutputPlatform(platform: TPlatform) {
853+
this.ADD_EXTRA_OUTPUT_PLATFORM(platform);
854+
}
855+
856+
/**
857+
* Remove a platform from the list of platforms that use extra outputs
858+
* (in addition to horizontal/vertical).
859+
*/
860+
removeExtraOutputPlatform(platform: TPlatform) {
861+
this.REMOVE_EXTRA_OUTPUT_PLATFORM(platform);
862+
}
863+
823864
@mutation()
824865
private SET_SHOW_DUAL_OUTPUT(status?: boolean) {
825866
this.state = {
@@ -853,4 +894,20 @@ export class DualOutputService extends PersistentStatefulService<IDualOutputServ
853894
private SET_IS_LOADING(status: boolean) {
854895
this.state = { ...this.state, isLoading: status };
855896
}
897+
898+
@mutation()
899+
private ADD_EXTRA_OUTPUT_PLATFORM(platform: TPlatform) {
900+
// TODO: this is more complex that it needs to be without using Sets
901+
if (!this.state.extraOutputPlatforms.includes(platform)) {
902+
this.state.extraOutputPlatforms = [...this.state.extraOutputPlatforms, platform];
903+
}
904+
}
905+
906+
@mutation()
907+
private REMOVE_EXTRA_OUTPUT_PLATFORM(platform: TPlatform) {
908+
// TODO: this is more complex that it needs to be without using Sets
909+
if (this.state.extraOutputPlatforms.includes(platform)) {
910+
this.state.extraOutputPlatforms = this.state.extraOutputPlatforms.filter(p => p !== platform);
911+
}
912+
}
856913
}

app/services/platforms/youtube.ts

-2
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ export interface IYoutubeStartStreamOptions extends IExtraBroadcastSettings {
4545
privacyStatus?: 'private' | 'public' | 'unlisted';
4646
scheduledStartTime?: number;
4747
mode?: TOutputOrientation;
48-
/** Use extra output to stream a vertical context to a separate broadcast */
49-
hasExtraOutputs?: boolean;
5048
}
5149

5250
/**

app/services/settings/streaming/stream-settings.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export class StreamSettingsService extends PersistentStatefulService<IStreamSett
146146
if (
147147
this.streamingService.views.enabledPlatforms.length > 1 &&
148148
ytSettings?.enabled &&
149-
ytSettings.hasExtraOutputs
149+
this.dualOutputService.views.hasExtraOutput('youtube')
150150
) {
151151
return 'StreamSecond';
152152
}

app/services/streaming/streaming-view.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,7 @@ export class StreamInfoView<T extends Object> extends ViewHandler<T> {
244244
const platforms = settings?.platforms || this.settings.platforms;
245245
// If any platform is configured as "Both" for outputs we technically should satisfy
246246
// this requirement and ignore the warning
247-
// TODO: fix types
248-
if (Object.values(platforms).some(platform => (platform as any).hasExtraOutputs)) {
247+
if (this.dualOutputView.hasExtraOutputs) {
249248
return true;
250249
}
251250

app/services/streaming/streaming.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -435,10 +435,9 @@ export class StreamingService
435435
if (
436436
this.views.isDualOutputMode &&
437437
this.views.enabledPlatforms.includes('youtube') &&
438-
this.views.getPlatformSettings('youtube')?.hasExtraOutputs &&
438+
this.dualOutputService.views.hasExtraOutput('youtube') &&
439439
/*
440440
* Super safe so we don't ever bypass validation due to the toggles
441-
* or `hasExtraOutputs` not being reset.
442441
* TODO: might not cover free TikTok, but probably by design
443442
*/
444443
(this.userService.views.isPrime || this.views.enabledPlatforms.length === 1)
@@ -697,7 +696,10 @@ export class StreamingService
697696
* it for tracking, in the future, we'd rather track extraOutputs from
698697
* StreamingService themselves
699698
*/
700-
if (settings.platforms.youtube?.enabled && settings.platforms.youtube?.hasExtraOutputs) {
699+
if (
700+
settings.platforms.youtube?.enabled &&
701+
this.dualOutputService.views.hasExtraOutput('youtube')
702+
) {
701703
this.usageStatisticsService.recordFeatureUsage('StreamToYouTubeBothOutputs');
702704
}
703705
}
@@ -1000,10 +1002,11 @@ export class StreamingService
10001002

10011003
NodeObs.OBS_service_setVideoInfo(horizontalContext, 'horizontal');
10021004
const ytSettings = this.views.getPlatformSettings('youtube');
1005+
// TODO: this can probably be more generic now
10031006
if (
10041007
this.views.enabledPlatforms.length > 1 &&
10051008
ytSettings?.enabled &&
1006-
ytSettings.hasExtraOutputs
1009+
this.dualOutputService.views.hasExtraOutput('youtube')
10071010
) {
10081011
NodeObs.OBS_service_setVideoInfo(horizontalContext, 'vertical');
10091012
} else {

0 commit comments

Comments
 (0)