Skip to content

Commit 92c837d

Browse files
authored
Fix LOE bugs. (#6176)
* Fix dual stream display coercion and enhanced broadcasting disabling. * Fix title persistence. * Remove redundant if
1 parent 9ad16c7 commit 92c837d

5 files changed

Lines changed: 104 additions & 68 deletions

File tree

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,13 @@ export class GoLiveSettingsModule {
336336
/**
337337
* Fetch settings for each platform
338338
*/
339-
async prepopulate() {
339+
async prepopulate(options?: { preserveCommonFields?: boolean }) {
340340
const { StreamingService, RestreamService, DualOutputService } = Services;
341341
const { isMultiplatformMode } = StreamingService.views;
342342

343+
// Snapshot the common fields `updateSettings` below replaces every platform's settings
344+
const editedCommonFields = options?.preserveCommonFields ? this.state.commonFields : undefined;
345+
343346
this.state.setNeedPrepopulate(true);
344347
await StreamingService.actions.return.prepopulateInfo();
345348
// TODO investigate mutation order issue
@@ -399,6 +402,15 @@ export class GoLiveSettingsModule {
399402

400403
this.state.updateSettings(settings);
401404

405+
// Prepopulating rebuilds each platform's settings from the service, which drops a title or
406+
// description the user has typed but not submitted. Put the typed values back.
407+
if (editedCommonFields) {
408+
this.state.updateCommonFields({
409+
title: editedCommonFields.title || this.state.commonFields.title,
410+
description: editedCommonFields.description || this.state.commonFields.description,
411+
});
412+
}
413+
402414
/* If the user was in dual output before but doesn't have restream
403415
* we should disable one of the platforms if they have two enabled
404416
*/
@@ -500,7 +512,10 @@ export class GoLiveSettingsModule {
500512
}
501513

502514
this.save(this.state.settings);
503-
this.prepopulate();
515+
516+
// Keep whatever the user has typed into the shared title/description. Every other caller of
517+
// `prepopulate` is a window opening, where the fetched values should win instead.
518+
this.prepopulate({ preserveCommonFields: true });
504519
}
505520

506521
switchCustomDestination(destInd: number, enabled: boolean) {

app/services/platforms/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ export interface IPlatformService {
215215

216216
setupStreamShiftStream?: (options: IGoLiveSettings) => Promise<void>;
217217

218+
setupLiveOutputStream?: (options: IGoLiveSettings) => Promise<void>;
219+
218220
postNotification?: (message: string) => void;
219221

220222
formatError?: (e: any, platform: TPlatform, errorType?: TStreamErrorType) => never;

app/services/platforms/twitch.ts

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,27 @@ export class TwitchService
239239
return;
240240
}
241241

242+
const channelInfo = goLiveSettings?.platforms.twitch;
243+
244+
// Resolve enhanced broadcasting before the stream key is written below. The key is only sent
245+
// to the display's OBS context when Twitch is not going out through restream, and that
246+
// depends on whether this stream is an enhanced broadcast — deciding afterwards means the
247+
// check answers for the previous stream instead of this one.
248+
if (channelInfo) {
249+
if (this.streamingService.views.isLiveOutputEditingEnabled) {
250+
await this.setupLiveOutputStream(goLiveSettings);
251+
} else if (channelInfo.display === 'both') {
252+
await this.setupDualStream(goLiveSettings);
253+
} else {
254+
// Update enhanced broadcasting setting based on go live settings
255+
this.settingsService.setEnhancedBroadcasting(channelInfo.isEnhancedBroadcasting);
256+
}
257+
} else if (this.streamingService.views.isTwitchDualStreamEnabled) {
258+
// Failsafe to guarantee that enhanced broadcasting is enabled if dual streaming is active
259+
260+
await this.setupDualStream(goLiveSettings);
261+
}
262+
242263
if (
243264
this.streamSettingsService.protectedModeEnabled &&
244265
this.streamSettingsService.isSafeToModifyStreamKey()
@@ -265,34 +286,8 @@ export class TwitchService
265286
}
266287
}
267288

268-
if (goLiveSettings) {
269-
const channelInfo = goLiveSettings?.platforms.twitch;
270-
271-
if (channelInfo) {
272-
if (channelInfo?.display === 'both') {
273-
try {
274-
await this.setupDualStream(goLiveSettings);
275-
} catch (e: unknown) {
276-
console.error('Error setting up dual stream:', e);
277-
}
278-
} else if (this.streamingService.views.isLiveOutputEditingEnabled) {
279-
// When live output editing is enabled enhanced broadcasting won't work because it
280-
// uses restream, which is incompatible with enhanced broadcasting.
281-
this.settingsService.setEnhancedBroadcasting(false);
282-
} else {
283-
// Update enhanced broadcasting setting based on go live settings
284-
this.settingsService.setEnhancedBroadcasting(channelInfo.isEnhancedBroadcasting);
285-
}
286-
287-
await this.putChannelInfo(channelInfo);
288-
}
289-
} else if (this.streamingService.views.isTwitchDualStreamEnabled) {
290-
// Failsafe to guarantee that enhanced broadcasting is enabled if dual streaming is active
291-
try {
292-
await this.setupDualStream(goLiveSettings);
293-
} catch (e: unknown) {
294-
console.error('Error setting up dual stream:', e);
295-
}
289+
if (channelInfo) {
290+
await this.putChannelInfo(channelInfo);
296291
}
297292

298293
this.setPlatformContext('twitch');
@@ -456,6 +451,9 @@ export class TwitchService
456451
return;
457452
}
458453

454+
// Stream shift not compatible with enhanced broadcasting
455+
this.settingsService.setEnhancedBroadcasting(false);
456+
459457
const [channelInfo] = await Promise.all([
460458
this.requestTwitch<{
461459
data: {
@@ -467,7 +465,7 @@ export class TwitchService
467465
}[];
468466
}>(`${this.apiBase}/helix/channels?broadcaster_id=${this.twitchId}`).then(json => {
469467
return {
470-
title: settings?.stream_title ?? json.data[0].title,
468+
title: json.data[0].title,
471469
game: json.data[0].game_name,
472470
gameId: json.data[0].game_id,
473471
gameName: json.data[0].game_name,
@@ -481,7 +479,22 @@ export class TwitchService
481479
]);
482480

483481
const title = settings?.stream_title ?? channelInfo.title;
484-
const game = settings?.game_id ?? channelInfo.game;
482+
483+
// Stream Shift reports the category as an id, but `game` and `gameName` hold the category
484+
// *name* everywhere else in this service — the Go Live form renders `game` directly. Resolve
485+
// the id to a name so a shifted stream doesn't show a bare number as its category.
486+
let game = channelInfo.game;
487+
let gameId = channelInfo.gameId;
488+
489+
if (settings?.game_id) {
490+
gameId = settings.game_id;
491+
try {
492+
game = (await this.fetchGame(settings.game_id)).name;
493+
} catch (e: unknown) {
494+
console.error('Stream Shift: could not resolve game name for id', settings.game_id, e);
495+
game = channelInfo.game;
496+
}
497+
}
485498

486499
const tags: string[] = this.twitchTagsService.views.hasTags
487500
? this.twitchTagsService.views.tags
@@ -491,16 +504,23 @@ export class TwitchService
491504
tags,
492505
title,
493506
game,
494-
gameId: channelInfo.gameId,
495-
gameName: channelInfo.gameName,
507+
gameId,
508+
gameName: game,
496509
isBrandedContent: channelInfo.is_branded_content,
497-
isEnhancedBroadcasting: this.settingsService.isEnhancedBroadcasting(),
510+
// The user's persisted preference, not the OBS runtime flag. Stream shift already forced the OBS flag off,
511+
// so reading the OBS runtime flag here would overwrite the preference with `false` every time a stream is shifted.
512+
isEnhancedBroadcasting: this.state.settings.isEnhancedBroadcasting,
498513
contentClassificationLabels: channelInfo.content_classification_labels,
499514
});
500515

501516
this.setPlatformContext('twitch');
502517
}
503518

519+
async setupLiveOutputStream(options?: IGoLiveSettings): Promise<void> {
520+
// Live output editing not compatible with enhanced broadcasting, so disable it here
521+
this.settingsService.setEnhancedBroadcasting(false);
522+
}
523+
504524
fetchFollowers(): Promise<number> {
505525
return this.requestTwitch<{ total: number }>({
506526
url: `${this.apiBase}/helix/users/follows?to_id=${this.twitchId}`,

app/services/streaming/streaming-view.ts

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -582,26 +582,6 @@ export class StreamInfoView<T extends Object> extends ViewHandler<T> {
582582
);
583583
}
584584

585-
/**
586-
* Validate the display when live output editing is enabled
587-
* @remark Used to ensure a platform with the `both` display, used for dual streaming, uses the
588-
* default display instead. Reads `savedLiveOutputEditing` instead of `isLiveOutputEditingEnabled`
589-
* to avoid the circular dependency: settings → savedSettings → getSavedPlatformSettings → settings
590-
* @param display - The display saved for the platform
591-
* @remark Use the dual output mode service state to prevent circular references
592-
* @warning The `get` prefix is required. This class is passed to `injectState` in
593-
* `useGoLiveSettings`, and slap registers any method not named `get*`/`is*`/`should*` as a
594-
* mutation. Calling a mutation from a getter dispatches it during the component snapshot,
595-
* which re-enters `updateUI` and recurses until the stack overflows.
596-
*/
597-
private getValidatedDisplay(display?: TDisplayOutput): TDisplayType {
598-
if (!display || display === 'both' || !this.dualOutputView.dualOutputMode) {
599-
return 'horizontal';
600-
}
601-
602-
return display as TDisplayType;
603-
}
604-
605585
get shouldSetupDualOutput(): boolean {
606586
if (this.dualOutputView.dualOutputMode) return true;
607587
// Read from state to avoid circular dependency:
@@ -618,10 +598,10 @@ export class StreamInfoView<T extends Object> extends ViewHandler<T> {
618598
const p = platforms[platform as TPlatform];
619599
if (!p?.enabled || !this.isPlatformLinked(platform as TPlatform)) continue;
620600

621-
// Note: this is to prevent an error where the platform doesn't go live because the display is set to 'both'
622-
// in dual output mode when live output editing is enabled. It should never happen but to prevent errors indexing
623-
// `platformDisplays`, default a platform without a display to horizontal
624-
const display = this.getValidatedDisplay(p.display);
601+
const display = p.display ?? 'horizontal';
602+
603+
// Any enabled platform with 'both' display automatically enables dual output mode
604+
if (display === 'both') return true;
625605

626606
platformDisplays[display].push(platform as TPlatform);
627607
}
@@ -657,13 +637,19 @@ export class StreamInfoView<T extends Object> extends ViewHandler<T> {
657637
);
658638
}
659639

640+
// TODO: cleanup — dead code, no callers. Diagnostics uses the identically named
641+
// `OutputSettingsService.getIsEnhancedBroadcasting`, not this one. Delete it.
660642
getIsEnhancedBroadcasting(): boolean {
661643
return Services.SettingsService.isEnhancedBroadcasting();
662644
}
663645

664646
/**
665647
* Check for multistreaming with Twitch enhanced broadcasting
666648
*/
649+
// TODO: cleanup — this is a method rather than a getter, so it is unmemoized, and every call
650+
// reaches native OBS through `SettingsService.isEnhancedBroadcasting()`. It runs on each go
651+
// live from both `twitch.beforeGoLive` and `createEnhancedBroadcastDualOutput`. Convert to a
652+
// getter, or read the per-stream `StreamingService.state.enhancedBroadcasting` decision.
667653
isEnhancedBroadcastingMultistream(): boolean {
668654
// Enhanced broadcasting is not available while live output editing is enabled because it uses
669655
// its own video context and stream, which cannot be edited mid-stream
@@ -1021,12 +1007,17 @@ export class StreamInfoView<T extends Object> extends ViewHandler<T> {
10211007
settings['liveVideoId'] = '';
10221008
}
10231009

1024-
// Make sure platforms assigned to the vertical display in dual output mode still go live in single output mode
1025-
// Note: This is a check to ensure that the display is valid when live output editing is enabled. If the display
1026-
// is set to 'both', it will be defaulted to 'horizontal' for single output mode.
1010+
// make sure platforms assigned to the vertical display in dual output mode still go live in
1011+
// single output mode
1012+
// Note: `both` is deliberately passed through. It must not be collapsed here, because this
1013+
// value seeds the Go Live window and is written straight back by `save()`, so coercing it
1014+
// would overwrite the user's saved dual stream choice. Live output editing's inability to
1015+
// dual stream is enforced where the display is used, not where it is stored.
1016+
// The `?? 'horizontal'` matters: without it a platform with no saved display yields
1017+
// `undefined` here, and callers that index by display rather than defaulting it break.
10271018
const display =
1028-
this.isDualOutputMode && savedDestinations && savedDestinations[platform]?.display
1029-
? this.getValidatedDisplay(savedDestinations[platform]?.display)
1019+
this.isDualOutputMode && savedDestinations
1020+
? savedDestinations[platform]?.display ?? 'horizontal'
10301021
: 'horizontal';
10311022

10321023
return {

app/services/streaming/streaming.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -851,9 +851,12 @@ export class StreamingService
851851
// in osn is what actually determines if the stream will use enhanced broadcasting.
852852
if (platform === 'twitch') {
853853
// Enhanced broadcasting is unavailable while live output editing is enabled because it
854-
// uses its own video context and stream, which cannot be edited mid-stream
854+
// uses its own video context and stream, which cannot be edited mid-stream.
855+
// It is also unavailable during a stream shift, which always goes out through the
856+
// restream service.
855857
const isEnhancedBroadcasting =
856858
!this.views.isLiveOutputEditingEnabled &&
859+
!this.views.isStreamShiftMode &&
857860
(this.views.isTwitchDualStreamEnabled ||
858861
settings.platforms.twitch?.isEnhancedBroadcasting ||
859862
false);
@@ -2587,9 +2590,14 @@ export class StreamingService
25872590
}
25882591

25892592
private async createEnhancedBroadcastMultistream() {
2590-
const display = this.settingsService.views.values.Stream.server.includes('streamlabs')
2591-
? 'horizontal'
2592-
: 'vertical';
2593+
// The enhanced broadcasting instance carries Twitch, so it has to use the canvas Twitch is
2594+
// assigned to. Outside dual output mode there is only the horizontal canvas.
2595+
// Note: do not infer this from the horizontal display's ingest server. When both displays
2596+
// are being restreamed, the horizontal server is a Streamlabs ingest whether or not Twitch
2597+
// is on that display, so Twitch on the vertical display would be sent landscape.
2598+
const display = this.views.isDualOutputMode
2599+
? this.views.getPlatformDisplayType('twitch')
2600+
: 'horizontal';
25932601

25942602
const outputSettings =
25952603
display === 'horizontal'

0 commit comments

Comments
 (0)