Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b99bbc0
Pass saved streaming encoder settings to Factory API video encoder
aleksandr-voitenko Apr 7, 2026
18d15e3
Fix apply encoder settings to video encoder instance when recording w…
michelinewu Apr 7, 2026
d6ab2f4
Test fixes.
michelinewu Apr 8, 2026
b1918cf
Remove audio track from enhanced broadcasting.
michelinewu Apr 8, 2026
fa8e3c2
Fix for undefined audio track.
michelinewu Apr 8, 2026
438af75
Fix Twitch VOD and enhanced broadcasting.
michelinewu Apr 8, 2026
c4339ec
Fix enhanced broadcasting test.
michelinewu Apr 8, 2026
fa483cf
Fix enhanced broadcasting test.
michelinewu Apr 8, 2026
02f4137
Fix VOD part of Enhanced Broadcasting Test.
michelinewu Apr 9, 2026
4d29875
Remove multistreaming from enhanced broadcasting test.
michelinewu Apr 9, 2026
5245f1d
Remove VOD from Enhanced Broadcasting Test.
michelinewu Apr 9, 2026
0ea7dec
Merge branch 'staging' into fix-factory-api-video-encoder-bitrate
michelinewu Apr 10, 2026
7c0dcf4
Skip Enhnaced Broadcasting test.
michelinewu Apr 10, 2026
aae2d62
Fix destroy enhanced broadcasting instances and error handling.
michelinewu Apr 10, 2026
79c8a27
Remove check for second signal from dual output ultra test.
michelinewu Apr 10, 2026
33c93b6
Fix for strictnulls.
michelinewu Apr 10, 2026
34e8662
Remove dual output from selective recording test.
michelinewu Apr 10, 2026
7acd61e
Re-enable Enhanced Broadcasting test.
michelinewu Apr 10, 2026
579a131
Disable enhanced broadcasting test.
michelinewu Apr 10, 2026
c8d58e8
Fixes for dual output and enhanced broadcasting.
michelinewu Apr 13, 2026
7b2e508
Add YouTube dual stream to test.
michelinewu Apr 13, 2026
620a377
Re-enable Twitch Enhanced Broadcasting test.
michelinewu Apr 13, 2026
94a52ab
Test add delay.
michelinewu Apr 14, 2026
254aacc
Fix crash for vertical display and enhanced broadcasting.
michelinewu Apr 14, 2026
f37c3b2
Preserve error type for factory output errors.
michelinewu Apr 14, 2026
e6866b5
Pass in display when starting stream.
michelinewu Apr 14, 2026
6047e86
Merge branch 'staging' into fix-factory-api-video-encoder-bitrate
michelinewu Apr 14, 2026
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
23 changes: 13 additions & 10 deletions app/services/settings/output/output-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,12 @@ interface IAdvancedRecordingOutputSettings extends IRecordingOutputSettings {

interface IStreamingOutputSettings {
enforceServiceBitrate: boolean;
enableTwitchVOD: boolean;
}

interface ISimpleStreamingOutputSettings extends IStreamingOutputSettings {
enforceServiceBitrate: boolean;
useAdvanced: boolean;
customEncSettings: string;
videoEncoder: EObsAdvancedEncoder; // TODO: should this be `EObsSimpleEncoder`?
videoEncoder: EObsAdvancedEncoder;
}

interface IAdvancedStreamingOutputSettings extends IStreamingOutputSettings {
Expand All @@ -163,6 +161,7 @@ interface IAdvancedStreamingOutputSettings extends IStreamingOutputSettings {
outputWidth: number;
outputHeight: number;
videoEncoder: EObsAdvancedEncoder;
enableTwitchVOD: boolean;
twitchTrack?: number;
}

Expand Down Expand Up @@ -647,18 +646,17 @@ export class OutputSettingsService extends Service {
'VodTrackIndex',
);

return { ...advancedStreamSettings, twitchTrack };
return { ...advancedStreamSettings, twitchTrack } as IAdvancedStreamingOutputSettings;
}

return advancedStreamSettings;
return advancedStreamSettings as IAdvancedStreamingOutputSettings;
} else {
return {
videoEncoder,
enforceServiceBitrate,
useAdvanced,
customEncSettings,
enableTwitchVOD,
};
} as ISimpleStreamingOutputSettings;
}
}

Expand Down Expand Up @@ -806,14 +804,19 @@ export class OutputSettingsService extends Service {
return this.settingsService.findSettingValue(output, 'Recording', 'RecAAudio') ?? 'ffmpeg_aac';
}

getStreamingVideoEncoderSettings(): ISettings {
getStreamingVideoEncoderSettings(mode: TOutputSettingsMode): ISettings {
const output = this.settingsService.state.Output.formData;

const bitrate = this.settingsService.findSettingValue(output, 'Streaming', 'VBitrate');
const bitrate =
this.settingsService.findSettingValue(output, 'Streaming', 'bitrate') ??
this.settingsService.findSettingValue(output, 'Streaming', 'VBitrate');

if (mode === 'Simple') {
return { bitrate };
}

// TODO: these are only being fetched in advanced mode
const rateControl = this.settingsService.findSettingValue(output, 'Streaming', 'rate_control');
// const bitrate = this.settingsService.findSettingValue(output, 'Streaming', 'bitrate');
const keyintSec = this.settingsService.findSettingValue(output, 'Streaming', 'keyint_sec');
const x264opts = this.settingsService.findSettingValue(output, 'Streaming', 'x264opts');

Expand Down
68 changes: 49 additions & 19 deletions app/services/streaming/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ interface IOutputContext {

interface IStreamingContextSettings {
output: TDisplayOutput;
audioTrack: number;
audioTrack?: number;
start?: boolean;
context?: TOutputContext;
isEnhancedBroadcasting?: boolean;
Expand Down Expand Up @@ -1595,13 +1595,17 @@ export class StreamingService
) {
// Handle single output mode
if (context === 'enhancedBroadcasting') {
await this.createStreaming({
output: 'horizontal',
audioTrack: 1,
start: true,
context: 'horizontal',
isEnhancedBroadcasting: false,
});
if (this.views.shouldSetupRestream) {
await this.createStreaming({
output: 'horizontal',
audioTrack: 1,
start: true,
context: 'horizontal',
isEnhancedBroadcasting: false,
});
} else {
await this.handleStartStreaming(code);
}
}

if (context === 'horizontal') {
Expand Down Expand Up @@ -1727,7 +1731,6 @@ export class StreamingService

await this.createStreaming({
output: display,
audioTrack: 3,
start: true,
context: 'enhancedBroadcasting',
isEnhancedBroadcasting: true,
Expand Down Expand Up @@ -1814,16 +1817,30 @@ export class StreamingService
const resolution = this.videoSettingsService.outputResolutions[display];
stream.outputWidth = resolution.outputWidth;
stream.outputHeight = resolution.outputHeight;
// stream audio track
this.validateOrCreateAudioTrack(index);
stream.audioTrack = index;

if (!isEnhancedBroadcasting) {
// stream audio track
const defaultAudioTrack = display === 'horizontal' ? 1 : 2;
this.validateOrCreateAudioTrack(index ?? defaultAudioTrack);
stream.audioTrack = index ?? defaultAudioTrack;
}

// Twitch VOD audio track
if (stream.enableTwitchVOD && stream.twitchTrack) {
this.validateOrCreateAudioTrack(stream.twitchTrack);
} else if (stream.enableTwitchVOD) {
// do not use the same audio track for the VOD as the stream
stream.twitchTrack = !isEnhancedBroadcasting ? index : index + 1;
this.validateOrCreateAudioTrack(stream.twitchTrack);
console.error('Twitch VOD is enabled but no Twitch audio track is set.');
this.rejectStartStreaming();
this.setError(
createStreamError(
'UNKNOWN_STREAMING_ERROR_WITH_MESSAGE',
{},
'Twitch VOD is enabled but no Twitch audio track is set. Please select a Twitch audio track in the output settings and try again.',
),
);
this.handleDestroyOutputContexts(contextName);

return;
}

this.contexts[contextName].streaming = stream as
Expand Down Expand Up @@ -2237,10 +2254,23 @@ export class StreamingService
key === 'videoEncoder' &&
(contextName !== 'enhancedBroadcasting' || isEnhancedBroadcastingContext)
) {
instance.videoEncoder = VideoEncoderFactory.create(
settings.videoEncoder,
`video-encoder-${type}-${contextName}`,
);
const encoderSettings =
type === 'streaming'
? this.outputSettingsService.getStreamingVideoEncoderSettings(mode)
: undefined;

if (encoderSettings) {
instance.videoEncoder = VideoEncoderFactory.create(
settings.videoEncoder,
`video-encoder-${type}-${contextName}`,
encoderSettings,
);
} else {
instance.videoEncoder = VideoEncoderFactory.create(
settings.videoEncoder,
`video-encoder-${type}-${contextName}`,
);
}

if (instance.videoEncoder.lastError) {
console.error(
Expand Down
26 changes: 20 additions & 6 deletions test/regular/recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,13 @@ async function validateRecordingFiles(
waitForDisplayed('h1=Recordings');

const numRecordings = await getNumElements('[data-test=filename]');
t.is(numRecordings, numFiles, 'All recordings show in history matches number of files recorded');
t.is(
numRecordings,
numFiles,
`All recordings from ${
advanced ? 'Advanced' : 'Simple'
} mode in history matches number of files recorded`,
);
}

/**
Expand Down Expand Up @@ -158,27 +164,35 @@ test('Recording with two contexts active', async t => {

test('Recording from Go Live window', async t => {
const user = await logIn(t);
await setOutputResolution('100x100');
const tmpDir = await setTemporaryRecordingPath();
await prepareToGoLive();
const tmpDir = await setTemporaryRecordingPath();

await clickGoLive();
await waitForSettingsWindowLoaded();

await clickToggle('recording-toggle');

if (user.type === 'twitch') {
await fillForm({
twitchGame: 'Fortnite',
});
}

await clickToggle('recording-toggle');
if (user.type === 'youtube') {
await fillForm({
title: 'Test Stream',
description: 'Test Stream Description',
});
}

await submit();
await waitForStreamStart();
await focusMain();
await sleep(2000);
await stopRecording();
await stopStream();

const files = await readdir(tmpDir);
t.is(files.length, 1, `Files that were created:\n${files.join('\n')}`);
await validateRecordingFiles(t, tmpDir, 1);

await logOut(t, true);
});
3 changes: 2 additions & 1 deletion test/regular/streaming/multistream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ test('Stream Shift', withUser('twitch', { prime: true, multistream: true }), asy
// Multistream shift
await goLiveWithStreamShift(t, true);

await goLiveWithDefaultCodec();
// Skip testing UI for incompatible codecs until backend changes are merged
// await goLiveWithDefaultCodec();

t.pass();
});
64 changes: 63 additions & 1 deletion test/regular/streaming/twitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,29 @@ import {
import { showSettingsWindow } from '../../helpers/modules/settings/settings';
import { clickButton, focusChild, isDisplayed, waitForDisplayed } from '../../helpers/modules/core';
import { restartApp, skipCheckingErrorsInLog, test, useWebdriver } from '../../helpers/webdriver';
import { reserveUserFromPool } from '../../helpers/webdriver/user';
import { reserveUserFromPool, withUser } from '../../helpers/webdriver/user';
import { getApiClient } from '../../helpers/api-client';
import { StreamSettingsService } from '../../../app/services/settings/streaming';
import { assertFormContains, fillForm } from '../../helpers/modules/forms';
import { setInputValue } from '../../helpers/modules/forms/base';
import { logIn } from '../../helpers/modules/user';
import { dismissModal } from '../../helpers/webdriver/modals';

async function enableTwitchVOD() {
await showSettingsWindow('Output', async () => {
await fillForm({ Mode: 'Advanced' });
await fillForm('Streaming', { VodTrackEnabled: true, VodTrackIndex: 3 });
await clickButton('Close');
});
}

// not a react hook
// eslint-disable-next-line react-hooks/rules-of-hooks
useWebdriver();

test('Streaming to Twitch', async t => {
await logIn('twitch', { multistream: false });

await goLive({
title: 'SLOBS Test Stream',
twitchGame: 'Warcraft III',
Expand All @@ -37,6 +46,15 @@ test('Streaming to Twitch', async t => {
await showSettingsWindow('Stream');
await waitForDisplayed("div=You can not change these settings when you're live");
await stopStream();

// Twitch VOD Track
await enableTwitchVOD();
await clickGoLive();
await waitForSettingsWindowLoaded();
await submit();
await waitForStreamStart();
await stopStream();

t.pass();
});

Expand Down Expand Up @@ -142,3 +160,47 @@ test('Streaming to Twitch unlisted category', async t => {
});
t.pass();
});

test('Twitch Enhanced Broadcasting', withUser('twitch', { multistream: true }), async t => {
await prepareToGoLive();

// Single Output Single Stream
await clickGoLive();
await waitForSettingsWindowLoaded();

await fillForm({
title: 'Test Stream',
twitchGame: 'Fortnite',
isEnhancedBroadcasting: true,
});

await submit();
await waitForStreamStart();
await stopStream();

// Single Output Single Stream with Twitch VOD enabled
await enableTwitchVOD();
await goLive();
await stopStream();

// Single Output Multistream
await clickGoLive();
await waitForSettingsWindowLoaded();

await fillForm({
trovo: true,
});

await waitForSettingsWindowLoaded();
await submit();
await waitForStreamStart();
await stopStream();

// Twitch VOD not available with multistream
await showSettingsWindow('Output', async () => {
const vodTrackDisplayed = await isDisplayed('input[name="VodTrackEnabled"]');
t.false(vodTrackDisplayed, 'Twitch VOD option is not displayed when multistream is enabled');
});

t.pass();
});
Loading