Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 30 additions & 55 deletions app/components-react/root/StartStreamingButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as remote from '@electron/remote';
import { TStreamShiftStatus } from 'services/restream';
import { promptAction } from 'components-react/modals';
import { IStreamShiftRequested, IStreamShiftActionCompleted } from 'services/websocket';
import { confirmStreamShift } from 'components-react/shared/StreamShiftModal';

export default function StartStreamingButton(p: { disabled?: boolean }) {
const {
Expand Down Expand Up @@ -77,41 +78,35 @@ export default function StartStreamingButton(p: { disabled?: boolean }) {
(event: IStreamShiftRequested | IStreamShiftActionCompleted) => {
const { streamShiftStreamId } = RestreamService.state;
console.debug('Event ID: ' + event.data.identifier, '\n Stream ID: ' + streamShiftStreamId);
const isFromOtherDevice =
streamShiftStreamId && event.data.identifier !== streamShiftStreamId;

const isMobileRemote = isFromOtherDevice ? /[A-Z]/.test(event.data.identifier) : false;
const remoteDeviceType = isMobileRemote ? 'mobile' : 'desktop';

// Note: because the event's stream id is from the device that requested the switch,
// it is not possible to know what type of device the stream will be switching from.
// We can only identify the type of device the stream is switching to.
const switchType = `desktop-${remoteDeviceType}`;
const isFromOtherDevice = streamShiftStreamId
? event.data.identifier !== streamShiftStreamId
: true;
const switchType = formatStreamType(isFromOtherDevice, event.data.identifier);

if (event.type === 'streamSwitchRequest') {
if (!isFromOtherDevice) {
// Don't record the request from this device because the other device will record it
RestreamService.actions.confirmStreamShift('approved');
} else {
UsageStatisticsService.recordAnalyticsEvent('StreamShift', {
stream: switchType,
action: 'request',
});
}

UsageStatisticsService.recordAnalyticsEvent('StreamShift', {
stream: switchType,
action: 'request',
});
}

if (event.type === 'switchActionComplete') {
// End the stream on this device if switching the stream to another device
// Only record analytics if the stream was switched from this device to a different one

if (isFromOtherDevice) {
Services.RestreamService.actions.endStreamShiftStream(event.data.identifier);

UsageStatisticsService.recordAnalyticsEvent('StreamShift', {
stream: switchType,
action: 'complete',
});
}

UsageStatisticsService.recordAnalyticsEvent('StreamShift', {
stream: switchType,
action: 'complete',
});

// Notify the user
const message = isFromOtherDevice
? $t(
Expand All @@ -137,6 +132,19 @@ export default function StartStreamingButton(p: { disabled?: boolean }) {
};
}, []);

const formatStreamType = useCallback((isFromOtherDevice: boolean, eventStreamId?: string) => {
// Because the event's stream id is from the device that requested the switch,
// it is not possible to know what type of device the stream will be switching from.
// We can only identify the type of device the stream is switching to.
if (!isFromOtherDevice || !eventStreamId) {
return 'other-desktop';
}

// Mobile stream ids have capital letters, Desktop stream ids do not.
const remoteDeviceType = /[A-Z]/.test(eventStreamId) ? 'mobile' : 'desktop';
return `desktop-${remoteDeviceType}`;
}, []);

const toggleStreaming = useCallback(async () => {
if (StreamingService.isStreaming) {
StreamingService.toggleStreaming();
Expand Down Expand Up @@ -189,33 +197,8 @@ export default function StartStreamingButton(p: { disabled?: boolean }) {
const isLive = await fetchStreamShiftStatus();
setIsLoading(false);

const message = isDualOutputMode
? $t(
'A stream on another device has been detected. Would you like to switch your stream to Streamlabs Desktop? If you do not wish to continue this stream, please end it from the current streaming source. Dual Output will be disabled since not supported in this mode. If you\'re sure you\'re not live and it has been incorrectly detected, choose "Force Start" below.',
)
: $t(
'A stream on another device has been detected. Would you like to switch your stream to Streamlabs Desktop? If you do not wish to continue this stream, please end it from the current streaming source. If you\'re sure you\'re not live and it has been incorrectly detected, choose "Force Start" below.',
);

if (isLive) {
const { streamShiftForceGoLive } = RestreamService.state;
let shouldForceGoLive = streamShiftForceGoLive;

await promptAction({
title: $t('Another stream detected'),
message,
btnText: $t('Switch to Streamlabs Desktop'),
fn: startStreamShift,
cancelBtnText: $t('Cancel'),
cancelBtnPosition: 'left',
secondaryActionText: $t('Force Start'),
secondaryActionFn: async () => {
// FIXME: this should actually do something server-side
RestreamService.actions.return.forceStreamShiftGoLive(true);
shouldForceGoLive = true;
},
});

const shouldForceGoLive = await confirmStreamShift();
if (!shouldForceGoLive) {
return;
}
Expand Down Expand Up @@ -252,14 +235,6 @@ export default function StartStreamingButton(p: { disabled?: boolean }) {
}
}, []);

const startStreamShift = useCallback(() => {
if (isDualOutputMode) {
Services.DualOutputService.actions.toggleDisplay(false, 'vertical');
}

StreamingService.actions.goLive();
}, [isDualOutputMode]);

const shouldShowGoLiveWindow = useCallback(() => {
if (!UserService.isLoggedIn) return false;
const primaryPlatform = UserService.state.auth?.primaryPlatform;
Expand Down
37 changes: 37 additions & 0 deletions app/components-react/shared/StreamShiftModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Services } from '../service-provider';
import { promptAction } from 'components-react/modals';
import { $t } from 'services/i18n/i18n';

export async function confirmStreamShift() {
const { RestreamService, DualOutputService } = Services;
const { streamShiftForceGoLive } = RestreamService.state;
let shouldForceGoLive = streamShiftForceGoLive;

const message = DualOutputService.state.dualOutputMode
? $t(
'A stream on another device has been detected. Would you like to switch your stream to Streamlabs Desktop? If you do not wish to continue this stream, please end it from the current streaming source. Dual Output will be disabled since not supported in this mode. If you\'re sure you\'re not live and it has been incorrectly detected, choose "Force Start" below.',
)
: $t(
'A stream on another device has been detected. Would you like to switch your stream to Streamlabs Desktop? If you do not wish to continue this stream, please end it from the current streaming source. If you\'re sure you\'re not live and it has been incorrectly detected, choose "Force Start" below.',
);

await promptAction({
title: $t('Another stream detected'),
message,
btnText: $t('Switch to Streamlabs Desktop'),
fn: () => {
RestreamService.actions.startStreamShift();
shouldForceGoLive = false;
},
cancelBtnText: $t('Cancel'),
cancelBtnPosition: 'left',
secondaryActionText: $t('Force Start'),
secondaryActionFn: async () => {
// FIXME: this should actually do something server-side
RestreamService.actions.return.forceStreamShiftGoLive(true);
shouldForceGoLive = true;
},
});

return shouldForceGoLive;
}
1 change: 1 addition & 0 deletions app/components-react/shared/StreamShiftToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default function StreamShiftToggle(p: IStreamShiftToggle) {
<div className={styles.streamShiftWrapper}>
<div className={cx(p?.className, styles.streamShiftToggle)} style={p?.style}>
<CheckboxInput
name={'streamShift'}
className={p?.checkboxClassname}
label={
!isPrime ? (
Expand Down
35 changes: 7 additions & 28 deletions app/components-react/windows/go-live/GoLiveWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import styles from './GoLive.m.less';
import { WindowsService, DualOutputService, IncrementalRolloutService } from 'app-services';
import { WindowsService } from 'app-services';
import { ModalLayout } from '../../shared/ModalLayout';
import { Button, message } from 'antd';
import { Services } from '../../service-provider';
Expand All @@ -12,8 +12,7 @@ import Animation from 'rc-animate';
import { useGoLiveSettings, useGoLiveSettingsRoot } from './useGoLiveSettings';
import { inject } from 'slap';
import RecordingSwitcher from './RecordingSwitcher';
import { promptAction } from 'components-react/modals';
import { EAvailableFeatures } from 'services/incremental-rollout';
import { confirmStreamShift } from 'components-react/shared/StreamShiftModal';

export default function GoLiveWindow() {
const { lifecycle, form } = useGoLiveSettingsRoot().extend(module => ({
Expand Down Expand Up @@ -86,31 +85,11 @@ function ModalFooter() {
setIsFetchingStreamStatus(false);

// Prompt to confirm stream switch if the stream exists
// TODO: unify with start streaming button prompt
const { streamShiftForceGoLive } = Services.RestreamService.state;
if (isLive && !streamShiftForceGoLive) {
let shouldForceGoLive = false;

await promptAction({
title: $t('Another stream detected'),
message: $t(
'A stream on another device has been detected. Would you like to switch your stream to Streamlabs Desktop? If you do not wish to continue this stream, please end it from the current streaming source. If you\'re sure you\'re not live and it has been incorrectly detected, choose "Force Start" below.',
),
btnText: $t('Switch to Streamlabs Desktop'),
fn: () => {
goLive();
close();
},
cancelBtnText: $t('Cancel'),
cancelBtnPosition: 'left',
secondaryActionText: $t('Force Start'),
secondaryActionFn: async () => {
Services.RestreamService.actions.forceStreamShiftGoLive(true);
shouldForceGoLive = true;
},
});

if (!shouldForceGoLive) return;
if (isLive && !Services.RestreamService.state.streamShiftForceGoLive) {
const shouldForceGoLive = await confirmStreamShift();
if (!shouldForceGoLive) {
return;
}
}
} catch (e: unknown) {
console.error('Error checking stream switcher status:', e);
Expand Down
24 changes: 19 additions & 5 deletions app/services/restream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export interface ITargetLiveData extends IStreamShiftTarget {
game_name?: string;
}

export type TStreamShiftStatus = 'pending' | 'inactive' | 'active';
export type TStreamShiftStatus = 'pending' | 'claimed' | 'inactive' | 'active';
export type TStreamShiftAction = 'approved' | 'rejected';

interface IRestreamState {
Expand Down Expand Up @@ -121,7 +121,7 @@ export class RestreamService extends StatefulService<IRestreamState> {
enabled: true,
grandfathered: false,
tiktokGrandfathered: false,
streamShiftStreamId: undefined,
streamShiftStreamId: null,
streamShiftStatus: 'inactive',
streamShiftTargets: [],
streamShiftForceGoLive: false,
Expand Down Expand Up @@ -518,7 +518,8 @@ export class RestreamService extends StatefulService<IRestreamState> {
this.streamSettingsService.setGoLiveSettings({ streamShift: true });
this.SET_STREAM_SWITCHER_STATUS('pending');
this.SET_STREAM_SWITCHER_TARGETS(status.targets);
} else if (this.state.streamShiftStatus === 'pending') {
} else {
this.SET_STREAM_SWITCHER_STREAM_ID();
this.SET_STREAM_SWITCHER_STATUS('inactive');
this.SET_STREAM_SWITCHER_TARGETS([]);
}
Expand Down Expand Up @@ -661,6 +662,19 @@ export class RestreamService extends StatefulService<IRestreamState> {
this.SET_STREAM_SWITCHER_TARGETS([]);
}

async startStreamShift() {
try {
await this.streamingService.goLive();
this.updateStreamShift('approved');

return Promise.resolve();
} catch (e: unknown) {
console.error('Error switching stream shift stream:', e);

return Promise.reject(e);
}
}

async confirmStreamShift(action: TStreamShiftAction) {
if (action === 'rejected') {
this.SET_STREAM_SWITCHER_STATUS('pending');
Expand All @@ -669,7 +683,7 @@ export class RestreamService extends StatefulService<IRestreamState> {
this.dualOutputService.toggleDisplay(false, 'vertical');
}

this.SET_STREAM_SWITCHER_STATUS('inactive');
this.SET_STREAM_SWITCHER_STATUS('claimed');
this.updateStreamShift('approved');
}
}
Expand Down Expand Up @@ -699,7 +713,7 @@ export class RestreamService extends StatefulService<IRestreamState> {
*/
async endStreamShiftStream(remoteStreamId: string): Promise<void> {
try {
this.SET_STREAM_SWITCHER_STATUS('active');
this.SET_STREAM_SWITCHER_STATUS('inactive');
await this.streamingService.toggleStreaming();
this.SET_STREAM_SWITCHER_STREAM_ID(remoteStreamId);
} catch (error: unknown) {
Expand Down
5 changes: 3 additions & 2 deletions app/services/streaming/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,8 +1057,9 @@ export class StreamingService
startStreamingPromise
.then(() => {
if (this.views.settings.streamShift) {
// Remove the pending state to show the correct text in the start streaming button
this.restreamService.setStreamShiftStatus('inactive');
// Remove the pending state and update streaming state to show the correct text in the start streaming button
this.restreamService.setStreamShiftStatus('active');
this.SET_STREAMING_STATUS(EStreamingState.Live);

// Confirm that the primary platform is streaming to correctly show chat
// Otherwise, use the first enabled platform. Note: this is a failsafe to guarantee
Expand Down
Loading
Loading