Revert Multistream Toggle (2) - #6147
Conversation
This reverts commit eab5de9.
There was a problem hiding this comment.
Pull request overview
This PR reverts the “Multistream Toggle” / live output editing feature set and its related UI/service-layer changes (previously spread across several PRs) to restore the prior Go Live / Edit Stream behavior and reduce conflict with master.
Changes:
- Rolls back live output editing + multistream toggle behaviors across streaming/restream services and Go Live/Edit Stream React UI.
- Reverts error-type granularity and some display/mode handling introduced for live output editing/dual output interactions.
- Updates i18n dictionaries and e2e test skips to match the reverted behavior.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test/regular/filters.ts | Adjusts which filter tests are skipped/enabled. |
| app/services/streaming/streaming.ts | Reverts parts of restream update error handling and some helper logic. |
| app/services/streaming/streaming-view.ts | Reverts dual output/live output editing precedence and common-fields behavior. |
| app/services/streaming/stream-error.ts | Removes several RESTREAM-specific error types and helpers; updates messages. |
| app/services/restream.ts | Reverts restream error typing and target/display routing behavior used during live updates. |
| app/services/platforms/twitch.ts | Removes the live output editing → disable enhanced broadcasting enforcement. |
| app/services/dismissables.ts | Removes Highlighter banner dismissable enum entry; adjusts dismissAll implementation. |
| app/i18n/fallback.ts | Switches fallback dictionary inclusion from live-output-editing.json to live-outputs.json. |
| app/i18n/en-US/twitch.json | Removes the live-output-editing enhanced broadcasting tooltip string. |
| app/i18n/en-US/streaming.json | Adds updated enhanced broadcasting restream error string. |
| app/i18n/en-US/live-outputs.json | Adds new live outputs dictionary (replacing the removed live-output-editing dictionary). |
| app/i18n/en-US/live-output-editing.json | Deletes the previous live output editing dictionary. |
| app/i18n/en-US/highlighter.json | Removes “Do not ask again” string tied to the reverted dismissable behavior. |
| app/components-react/windows/go-live/useGoLiveSettings.ts | Reverts parts of settings coordination for dual output/live editing. |
| app/components-react/windows/go-live/StreamShiftCard.tsx | Deletes the card-based Stream Shift UI component (reverted). |
| app/components-react/windows/go-live/PlatformSettings.tsx | Reworks feature toggle cards and platform binding props to match the reverted model. |
| app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx | Reverts enhanced broadcasting disabling/tooltip behavior for live output editing. |
| app/components-react/windows/go-live/platforms/PlatformSettingsLayout.tsx | Removes isLiveOutputEditingEnabled from platform settings params. |
| app/components-react/windows/go-live/LiveOutputEditingCard.tsx | Deletes the card-based Live Output Editing UI component (reverted). |
| app/components-react/windows/go-live/GoLiveWindow.tsx | Minor comment placement changes around effect dependency intent. |
| app/components-react/windows/go-live/GoLiveSettings.tsx | Reverts feature-flag gating and layout behavior for update mode and Stream Shift toggle. |
| app/components-react/windows/go-live/GoLiveError.tsx | Collapses multiple RESTREAM error cases back to the generic restream error handling. |
| app/components-react/windows/go-live/GoLive.m.less | Reverts/adjusts CSS class structure for banners/update mode styling. |
| app/components-react/windows/go-live/EditStreamWindow.tsx | Reverts update-mode layout details and cooldown timing/message text. |
| app/components-react/windows/go-live/DestinationSwitchers.tsx | Reverts live output editing-specific switcher disabling logic. |
| app/components-react/windows/go-live/CommonPlatformFields.tsx | Reverts mid-stream common title disabling logic. |
| app/components-react/windows/go-live/AiHighlighterToggle.tsx | Reverts update-mode dismissable behavior and auto-expand logic. |
| app/components-react/windows/go-live/AiHighlighterToggle.m.less | Removes CSS for the update-mode “dismissable” link. |
| app/components-react/shared/StreamShiftToggle.tsx | Reverts gating logic/tooltip behavior; adds force-enable logic for a specific edge case. |
| app/components-react/shared/inputs/RadioInput.m.less | Removes disabled icon opacity styling. |
| app/components-react/shared/DisplaySelector.tsx | Reverts live-output-editing-specific display option behavior and memo deps. |
Suppressed comments (1)
app/components-react/windows/go-live/PlatformSettings.tsx:129
SwitcherCardstill callsonClickeven whendisabled(see SwitcherCard.tsx:106-109), but this handler doesn’t guard againstisLiveOutputEditingDisabled. That allows enabling live output editing even when it’s disabled (e.g., Stream Shift on).
const handleToggleLiveOutputEditing = useCallback(
(status?: boolean) => {
if (!isPrime) {
// TODO: Comment in when ready
// Services.MagicLinkService.actions.linkToPrime('slobs-live-output-editing', {
// event: 'LiveOutputEditing',
// });
return;
}
setLiveOutputEditingEnabled(status ?? !isLiveOutputEditingEnabled);
Services.UsageStatisticsService.actions.recordAnalyticsEvent('LiveOutputEditing', {
toggle: status ?? !isLiveOutputEditingEnabled,
});
},
[setLiveOutputEditingEnabled, isLiveOutputEditingEnabled],
);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| @@ -1,4 +1,4 @@ | |||
| import React, { useCallback } from 'react'; | |||
| import React, { useMemo, useCallback } from 'react'; | |||
| const handleToggleStreamShift = useCallback( | ||
| (status?: boolean) => { | ||
| if (!isPrime) { | ||
| // TODO: Comment in when ready | ||
| // Services.MagicLinkService.actions.linkToPrime('slobs-streamswitcher', { | ||
| // event: 'StreamShift', | ||
| // }); | ||
| return; | ||
| } | ||
|
|
||
| setStreamShift(status ?? !isStreamShiftMode); | ||
| Services.UsageStatisticsService.actions.recordAnalyticsEvent('StreamShift', { | ||
| toggle: status ?? !isStreamShiftMode, | ||
| }); | ||
| }, | ||
| [setStreamShift, isStreamShiftMode], | ||
| ); |
| console.log( | ||
| 'updating ', | ||
| this.state.customDestinations[otherEnabledTargetIndex]?.name, | ||
| ' to disabled', | ||
| ); |
| import React, { useEffect, useState, memo } from 'react'; | ||
| import styles from './AiHighlighterToggle.m.less'; | ||
| import { Services } from 'components-react/service-provider'; | ||
| import * as remote from '@electron/remote'; |
| if (e instanceof StreamError) { | ||
| this.setError({ ...e, type: resolvedType }); | ||
| return resolvedType; | ||
| this.setError({ ...e, type }); | ||
| return e.type; | ||
| } |
| throwStreamError( | ||
| 'RESTREAM_UPDATE_FAILED', | ||
| e, | ||
| `Unable to fetch user stream key for ${mode}.`, | ||
| ); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.
Suppressed comments (13)
Previously missed (3) — in code that hasn't changed since the last review.
app/services/restream.ts:1707
getMode()now returnsportraitfor theverticaldisplay even when dual output mode is off. In single output mode there is no portrait stream; routing targets/removals toportraitcan cause updates/removals to hit a non-running stream key.
getMode(display: TDisplayType): TOutputOrientation {
if (!display) return 'landscape';
return display === 'horizontal' ? 'landscape' : 'portrait';
}
app/components-react/shared/StreamShiftToggle.tsx:35
useEffectusessetStreamShiftbut it’s missing from the dependency array, which will failreact-hooks/exhaustive-depsand can capture a stale setter in unusual cases.
useEffect(() => {
// Ensure that non-ultra users have the stream switcher disabled
if (!isPrime && isStreamShiftMode) {
setStreamShift(false);
}
}, [isPrime, isStreamShiftMode]);
app/components-react/shared/StreamShiftToggle.tsx:58
forceStreamShiftToggleEnabledis intended to bypass the dual output restriction, butdisableTogglestill returnsisStreamShiftDisabled, andisStreamShiftDisabledalready includesisDualOutputMode. This makes the force flag ineffective (toggle stays disabled even whenforceStreamShiftToggleEnabledis true).
const disableToggle = useMemo(() => {
if (p?.disabled) return true;
if (!isPrime) return true;
if (isPatreonEnabled) return true;
if (isDualOutputMode && !forceStreamShiftToggleEnabled) return true;
return isStreamShiftDisabled;
}, [
app/services/streaming/streaming.ts:1666
- When
eis aStreamError,handleTypedStreamErroroverwrites the type in state ({ ...e, type }) but then returnse.type(the original type) instead of the resolvedtype. Callers relying on the returned type may rethrow/report the wrong error type.
app/services/restream.ts:510 throwStreamErrorexpects anIRejectedRequestas its 2nd argument, but this call passes the caughtunknownerror (e). This is a type mismatch and also doesn't match howthrowStreamErroris intended to be used (request metadata vs. exception object).
console.error('Restream Error: Unable to fetch user stream key for', mode, e);
throwStreamError(
'RESTREAM_UPDATE_FAILED',
e,
`Unable to fetch user stream key for ${mode}.`,
);
app/services/restream.ts:524
throwStreamErrorexpects anIRejectedRequestas its 2nd argument, but this call passes the caughtunknownerror (e). This should pass request metadata (or{}) and keep the actual exception in logs.
try {
await this.setupDisplayTargets(platforms, customDestinations, display);
} catch (e: unknown) {
console.error('Restream Error: Unable to create targets for', display, e);
throwStreamError('RESTREAM_UPDATE_FAILED', e, `Unable to create targets for ${display}.`);
}
app/services/restream.ts:533
throwStreamErrorexpects anIRejectedRequestas its 2nd argument, but this call passes the caughtunknownerror (e). This will not typecheck and also discards the intended meaning of therejectedRequestparameter.
try {
await this.addRuntimeTargets(streamKey, targetsByMode[mode] as IRestreamRuntimeTarget[]);
} catch (e: unknown) {
console.error('Restream Error: Unable to add targets for', display, e);
throwStreamError('RESTREAM_UPDATE_FAILED', e, `Unable to add targets for ${display}.`);
}
app/services/restream.ts:611
- Same issue here:
throwStreamError's 2nd arg isIRejectedRequest, but this passes the caughtunknownerror (e). This should pass request metadata (or{}) and keep the exception object in logging only.
try {
// Fetch the key for this mode rather than deriving it, the same way `addTargets` does, so
// that targets are removed from the stream they were added to
await this.removeRuntimeTargets(streamKey, stopTargets);
} catch (e: unknown) {
console.error('Restream Error: Error removing restream targets for', mode, e);
throwStreamError('RESTREAM_UPDATE_FAILED', e, `Unable to remove targets for ${mode}.`);
}
app/components-react/windows/go-live/useGoLiveSettings.ts:145
- Leftover debug logging (
console.log) was introduced here. This will spam production logs when toggling display/both logic.
console.log(
'updating ',
this.state.customDestinations[otherEnabledTargetIndex]?.name,
' to disabled',
);
app/components-react/windows/go-live/AiHighlighterToggle.tsx:6
@electron/remoteis imported here but never used, which will fail lint/typecheck (and adds an unnecessary dependency edge).
import { Services } from 'components-react/service-provider';
import * as remote from '@electron/remote';
import { useDebounce, useVuex } from 'components-react/hooks';
app/components-react/windows/go-live/PlatformSettings.tsx:1
useMemois imported but not used in this file (will fail lint/typecheck).
import React, { useMemo, useCallback } from 'react';
app/components-react/windows/go-live/PlatformSettings.tsx:109
- This handler can still run when the Stream Shift card is disabled.
SwitcherCarddeliberately callsonClickeven whendisabledis true (to allow upgrade flows), so without an explicitisStreamShiftDisabledguard a disabled toggle can be enabled and break feature mutual-exclusion. Also,isPrime/isStreamShiftDisabledare used but missing from the hook deps.
setStreamShift(status ?? !isStreamShiftMode);
Services.UsageStatisticsService.actions.recordAnalyticsEvent('StreamShift', {
toggle: status ?? !isStreamShiftMode,
});
},
app/components-react/windows/go-live/PlatformSettings.tsx:127
- Same issue as Stream Shift: this handler can still fire when the card is disabled because
SwitcherCardcallsonClickeven whendisabledis true. Without anisLiveOutputEditingDisabledguard, a disabled Live Output Editing toggle can be enabled.
setLiveOutputEditingEnabled(status ?? !isLiveOutputEditingEnabled);
Services.UsageStatisticsService.actions.recordAnalyticsEvent('LiveOutputEditing', {
toggle: status ?? !isLiveOutputEditingEnabled,
});
},
| private getValidatedDisplay(display?: TDisplayOutput): TDisplayType { | ||
| if (!display || display === 'both' || !this.dualOutputView.dualOutputMode) { | ||
| if (!display || display === 'both' || !this.isDualOutputMode) { | ||
| return 'horizontal'; | ||
| } |
BundleMonFiles updated (1)
Unchanged files (3)
Total files change -54.23KB -0.35% Final result: ✅ View report in BundleMon website ➡️ |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 49 out of 49 changed files in this pull request and generated 10 comments.
Suppressed comments (3)
app/components-react/windows/go-live/useGoLiveSettings.ts:145
- Remove the debug console.log; this will spam the console in production/test runs and isn't guarded by a dev-mode check.
console.log(
'updating ',
this.state.customDestinations[otherEnabledTargetIndex]?.name,
' to disabled',
);
app/components-react/windows/go-live/useGoLiveSettings.ts:570
- This custom-destination live check builds malformed keys (missing closing brace) and compares concatenated url+streamKey without a delimiter, which can produce false matches/never match.
const dest = this.state.customDestinations[target];
return this.activeDestinations?.some(
d => `{${d.url}${d.streamKey}` === `{${dest.url}${dest.streamKey}`,
);
app/components-react/windows/go-live/AiHighlighterToggle.tsx:6
- Unused import
remotewas added but is not referenced in this file; this will fail linting with unused-import rules.
import { Services } from 'components-react/service-provider';
import * as remote from '@electron/remote';
import { useDebounce, useVuex } from 'components-react/hooks';
| xorWith( | ||
| this.activeDestinations?.map(d => getDestinationId(d)), | ||
| this.state.customDestinations.filter(dest => dest.enabled).map(d => getDestinationId(d)), | ||
| this.activeDestinations?.map(dest => dest.streamKey), | ||
| this.state.customDestinations.filter(dest => dest.enabled).map(dest => dest.streamKey), | ||
| isEqual, | ||
| ).length > 0 |
| } else if (this.streamingService.views.isDualOutputMode) { | ||
| // in dual output mode, we need to set the ingest for each display | ||
| const displays = this.streamInfo.displaysToRestream; | ||
|
|
||
| displays.forEach(async display => { | ||
| const mode = this.getMode(display); | ||
| const settings = await this.fetchUserSettings(mode); | ||
|
|
||
| this.streamSettingsService.setSettings( | ||
| { | ||
| streamType: 'rtmp_custom', | ||
| }, | ||
| display, | ||
| ); | ||
|
|
||
| this.streamSettingsService.setSettings( | ||
| { | ||
| key: settings.streamKey, | ||
| server: ingest, | ||
| }, | ||
| display, | ||
| ); | ||
| }); | ||
| } else { |
| this.SET_STREAM_SWITCHER_STATUS('inactive'); | ||
| this.updateStreamShift('approved'); | ||
| } |
| private getPlatformMode(platform: TPlatform): TOutputOrientation { | ||
| const display = this.streamingService.views.getPlatformMode(platform); | ||
| return this.streamingService.views.getPlatformMode(platform); | ||
| } |
| async searchGames(searchString: string): Promise<IGame[]> { | ||
| if (!searchString || searchString === '') { | ||
| console.debug('Kick search string is empty.'); | ||
| return [] as IGame[]; | ||
| } | ||
|
|
||
| const host = this.hostsService.streamlabs; | ||
| const params = new URLSearchParams({ category: searchString }); | ||
| const url = `https://${host}/api/v5/slobs/kick/info?${params.toString()}`; | ||
| const url = `https://${host}/api/v5/slobs/kick/info?category=${searchString}`; | ||
| const headers = authorizedHeaders(this.userService.apiToken); | ||
| const request = new Request(url, { headers }); |
| if (canDualStream) { | ||
| const tooltip = isLiveOutputEditingEnabled | ||
| ? $t('Dual Stream is not available while live output editing is enabled') | ||
| : $t('Stream both horizontally and vertically to %{platform}', { | ||
| platform: platformLabels(p.platform!), | ||
| }); | ||
| const tooltip = p?.platform | ||
| ? $t('Stream both horizontally and vertically to %{platform}', { | ||
| platform: platformLabels(p.platform), | ||
| }) | ||
| : undefined; | ||
|
|
| get canEditLiveOutputs() { | ||
| return false; | ||
| // return ( | ||
| // !this.isMidStreamMode && | ||
| // this.incrementalRolloutView.featureIsEnabled(EAvailableFeatures.liveOutputEditing) | ||
| // ); | ||
| } |
| .s-spinner--large { | ||
| height: 80px; | ||
| width: 56px; | ||
| } | ||
| .s-spinner--small { | ||
| height: 15px; | ||
| width: 15px; | ||
| } | ||
| .s-spinner__bar { |
| if (!isPrime && isStreamShiftMode) { | ||
| setStreamShift(false); | ||
| } | ||
| }, [isPrime, isStreamShiftMode]); |
| checkIsLive().catch((e: unknown) => { | ||
| console.error('Error checking stream shift status on mount:', e); | ||
| }); | ||
| }, []); |
Replaces: #6142 which had a merge conflict with master. The branch tip is now the same as at 2c33aeabcaf3e3070098c1cecb45d475f793a9eb.
This PR reverts all related PRs that touch functionality: