Skip to content

Commit fa04a27

Browse files
committed
feat: add structured action error codes to CasparCG device
1 parent b45b825 commit fa04a27

4 files changed

Lines changed: 142 additions & 13 deletions

File tree

packages/timeline-state-resolver-types/src/__tests__/__snapshots__/index.spec.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ exports[`index imports 1`] = `
1212
"AtemTransitionStyle",
1313
"BlendMode",
1414
"BorderBevel",
15+
"CasparCGActionErrorCode",
16+
"CasparCGActionErrorMessages",
1517
"CasparCGActions",
1618
"CasparCGScaleMode",
1719
"CasparCGStatusCode",

packages/timeline-state-resolver-types/src/integrations/casparcg/timeline.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 deviceActionErrorMessages.
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/__tests__/__snapshots__/index.spec.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ exports[`index imports 1`] = `
1313
"AtemTransitionStyle",
1414
"BlendMode",
1515
"BorderBevel",
16+
"CasparCGActionErrorCode",
17+
"CasparCGActionErrorMessages",
1618
"CasparCGActions",
1719
"CasparCGDevice",
1820
"CasparCGScaleMode",

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

Lines changed: 55 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,29 @@ 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+
response: t('Cannot clear CasparCG channels: failed to get channel info'),
650+
}
643651
}
644652
const response = await request
645653
if (!response?.data[0]) {
646-
return { result: ActionExecutionResultCode.Error }
654+
return {
655+
result: ActionExecutionResultCode.Error,
656+
code: CasparCGActionErrorCode.CLEAR_NO_CHANNELS,
657+
context: {},
658+
response: t('Cannot clear CasparCG channels: no channels found'),
659+
}
647660
}
648661

649662
await Promise.all(
@@ -697,18 +710,35 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
697710
*/
698711
private async restartCasparCG(): Promise<ActionExecutionResult> {
699712
if (!this.initOptions) {
700-
return { result: ActionExecutionResultCode.Error, response: t('CasparCGDevice._connectionOptions is not set!') }
713+
return {
714+
result: ActionExecutionResultCode.Error,
715+
code: CasparCGActionErrorCode.RESTART_NOT_INITIALIZED,
716+
context: {},
717+
response: t('Cannot restart CasparCG: device not initialized'),
718+
}
701719
}
702720
if (!this.initOptions.launcherHost) {
703-
return { result: ActionExecutionResultCode.Error, response: t('CasparCGDevice: config.launcherHost is not set!') }
721+
return {
722+
result: ActionExecutionResultCode.Error,
723+
code: CasparCGActionErrorCode.RESTART_LAUNCHER_HOST_NOT_SET,
724+
context: {},
725+
response: t('Cannot restart CasparCG: launcher host not configured'),
726+
}
704727
}
705728
if (!this.initOptions.launcherPort) {
706-
return { result: ActionExecutionResultCode.Error, response: t('CasparCGDevice: config.launcherPort is not set!') }
729+
return {
730+
result: ActionExecutionResultCode.Error,
731+
code: CasparCGActionErrorCode.RESTART_LAUNCHER_PORT_NOT_SET,
732+
context: {},
733+
response: t('Cannot restart CasparCG: launcher port not configured'),
734+
}
707735
}
708736
if (!this.initOptions.launcherProcess) {
709737
return {
710738
result: ActionExecutionResultCode.Error,
711-
response: t('CasparCGDevice: config.launcherProcess is not set!'),
739+
code: CasparCGActionErrorCode.RESTART_LAUNCHER_PROCESS_NOT_SET,
740+
context: {},
741+
response: t('Cannot restart CasparCG: launcher process not configured'),
712742
}
713743
}
714744

@@ -725,7 +755,9 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
725755
} else {
726756
return {
727757
result: ActionExecutionResultCode.Error,
728-
response: t('Bad reply: [{{statusCode}}] {{body}}', {
758+
code: CasparCGActionErrorCode.RESTART_BAD_REPLY,
759+
context: { statusCode: response.statusCode, body: response.body },
760+
response: t('CasparCG restart failed: launcher returned {{statusCode}} {{body}}', {
729761
statusCode: response.statusCode,
730762
body: response.body,
731763
}),
@@ -735,8 +767,10 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
735767
.catch((error) => {
736768
return {
737769
result: ActionExecutionResultCode.Error,
738-
response: t('{{message}}', {
739-
message: error.toString(),
770+
code: CasparCGActionErrorCode.RESTART_REQUEST_FAILED,
771+
context: { errorMessage: error.toString() },
772+
response: t('CasparCG restart failed: {{errorMessage}}', {
773+
errorMessage: error.toString(),
740774
}),
741775
}
742776
})
@@ -745,7 +779,9 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
745779
if (!this._ccg) {
746780
return {
747781
result: ActionExecutionResultCode.Error,
748-
response: t('CasparCG device not initialized'),
782+
code: CasparCGActionErrorCode.LIST_NOT_INITIALIZED,
783+
context: {},
784+
response: t('Cannot list CasparCG media: device not initialized'),
749785
}
750786
}
751787
const result = await this._ccg.executeCommand(
@@ -757,7 +793,9 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
757793
if (result.error)
758794
return {
759795
result: ActionExecutionResultCode.Error,
760-
response: t(`Error message from CasparCG: {{message}}`, { message: `${result.error}` }),
796+
code: CasparCGActionErrorCode.LIST_CLS_ERROR,
797+
context: { errorMessage: `${result.error}` },
798+
response: t(`CasparCG media list failed: {{errorMessage}}`, { errorMessage: `${result.error}` }),
761799
}
762800

763801
const request = await result.request
@@ -770,7 +808,11 @@ export class CasparCGDevice extends DeviceWithState<State, CasparCGDeviceTypes,
770808
} else {
771809
return {
772810
result: ActionExecutionResultCode.Error,
773-
response: t(`Error code {{code}} from CasparCG`, { code: request.responseCode }),
811+
code: CasparCGActionErrorCode.LIST_BAD_RESPONSE,
812+
context: { responseCode: request.responseCode },
813+
response: t(`CasparCG media list failed: server returned error {{responseCode}}`, {
814+
responseCode: request.responseCode,
815+
}),
774816
}
775817
}
776818
}

0 commit comments

Comments
 (0)