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
136 changes: 24 additions & 112 deletions src/extension/companion.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { player, startPlaylist } from './intermission-player';
import companion from './util/companion';
import { wait } from './util/helpers';
import { player } from './intermission-player';
import companion, { ActionHandler } from './util/companion';
import { get as nodecg } from './util/nodecg';
import obs, { canChangeScene, changeScene } from './util/obs';
import { assetsVideos, obsData, streamDeckData, videoPlayer } from './util/replicants';
import { assetsVideos, obsData, streamDeckData } from './util/replicants';
import { sc } from './util/speedcontrol';

const config = nodecg().bundleConfig;
import actionVideoPlay from './companion/actionVideoPlay';
import actionIntermissionSceneChange from './companion/actionIntermissionSceneChange';
import actionTimerToggle from './companion/actionTimerToggle';
import actionPlayerHudTriggerToggle from './companion/actionPlayerHudTriggerToggle';
import actionTwitchCommercialsDisable from './companion/actionTwitchCommercialsDisable';
import actionSceneCycle from './companion/actionSceneCycle';

// Replicants only applicable to this file from another bundle.
const twitchCommercialsDisabled = nodecg().Replicant<boolean>('disabled', 'esa-commercials');
Expand Down Expand Up @@ -36,112 +38,22 @@ companion.evt.on('open', (socket) => {
companion.send({ name: 'videos', value: assetsVideos.value });
});

const actionMap: { [key: string]: ActionHandler } = {
timer_toggle: actionTimerToggle,
player_hud_trigger_toggle: actionPlayerHudTriggerToggle,
twitch_commercials_disable: actionTwitchCommercialsDisable,
scene_cycle: actionSceneCycle,
intermission_scene_change: actionIntermissionSceneChange,
video_play: actionVideoPlay,

// Yes, this is still allowed :)
// anything that takes up more than a single line of code should have its own file.
video_stop: () => player.endPlaylistEarly(),
};

// Listening for any actions triggered from Companion.
let videoPlayPressedRecently = false;
companion.evt.on('action', async (name, value) => {
// Controls the nodecg-speedcontrol timer.
// Currently the "Stop Timer" state works if there's only 1 team.
// TODO: Add team support.
if (name === 'timer_toggle') {
try {
// Note: the nodecg-speedcontrol bundle will check if it *can* do these actions,
// we do not need to check that here.
switch (sc.timer.value.state) {
case 'stopped':
case 'paused':
await sc.startTimer();
break;
case 'running':
await sc.stopTimer();
break;
case 'finished':
await sc.resetTimer();
break;
default:
// Don't do anything
break;
}
} catch (err) {
// Drop for now
}
// Used to toggle the "Player HUD Trigger" type.
} else if (name === 'player_hud_trigger_toggle') {
const val = value as string;
if (streamDeckData.value.playerHUDTriggerType === val) {
delete streamDeckData.value.playerHUDTriggerType;
} else {
streamDeckData.value.playerHUDTriggerType = val;
}
// Used to disable the Twitch commercials for the remainder of a run.
} else if (name === 'twitch_commercials_disable') {
if (!twitchCommercialsDisabled.value
&& !['stopped', 'finished'].includes(sc.timer.value.state)) {
// Sends a message to the esa-commercials bundle.
// Because we are using server-to-server messages, no confirmation yet.
nodecg().sendMessageToBundle('disable', 'esa-commercials');
}
// Used to cycle scenes if applicable, usually used by hosts.
// Some of this is copied from obs-data.ts
} else if (name === 'scene_cycle') {
const { disableTransitioning, transitioning, connected } = obsData.value;
const { scenes } = config.obs.names;
// If transitioning is disabled, or we *are* transitioning, and OBS is connected,
// and the timer is not running or paused, we can trigger these actions.
if (!disableTransitioning && !transitioning && connected
&& !['running', 'paused'].includes(sc.timer.value.state)) {
// If the current scene is any of the applicable intermission ones, the next scene
// will be the game layout, so change to it.
if (obs.isCurrentScene(scenes.commercials)
|| obs.isCurrentScene(scenes.intermission)
|| obs.isCurrentScene(scenes.intermissionCrowd)) {
await changeScene({ scene: config.obs.names.scenes.gameLayout });
// If the current scene is the game layout, the next scene will be the intermission,
// so change to it.
} else if (obs.isCurrentScene(scenes.gameLayout)) {
// If the commercial intermission scene exists, use that, if not, use the regular one.
if (obs.findScene(scenes.commercials)) {
await changeScene({ scene: scenes.commercials });
} else {
await changeScene({ scene: scenes.intermission });
}
}
}
// Used to change between intermission scenes using a supplied scene name config key.
} else if (name === 'intermission_scene_change') {
const { scenes } = config.obs.names;
const val = value as string;
const scene = (scenes as { [k: string]: string })[val];
await changeScene({ scene, force: true });
// Used to play back a single video in the "Intermission Player" scene,
// intended to be used by hosts.
} else if (name === 'video_play') {
if (!videoPlayPressedRecently && !videoPlayer.value.playing
&& canChangeScene({ scene: config.obs.names.scenes.intermissionPlayer, force: true })) {
videoPlayPressedRecently = true;
setTimeout(() => { videoPlayPressedRecently = false; }, 1000);
const val = value as string;
nodecg().log.debug('[Companion] Message received to play video (sum: %s)', val);
const videos = assetsVideos.value.filter((v) => v.sum === val);
if (videos.length > 1) {
// VIDEO WAS FOUND TWICE, MAKES NO SENSE!
nodecg().log.debug('[Companion] Multiple videos with the same sum found!');
} else if (!videos.length) {
// VIDEO WAS NOT FOUND
nodecg().log.debug('[Companion] No videos found with that sum!');
} else {
nodecg().log.debug('[Companion] Video found matching sum: %s', videos[0].name);
videoPlayer.value.playlist = [
{
sum: videos[0].sum,
length: 0,
commercial: false,
},
];
wait(500); // Safety wait
await startPlaylist();
}
}
} else if (name === 'video_stop') {
await player.endPlaylistEarly();
if (name in actionMap) {
await actionMap[name](name, value);
}
});
14 changes: 14 additions & 0 deletions src/extension/companion/actionIntermissionSceneChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { get as nodecg } from '@esa-layouts/util/nodecg';
import { changeScene } from '@esa-layouts/util/obs';

const config = nodecg().bundleConfig;

// Used to change between intermission scenes using a supplied scene name config key.
export default async function actionIntermissionSceneChange(name: string, value: unknown)
: Promise<void> {
const { scenes } = config.obs.names;
const val = value as string;
const scene = (scenes as { [k: string]: string })[val];

await changeScene({ scene, force: true });
}
11 changes: 11 additions & 0 deletions src/extension/companion/actionPlayerHudTriggerToggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { streamDeckData } from '@esa-layouts/util/replicants';

// Used to toggle the "Player HUD Trigger" type.
export default function actionPlayerHudTriggerToggle(name: string, value: unknown): void {
const val = value as string;
if (streamDeckData.value.playerHUDTriggerType === val) {
delete streamDeckData.value.playerHUDTriggerType;
} else {
streamDeckData.value.playerHUDTriggerType = val;
}
}
35 changes: 35 additions & 0 deletions src/extension/companion/actionSceneCycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { obsData } from '@esa-layouts/util/replicants';
import { get as nodecg } from '@esa-layouts/util/nodecg';
import { sc } from '@esa-layouts/util/speedcontrol';
import obs, { changeScene } from '@esa-layouts/util/obs';

const config = nodecg().bundleConfig;

// Used to cycle scenes if applicable, usually used by hosts.
// Some of this is copied from obs-data.ts
export default async function actionSceneCycle(): Promise<void> {
const { disableTransitioning, transitioning, connected } = obsData.value;
const { scenes } = config.obs.names;

// If transitioning is disabled, or we *are* transitioning, and OBS is connected,
// and the timer is not running or paused, we can trigger these actions.
if (!disableTransitioning && !transitioning && connected
&& !['running', 'paused'].includes(sc.timer.value.state)) {
// If the current scene is any of the applicable intermission ones, the next scene
// will be the game layout, so change to it.
if (obs.isCurrentScene(scenes.commercials)
|| obs.isCurrentScene(scenes.intermission)
|| obs.isCurrentScene(scenes.intermissionCrowd)) {
await changeScene({ scene: config.obs.names.scenes.gameLayout });
// If the current scene is the game layout, the next scene will be the intermission,
// so change to it.
} else if (obs.isCurrentScene(scenes.gameLayout)) {
// If the commercial intermission scene exists, use that, if not, use the regular one.
if (obs.findScene(scenes.commercials)) {
await changeScene({ scene: scenes.commercials });
} else {
await changeScene({ scene: scenes.intermission });
}
}
}
}
5 changes: 5 additions & 0 deletions src/extension/companion/actionTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// This is a template of witch you can base your own actions on.

export default function actionTemplate(name: string, value: unknown): void {
// TODO: write code
}
28 changes: 28 additions & 0 deletions src/extension/companion/actionTimerToggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { sc } from '../util/speedcontrol';

// Controls the nodecg-speedcontrol timer.
// Currently the "Stop Timer" state works if there's only 1 team.
// TODO: Add team support.
export default async function actionTimerToggle(): Promise<void> {
try {
// Note: the nodecg-speedcontrol bundle will check if it *can* do these actions,
// we do not need to check that here.
switch (sc.timer.value.state) {
case 'stopped':
case 'paused':
await sc.startTimer();
break;
case 'running':
await sc.stopTimer();
break;
case 'finished':
await sc.resetTimer();
break;
default:
// Don't do anything
break;
}
} catch (err) {
// Drop for now
}
}
15 changes: 15 additions & 0 deletions src/extension/companion/actionTwitchCommercialsDisable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { sc } from '@esa-layouts/util/speedcontrol';
import { get as nodecg } from '@esa-layouts/util/nodecg';

// Replicants only applicable to this file from another bundle.
const twitchCommercialsDisabled = nodecg().Replicant<boolean>('disabled', 'esa-commercials');

// Used to disable the Twitch commercials for the remainder of a run.
export default function actionTwitchCommercialsDisable(): void {
if (!twitchCommercialsDisabled.value
&& !['stopped', 'finished'].includes(sc.timer.value.state)) {
// Sends a message to the esa-commercials bundle.
// Because we are using server-to-server messages, no confirmation yet.
nodecg().sendMessageToBundle('disable', 'esa-commercials');
}
}
40 changes: 40 additions & 0 deletions src/extension/companion/actionVideoPlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { assetsVideos, videoPlayer } from '@esa-layouts/util/replicants';
import { canChangeScene } from '@esa-layouts/util/obs';
import { get as nodecg } from '@esa-layouts/util/nodecg';
import { wait } from '@esa-layouts/util/helpers';
import { startPlaylist } from '@esa-layouts/intermission-player';

const config = nodecg().bundleConfig;
let videoPlayPressedRecently = false;

// Used to play back a single video in the "Intermission Player" scene,
// intended to be used by hosts.
export default async function actionVideoPlay(name: string, value: unknown): Promise<void> {
if (!videoPlayPressedRecently && !videoPlayer.value.playing
&& canChangeScene({ scene: config.obs.names.scenes.intermissionPlayer, force: true })) {
videoPlayPressedRecently = true;
setTimeout(() => { videoPlayPressedRecently = false; }, 1000);
const val = value as string;
nodecg().log.debug('[Companion] Message received to play video (sum: %s)', val);
const videos = assetsVideos.value.filter((v) => v.sum === val);
if (videos.length > 1) {
// VIDEO WAS FOUND TWICE, MAKES NO SENSE!
nodecg().log.debug('[Companion] Multiple videos with the same sum found!');
} else if (!videos.length) {
// VIDEO WAS NOT FOUND
nodecg().log.debug('[Companion] No videos found with that sum!');
} else {
nodecg().log.debug('[Companion] Video found matching sum: %s', videos[0].name);
videoPlayer.value.playlist = [
{
sum: videos[0].sum,
length: 0,
commercial: false,
},
];
// TODO: this was never awaited in the original code, did it ever work?
await wait(500); // Safety wait
await startPlaylist();
}
}
}
2 changes: 2 additions & 0 deletions src/extension/util/companion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ function send(data: { name: string, value: unknown }, socket?: WebSocket) {
}
}

export type ActionHandler = (name: string, value: unknown) => Promise<void> | void;

export default {
evt,
send,
Expand Down