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
89 changes: 89 additions & 0 deletions app/services/streaming/output-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ERecordingState, EReplayBufferState, EStreamingState } from './streaming-api';

export type TStreamingDisplay = 'horizontal' | 'vertical';
export type TStreamingOutputContext =
| TStreamingDisplay
| 'enhancedBroadcasting'
| 'stream'
| 'streamSecond';
export type TStreamingContextType = 'streaming' | 'recording' | 'replayBuffer';

export interface IDisplayOutputStatus {
streaming: EStreamingState;
recording: ERecordingState;
replayBuffer: EReplayBufferState;
}

type TDisplayNeedsOwnStream = (display: TStreamingDisplay) => boolean;

export function isDisplayOutputContext(
contextName: TStreamingOutputContext,
): contextName is TStreamingDisplay {
return contextName === 'horizontal' || contextName === 'vertical';
}

export function isDisplayStreamingCoveredByEnhancedBroadcasting(
contextName: TStreamingOutputContext,
hasEnhancedBroadcastingStream: boolean,
displayNeedsNonEnhancedBroadcastingInstance: TDisplayNeedsOwnStream,
): contextName is TStreamingDisplay {
return (
isDisplayOutputContext(contextName) &&
hasEnhancedBroadcastingStream &&
!displayNeedsNonEnhancedBroadcastingInstance(contextName)
);
}

export function shouldStopStreamingContext(
contextName: TStreamingOutputContext,
hasEnhancedBroadcastingStream: boolean,
displayNeedsNonEnhancedBroadcastingInstance: TDisplayNeedsOwnStream,
): boolean {
return !isDisplayStreamingCoveredByEnhancedBroadcasting(
contextName,
hasEnhancedBroadcastingStream,
displayNeedsNonEnhancedBroadcastingInstance,
);
}

export function canDestroyDisplayOutputContext(
contextName: TStreamingDisplay,
status: IDisplayOutputStatus,
hasEnhancedBroadcastingStream: boolean,
displayNeedsNonEnhancedBroadcastingInstance: TDisplayNeedsOwnStream,
): boolean {
const streamingIsOfflineOrCovered =
status.streaming === EStreamingState.Offline ||
isDisplayStreamingCoveredByEnhancedBroadcasting(
contextName,
hasEnhancedBroadcastingStream,
displayNeedsNonEnhancedBroadcastingInstance,
);

return (
status.replayBuffer === EReplayBufferState.Offline &&
status.recording === ERecordingState.Offline &&
streamingIsOfflineOrCovered
);
}

export function shouldStopDisplayContextBeforeDestroy(
contextName: TStreamingDisplay,
contextType: TStreamingContextType,
status: IDisplayOutputStatus,
hasEnhancedBroadcastingStream: boolean,
displayNeedsNonEnhancedBroadcastingInstance: TDisplayNeedsOwnStream,
): boolean {
if (
contextType === 'streaming' &&
isDisplayStreamingCoveredByEnhancedBroadcasting(
contextName,
hasEnhancedBroadcastingStream,
displayNeedsNonEnhancedBroadcastingInstance,
)
) {
return false;
}

return status[contextType].toString() !== 'offline';
}
55 changes: 47 additions & 8 deletions app/services/streaming/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ import { EOBSOutputType, EOBSOutputSignal, IOBSOutputSignalInfo } from 'services
import { SignalsService } from 'services/signals-manager';
import { TSocketEvent } from 'services/websocket';
import { HighlighterService } from 'services/highlighter';
import {
canDestroyDisplayOutputContext,
isDisplayOutputContext,
shouldStopDisplayContextBeforeDestroy as shouldStopDisplayOutputContextBeforeDestroy,
shouldStopStreamingContext as shouldStopStreamingOutputContext,
TStreamingDisplay,
} from './output-context';

type TOBSOutputType = 'streaming' | 'recording' | 'replayBuffer';
type TOutputContext = TDisplayType | 'enhancedBroadcasting' | 'stream' | 'streamSecond';
Expand Down Expand Up @@ -1853,6 +1860,7 @@ export class StreamingService
contextNames.forEach(contextName => {
const streaming = this.contexts[contextName].streaming;
if (!streaming) return;
if (!this.shouldStopStreamingOutputContext(contextName)) return;

const forceStop =
force ||
Expand Down Expand Up @@ -3256,7 +3264,41 @@ export class StreamingService
}

private isDisplayContext(context: TOutputContext): context is TDisplayType {
return context === 'horizontal' || context === 'vertical';
return isDisplayOutputContext(context);
}

private hasEnhancedBroadcastingStreamingInstance() {
return this.isEnhancedBroadcastingStreaming(this.contexts.enhancedBroadcasting.streaming);
}

private shouldStopStreamingOutputContext(contextName: TOutputContext) {
return shouldStopStreamingOutputContext(
contextName,
this.hasEnhancedBroadcastingStreamingInstance(),
display => this.displayNeedsNonEnhancedBroadcastingInstance(display),
);
}

private canDestroyDisplayOutputContext(contextName: TDisplayType) {
return canDestroyDisplayOutputContext(
contextName as TStreamingDisplay,
this.state.status[contextName],
this.hasEnhancedBroadcastingStreamingInstance(),
display => this.displayNeedsNonEnhancedBroadcastingInstance(display),
);
}

private shouldStopDisplayOutputContextBeforeDestroy(
contextName: TDisplayType,
contextType: keyof IOutputContext,
) {
return shouldStopDisplayOutputContextBeforeDestroy(
contextName as TStreamingDisplay,
contextType,
this.state.status[contextName],
this.hasEnhancedBroadcastingStreamingInstance(),
display => this.displayNeedsNonEnhancedBroadcastingInstance(display),
);
}

private isEnhancedBroadcastingStreaming(
Expand Down Expand Up @@ -3975,7 +4017,8 @@ export class StreamingService
if (
(contextName === 'horizontal' && skipHorizontal) ||
this.contexts[contextName].streaming === undefined ||
this.contexts[contextName].streaming === null
this.contexts[contextName].streaming === null ||
!this.shouldStopStreamingOutputContext(contextName)
) {
continue;
}
Expand Down Expand Up @@ -4015,10 +4058,7 @@ export class StreamingService
}

// For the horizontal and vertical contexts, only destroy instances if all outputs are offline
const offline =
this.state.status[context].replayBuffer === EReplayBufferState.Offline &&
this.state.status[context].recording === ERecordingState.Offline &&
this.state.status[context].streaming === EStreamingState.Offline;
const offline = this.canDestroyDisplayOutputContext(context);

if (offline || force) {
await this.destroyOutputContextIfExists(context, 'replayBuffer');
Expand Down Expand Up @@ -4055,8 +4095,7 @@ export class StreamingService
// Prevent errors by stopping an active context before destroying it
if (
this.isDisplayContext(contextName) &&
this.state.status[contextName][contextType] &&
this.state.status[contextName][contextType].toString() !== 'offline'
this.shouldStopDisplayOutputContextBeforeDestroy(contextName, contextType)
) {
this.contexts[contextName][contextType].stop(true);

Expand Down
48 changes: 48 additions & 0 deletions test/regular/streaming/output-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import test from 'ava';
import {
ERecordingState,
EReplayBufferState,
EStreamingState,
} from '../../../app/services/streaming/streaming-api';
import {
canDestroyDisplayOutputContext,
shouldStopDisplayContextBeforeDestroy,
shouldStopStreamingContext,
} from '../../../app/services/streaming/output-context';

const displayDoesNotNeedOwnStream = () => false;
const displayNeedsOwnStream = () => true;

test('Enhanced Broadcasting-covered display streaming dependencies are not stop targets', t => {
t.false(shouldStopStreamingContext('horizontal', true, displayDoesNotNeedOwnStream));
t.true(shouldStopStreamingContext('horizontal', false, displayDoesNotNeedOwnStream));
t.true(shouldStopStreamingContext('horizontal', true, displayNeedsOwnStream));
t.true(
shouldStopStreamingContext(
'enhancedBroadcasting',
true,
displayDoesNotNeedOwnStream,
),
);
});

test('Enhanced Broadcasting-covered display contexts can destroy recording-only stream wrappers', t => {
const status = {
streaming: EStreamingState.Live,
recording: ERecordingState.Offline,
replayBuffer: EReplayBufferState.Offline,
};

t.true(
canDestroyDisplayOutputContext('horizontal', status, true, displayDoesNotNeedOwnStream),
);
t.false(
shouldStopDisplayContextBeforeDestroy(
'horizontal',
'streaming',
status,
true,
displayDoesNotNeedOwnStream,
),
);
});
Loading