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
2 changes: 2 additions & 0 deletions app/components-react/editor/elements/SourceSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ class SourceSelectorController {
* clicking on the source selector selects both sources
*/
if (
this.isDualOutputActive &&
this.dualOutputService.views.hasNodeMap(this.scene.id) &&
this.dualOutputService.views.activeDisplays.horizontal &&
this.dualOutputService.views.activeDisplays.vertical
Expand Down Expand Up @@ -504,6 +505,7 @@ class SourceSelectorController {
* in the vertical display, convert the vertical node id to the horizontal node id.
*/
if (
this.isDualOutputActive &&
this.dualOutputService.views.activeDisplays.horizontal &&
this.dualOutputService.views.activeDisplays.vertical
) {
Expand Down
2 changes: 1 addition & 1 deletion app/services/dual-output/dual-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class DualOutputViews extends ViewHandler<IDualOutputServiceState> {
}

get onlyVerticalDisplayActive() {
return this.activeDisplays.vertical && !this.activeDisplays.horizontal;
return this.dualOutputMode && this.activeDisplays.vertical && !this.activeDisplays.horizontal;
}

get platformsDualStreaming() {
Expand Down
4 changes: 1 addition & 3 deletions app/services/scenes/scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,7 @@ export class Scene {
getSourceSelectorNodes(): TSceneNode[] {
let nodes = this.getNodes();

const populateWithVerticalNodes =
!this.dualOutputService.views.activeDisplays.horizontal &&
this.dualOutputService.views.activeDisplays.vertical;
const populateWithVerticalNodes = this.dualOutputService.views.onlyVerticalDisplayActive;

nodes = nodes.filter(node => {
// if only the vertical display is active
Expand Down
76 changes: 76 additions & 0 deletions test/regular/api/dual-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,87 @@ import { getApiClient } from '../../helpers/api-client';
import { test, useWebdriver, TExecutionContext } from '../../helpers/webdriver';
import { ScenesService, Scene, SceneItem } from 'services/scenes';
import { VideoSettingsService } from 'services/settings-v2/video';
import { SelectionService } from 'services/api/external-api/selection';
import { click, focusMain, waitForDisplayed } from '../../helpers/modules/core';

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

testSelectionAfterDisablingDualOutput(false);
testSelectionAfterDisablingDualOutput(true);

async function setDualOutputMode(status: boolean) {
const client = await getApiClient();
// The RPC fallback exposes this private method; its loading-mode decorator returns a promise.
const dualOutput = client.getResource<{
setDualOutputMode(status: boolean, skipShowVideoSettings: boolean): Promise<void>;
}>('DualOutputService');

// Exercise the normal mode transition without requiring a provider login.
await dualOutput.setDualOutputMode(status, true);
await focusMain();
}

function testSelectionAfterDisablingDualOutput(horizontalVisible: boolean) {
test(`Selection after disabling dual output with horizontal ${
horizontalVisible ? 'visible' : 'hidden'
}`, async t => {
const client = await getApiClient();
const scenesService = client.getResource<ScenesService>('ScenesService');
const dualOutputService = client.getResource<DualOutputService>('DualOutputService');
const selection = client.getResource<SelectionService>('SelectionService');
const scene = scenesService.createScene('Selection transition');
scenesService.makeSceneActive(scene.id);
const horizontalItem = scene.createAndAddSource('Selection target', 'color_source');
horizontalItem.fitToScreen();

await setDualOutputMode(true);
dualOutputService.toggleDisplay(horizontalVisible, 'horizontal');
const verticalItem = scene.getItems().find(item => item.display === 'vertical');
t.truthy(verticalItem, 'Dual output created a vertical partner');
t.deepEqual(
scene.getSourceSelectorNodes().map(node => node.id),
[horizontalVisible ? horizontalItem.id : verticalItem.id],
'Dual output source rows follow the visible displays',
);

await setDualOutputMode(false);
await waitForDisplayed('#horizontal-display');
t.deepEqual(
scene.getSourceSelectorNodes().map(node => node.id),
[horizontalItem.id],
'Single output source rows always use horizontal items',
);

await click('[data-name="Selection target"]');
t.deepEqual(selection.getIds(), [horizontalItem.id], 'List selects only the visible item');

selection.reset();
await t.context.app.client.waitUntil(async () => {
const selectedRows = await t.context.app.client.$$(
'.ant-tree-node-selected [data-name="Selection target"]',
);
return selectedRows.length === 0;
});
await click('#horizontal-display');
await waitForDisplayed('.ant-tree-node-selected [data-name="Selection target"]');
t.deepEqual(selection.getIds(), [horizontalItem.id], 'Canvas selection highlights the list');

await setDualOutputMode(true);
t.is(
dualOutputService.state.videoSettings.activeDisplays.horizontal,
horizontalVisible,
'The saved horizontal display preference is preserved',
);
t.deepEqual(
scene.getSourceSelectorNodes().map(node => node.id),
[horizontalVisible ? horizontalItem.id : verticalItem.id],
'Re-enabling dual output restores the corresponding source rows',
);
});
}

function confirmDualOutputSources(t: TExecutionContext, scene: Scene) {
const numSceneItems = scene
.getItems()
Expand Down
Loading