Skip to content

Commit 4df894f

Browse files
committed
feat: add structured action error codes to CasparCG device
1 parent 8960595 commit 4df894f

2 files changed

Lines changed: 136 additions & 13 deletions

File tree

  • packages
    • timeline-state-resolver-types/src/integrations
    • timeline-state-resolver/src/integrations/casparCG

packages/timeline-state-resolver-types/src/integrations/casparcg.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,89 @@ export const CasparCGStatusMessages: Record<CasparCGStatusCode, string> = {
4343
[CasparCGStatusCode.QUEUE_OVERFLOW]: 'CasparCG command queue overflow',
4444
}
4545

46+
/**
47+
* Action error codes for CasparCG device actions.
48+
* These codes can be customized in blueprints via deviceActionMessages.
49+
*
50+
* Error codes follow the pattern: ACTION_CASPARCG_{ACTION}_{REASON}
51+
*/
52+
export const CasparCGActionErrorCode = {
53+
// ClearAllChannels errors
54+
/** ClearAllChannels: no connection to CasparCG */
55+
CLEAR_NO_CONNECTION: 'ACTION_CASPARCG_CLEAR_NO_CONNECTION',
56+
/** ClearAllChannels: failed to execute INFO command */
57+
CLEAR_INFO_FAILED: 'ACTION_CASPARCG_CLEAR_INFO_FAILED',
58+
/** ClearAllChannels: no channel data returned from INFO command */
59+
CLEAR_NO_CHANNELS: 'ACTION_CASPARCG_CLEAR_NO_CHANNELS',
60+
61+
// RestartServer errors
62+
/** RestartServer: device not initialized (no connection options) */
63+
RESTART_NOT_INITIALIZED: 'ACTION_CASPARCG_RESTART_NOT_INITIALIZED',
64+
/** RestartServer: launcher host not configured */
65+
RESTART_LAUNCHER_HOST_NOT_SET: 'ACTION_CASPARCG_RESTART_LAUNCHER_HOST_NOT_SET',
66+
/** RestartServer: launcher port not configured */
67+
RESTART_LAUNCHER_PORT_NOT_SET: 'ACTION_CASPARCG_RESTART_LAUNCHER_PORT_NOT_SET',
68+
/** RestartServer: launcher process not configured */
69+
RESTART_LAUNCHER_PROCESS_NOT_SET: 'ACTION_CASPARCG_RESTART_LAUNCHER_PROCESS_NOT_SET',
70+
/** RestartServer: launcher returned a non-200 HTTP status */
71+
RESTART_BAD_REPLY: 'ACTION_CASPARCG_RESTART_BAD_REPLY',
72+
/** RestartServer: network request to launcher failed */
73+
RESTART_REQUEST_FAILED: 'ACTION_CASPARCG_RESTART_REQUEST_FAILED',
74+
75+
// ListMedia errors
76+
/** ListMedia: device not initialized */
77+
LIST_NOT_INITIALIZED: 'ACTION_CASPARCG_LIST_NOT_INITIALIZED',
78+
/** ListMedia: CLS command returned an error */
79+
LIST_CLS_ERROR: 'ACTION_CASPARCG_LIST_CLS_ERROR',
80+
/** ListMedia: CLS command returned a non-200 response code */
81+
LIST_BAD_RESPONSE: 'ACTION_CASPARCG_LIST_BAD_RESPONSE',
82+
} as const
83+
84+
export type CasparCGActionErrorCode = (typeof CasparCGActionErrorCode)[keyof typeof CasparCGActionErrorCode]
85+
86+
/**
87+
* Default human-readable messages for each CasparCG action error code.
88+
* Used as fallback when no blueprint customization is present.
89+
*/
90+
export const CasparCGActionErrorMessages: Record<CasparCGActionErrorCode, string> = {
91+
[CasparCGActionErrorCode.CLEAR_NO_CONNECTION]: 'Cannot clear CasparCG channels: no connection',
92+
[CasparCGActionErrorCode.CLEAR_INFO_FAILED]: 'Cannot clear CasparCG channels: failed to retrieve channel info',
93+
[CasparCGActionErrorCode.CLEAR_NO_CHANNELS]: 'Cannot clear CasparCG channels: no channels found',
94+
95+
[CasparCGActionErrorCode.RESTART_NOT_INITIALIZED]: 'Cannot restart CasparCG: device not initialized',
96+
[CasparCGActionErrorCode.RESTART_LAUNCHER_HOST_NOT_SET]: 'Cannot restart CasparCG: launcher host not configured',
97+
[CasparCGActionErrorCode.RESTART_LAUNCHER_PORT_NOT_SET]: 'Cannot restart CasparCG: launcher port not configured',
98+
[CasparCGActionErrorCode.RESTART_LAUNCHER_PROCESS_NOT_SET]:
99+
'Cannot restart CasparCG: launcher process not configured',
100+
[CasparCGActionErrorCode.RESTART_BAD_REPLY]: 'CasparCG restart failed: launcher returned {{statusCode}} {{body}}',
101+
[CasparCGActionErrorCode.RESTART_REQUEST_FAILED]: 'CasparCG restart failed: {{errorMessage}}',
102+
103+
[CasparCGActionErrorCode.LIST_NOT_INITIALIZED]: 'Cannot list CasparCG media: device not initialized',
104+
[CasparCGActionErrorCode.LIST_CLS_ERROR]: 'CasparCG media list failed: {{errorMessage}}',
105+
[CasparCGActionErrorCode.LIST_BAD_RESPONSE]: 'CasparCG media list failed: server returned error {{responseCode}}',
106+
}
107+
108+
/**
109+
* Context data for each CasparCG action error code.
110+
* These fields are available for message template interpolation.
111+
*/
112+
export interface CasparCGActionErrorContextMap {
113+
[CasparCGActionErrorCode.CLEAR_NO_CONNECTION]: Record<string, never>
114+
[CasparCGActionErrorCode.CLEAR_INFO_FAILED]: Record<string, never>
115+
[CasparCGActionErrorCode.CLEAR_NO_CHANNELS]: Record<string, never>
116+
117+
[CasparCGActionErrorCode.RESTART_NOT_INITIALIZED]: Record<string, never>
118+
[CasparCGActionErrorCode.RESTART_LAUNCHER_HOST_NOT_SET]: Record<string, never>
119+
[CasparCGActionErrorCode.RESTART_LAUNCHER_PORT_NOT_SET]: Record<string, never>
120+
[CasparCGActionErrorCode.RESTART_LAUNCHER_PROCESS_NOT_SET]: Record<string, never>
121+
[CasparCGActionErrorCode.RESTART_BAD_REPLY]: { statusCode: number; body: string }
122+
[CasparCGActionErrorCode.RESTART_REQUEST_FAILED]: { errorMessage: string }
123+
124+
[CasparCGActionErrorCode.LIST_NOT_INITIALIZED]: Record<string, never>
125+
[CasparCGActionErrorCode.LIST_CLS_ERROR]: { errorMessage: string }
126+
[CasparCGActionErrorCode.LIST_BAD_RESPONSE]: { responseCode: number }
127+
}
128+
46129
export enum TimelineContentTypeCasparCg {
47130
// CasparCG-state
48131
MEDIA = 'media',

packages/timeline-state-resolver/src/integrations/casparCG/index.ts

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
CasparCGStatusCode,
3535
DeviceStatusDetail,
3636
DeviceStatusInput,
37+
CasparCGActionErrorCode,
3738
} from 'timeline-state-resolver-types'
3839
import { createCasparCGStatusDetail } from './messages.js'
3940

@@ -633,17 +634,27 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
633634
if (!this._ccg?.connected) {
634635
return {
635636
result: ActionExecutionResultCode.Error,
636-
response: t('Cannot restart CasparCG without a connection'),
637+
code: CasparCGActionErrorCode.CLEAR_NO_CONNECTION,
638+
context: {},
639+
response: t('Cannot clear CasparCG channels: no connection'),
637640
}
638641
}
639642

640643
const { error, request } = await this._ccg.executeCommand({ command: Commands.Info, params: {} })
641644
if (error) {
642-
return { result: ActionExecutionResultCode.Error }
645+
return {
646+
result: ActionExecutionResultCode.Error,
647+
code: CasparCGActionErrorCode.CLEAR_INFO_FAILED,
648+
context: {},
649+
}
643650
}
644651
const response = await request
645652
if (!response?.data[0]) {
646-
return { result: ActionExecutionResultCode.Error }
653+
return {
654+
result: ActionExecutionResultCode.Error,
655+
code: CasparCGActionErrorCode.CLEAR_NO_CHANNELS,
656+
context: {},
657+
}
647658
}
648659

649660
await Promise.all(
@@ -697,18 +708,35 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
697708
*/
698709
private async restartCasparCG(): Promise<ActionExecutionResult> {
699710
if (!this.initOptions) {
700-
return { result: ActionExecutionResultCode.Error, response: t('CasparCGDevice._connectionOptions is not set!') }
711+
return {
712+
result: ActionExecutionResultCode.Error,
713+
code: CasparCGActionErrorCode.RESTART_NOT_INITIALIZED,
714+
context: {},
715+
response: t('Cannot restart CasparCG: device not initialized'),
716+
}
701717
}
702718
if (!this.initOptions.launcherHost) {
703-
return { result: ActionExecutionResultCode.Error, response: t('CasparCGDevice: config.launcherHost is not set!') }
719+
return {
720+
result: ActionExecutionResultCode.Error,
721+
code: CasparCGActionErrorCode.RESTART_LAUNCHER_HOST_NOT_SET,
722+
context: {},
723+
response: t('Cannot restart CasparCG: launcher host not configured'),
724+
}
704725
}
705726
if (!this.initOptions.launcherPort) {
706-
return { result: ActionExecutionResultCode.Error, response: t('CasparCGDevice: config.launcherPort is not set!') }
727+
return {
728+
result: ActionExecutionResultCode.Error,
729+
code: CasparCGActionErrorCode.RESTART_LAUNCHER_PORT_NOT_SET,
730+
context: {},
731+
response: t('Cannot restart CasparCG: launcher port not configured'),
732+
}
707733
}
708734
if (!this.initOptions.launcherProcess) {
709735
return {
710736
result: ActionExecutionResultCode.Error,
711-
response: t('CasparCGDevice: config.launcherProcess is not set!'),
737+
code: CasparCGActionErrorCode.RESTART_LAUNCHER_PROCESS_NOT_SET,
738+
context: {},
739+
response: t('Cannot restart CasparCG: launcher process not configured'),
712740
}
713741
}
714742

@@ -725,7 +753,9 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
725753
} else {
726754
return {
727755
result: ActionExecutionResultCode.Error,
728-
response: t('Bad reply: [{{statusCode}}] {{body}}', {
756+
code: CasparCGActionErrorCode.RESTART_BAD_REPLY,
757+
context: { statusCode: response.statusCode, body: response.body },
758+
response: t('CasparCG restart failed: launcher returned {{statusCode}} {{body}}', {
729759
statusCode: response.statusCode,
730760
body: response.body,
731761
}),
@@ -735,8 +765,10 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
735765
.catch((error) => {
736766
return {
737767
result: ActionExecutionResultCode.Error,
738-
response: t('{{message}}', {
739-
message: error.toString(),
768+
code: CasparCGActionErrorCode.RESTART_REQUEST_FAILED,
769+
context: { errorMessage: error.toString() },
770+
response: t('CasparCG restart failed: {{errorMessage}}', {
771+
errorMessage: error.toString(),
740772
}),
741773
}
742774
})
@@ -745,7 +777,9 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
745777
if (!this._ccg) {
746778
return {
747779
result: ActionExecutionResultCode.Error,
748-
response: t('CasparCG device not initialized'),
780+
code: CasparCGActionErrorCode.LIST_NOT_INITIALIZED,
781+
context: {},
782+
response: t('Cannot list CasparCG media: device not initialized'),
749783
}
750784
}
751785
const result = await this._ccg.executeCommand(
@@ -757,7 +791,9 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
757791
if (result.error)
758792
return {
759793
result: ActionExecutionResultCode.Error,
760-
response: t(`Error message from CasparCG: {{message}}`, { message: `${result.error}` }),
794+
code: CasparCGActionErrorCode.LIST_CLS_ERROR,
795+
context: { errorMessage: `${result.error}` },
796+
response: t(`CasparCG media list failed: {{errorMessage}}`, { errorMessage: `${result.error}` }),
761797
}
762798

763799
const request = await result.request
@@ -770,7 +806,11 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
770806
} else {
771807
return {
772808
result: ActionExecutionResultCode.Error,
773-
response: t(`Error code {{code}} from CasparCG`, { code: request.responseCode }),
809+
code: CasparCGActionErrorCode.LIST_BAD_RESPONSE,
810+
context: { responseCode: request.responseCode },
811+
response: t(`CasparCG media list failed: server returned error {{responseCode}}`, {
812+
responseCode: request.responseCode,
813+
}),
774814
}
775815
}
776816
}

0 commit comments

Comments
 (0)