From f3eaf431d1bb40a12688c5406c1ee77526759c46 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:54:58 +0100 Subject: [PATCH 1/2] Add t-timers info to LSG --- .../activePlaylistEvent-example.yaml | 15 ++ .../activePlaylistEvent.yaml | 9 +- .../api/components/tTimers/tTimerIndex.yaml | 6 + .../tTimers/tTimerMode/tTimerMode.yaml | 55 ++++ .../tTimerModeCountdown-example.yaml | 5 + .../tTimerMode/tTimerModeFreeRun-example.yaml | 3 + .../tTimerStatus/tTimerStatus-example.yaml | 9 + .../tTimers/tTimerStatus/tTimerStatus.yaml | 23 ++ .../src/generated/asyncapi.yaml | 243 +++++++++++++----- .../src/generated/schema.ts | 77 ++++++ .../topics/__tests__/activePlaylist.spec.ts | 96 +++++++ .../src/topics/__tests__/utils.ts | 6 +- .../src/topics/activePlaylistTopic.ts | 67 +++++ 13 files changed, 552 insertions(+), 62 deletions(-) create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml diff --git a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml index d09a8222ef3..05ef11767ac 100644 --- a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml +++ b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml @@ -15,3 +15,18 @@ timing: $ref: '../../timing/activePlaylistTiming/activePlaylistTiming-example.yaml' quickLoop: $ref: '../../quickLoop/activePlaylistQuickLoop/activePlaylistQuickLoop-example.yaml' +tTimers: + - index: 1 + label: 'On Air Timer' + configured: true + mode: + $ref: '../../tTimers/tTimerMode/tTimerModeCountdown-example.yaml' + - index: 2 + label: '' + configured: false + mode: null + - index: 3 + label: 'Studio Clock' + configured: true + mode: + $ref: '../../tTimers/tTimerMode/tTimerModeFreeRun-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml index c41fed04c05..48ccc7a8fd2 100644 --- a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml +++ b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml @@ -44,7 +44,14 @@ $defs: $ref: '../../timing/activePlaylistTiming/activePlaylistTiming.yaml#/$defs/activePlaylistTiming' quickLoop: $ref: '../../quickLoop/activePlaylistQuickLoop/activePlaylistQuickLoop.yaml#/$defs/activePlaylistQuickLoop' - required: [event, id, externalId, name, rundownIds, currentPart, currentSegment, nextPart, timing] + tTimers: + description: T-timers for the playlist. Always contains 3 elements (one for each timer slot). + type: array + items: + $ref: '../../tTimers/tTimerStatus/tTimerStatus.yaml#/$defs/tTimerStatus' + minItems: 3 + maxItems: 3 + required: [event, id, externalId, name, rundownIds, currentPart, currentSegment, nextPart, timing, tTimers] additionalProperties: false examples: - $ref: './activePlaylistEvent-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml new file mode 100644 index 00000000000..aab940cecd5 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml @@ -0,0 +1,6 @@ +$defs: + tTimerIndex: + type: integer + title: TTimerIndex + description: Timer index (1-3). The playlist always has 3 T-timer slots. + enum: [1, 2, 3] diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml new file mode 100644 index 00000000000..12c29a0740a --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml @@ -0,0 +1,55 @@ +$defs: + tTimerModeCountdown: + type: object + title: TTimerModeCountdown + description: Countdown timer mode - counts down from a duration + properties: + type: + type: string + const: countdown + startTime: + description: Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + type: number + pauseTime: + description: Unix timestamp when paused (milliseconds), or null if running + oneOf: + - type: number + - type: 'null' + durationMs: + description: Total countdown duration in milliseconds + type: number + stopAtZero: + description: Whether timer stops at zero or continues into negative values + type: boolean + required: [type, startTime, pauseTime, durationMs, stopAtZero] + additionalProperties: false + examples: + - $ref: './tTimerModeCountdown-example.yaml' + + tTimerModeFreeRun: + type: object + title: TTimerModeFreeRun + description: Free-running timer mode - counts up from start time + properties: + type: + type: string + const: freeRun + startTime: + description: Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + type: number + pauseTime: + description: Unix timestamp when paused (milliseconds), or null if running + oneOf: + - type: number + - type: 'null' + required: [type, startTime, pauseTime] + additionalProperties: false + examples: + - $ref: './tTimerModeFreeRun-example.yaml' + + tTimerMode: + title: TTimerMode + description: The mode/state of a T-timer + oneOf: + - $ref: '#/$defs/tTimerModeCountdown' + - $ref: '#/$defs/tTimerModeFreeRun' diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml new file mode 100644 index 00000000000..3f76f9cfdf8 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml @@ -0,0 +1,5 @@ +type: countdown +startTime: 1706371800000 +pauseTime: null +durationMs: 120000 +stopAtZero: true diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml new file mode 100644 index 00000000000..34cf8b5671a --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml @@ -0,0 +1,3 @@ +type: freeRun +startTime: 1706371900000 +pauseTime: 1706372000000 diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml new file mode 100644 index 00000000000..6a95e8daa1c --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml @@ -0,0 +1,9 @@ +index: 1 +label: 'Segment Timer' +configured: true +mode: + type: countdown + startTime: 1706371800000 + pauseTime: null + durationMs: 120000 + stopAtZero: true diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml new file mode 100644 index 00000000000..31007f3f86b --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml @@ -0,0 +1,23 @@ +$defs: + tTimerStatus: + type: object + title: TTimerStatus + description: Status of a single T-timer in the playlist + properties: + index: + $ref: '../tTimerIndex.yaml#/$defs/tTimerIndex' + label: + description: User-defined label for the timer + type: string + configured: + description: Whether the timer has been configured (mode is not null) + type: boolean + mode: + description: Timer mode and timing state. Null if not configured. + oneOf: + - type: 'null' + - $ref: '../tTimerMode/tTimerMode.yaml#/$defs/tTimerMode' + required: [index, label, configured] + additionalProperties: false + examples: + - $ref: './tTimerStatus-example.yaml' diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index b747e97a84c..a83a698b6f8 100644 --- a/packages/live-status-gateway-api/src/generated/asyncapi.yaml +++ b/packages/live-status-gateway-api/src/generated/asyncapi.yaml @@ -401,7 +401,7 @@ channels: pieces: description: All pieces in this part type: array - items: &a30 + items: &a32 type: object title: PieceStatus properties: @@ -510,7 +510,7 @@ channels: - type: object title: CurrentSegment allOf: - - &a32 + - &a34 title: SegmentBase type: object properties: @@ -529,7 +529,7 @@ channels: title: CurrentSegmentTiming description: Timing information about the current segment allOf: - - &a33 + - &a35 type: object title: SegmentTiming properties: @@ -709,6 +709,115 @@ channels: running: true start: *a23 end: *a23 + tTimers: + description: T-timers for the playlist. Always contains 3 elements (one for each + timer slot). + type: array + items: + type: object + title: TTimerStatus + description: Status of a single T-timer in the playlist + properties: + index: + type: integer + title: TTimerIndex + description: Timer index (1-3). The playlist always has 3 T-timer slots. + enum: + - 1 + - 2 + - 3 + label: + description: User-defined label for the timer + type: string + configured: + description: Whether the timer has been configured (mode is not null) + type: boolean + mode: + description: Timer mode and timing state. Null if not configured. + oneOf: + - type: "null" + - title: TTimerMode + description: The mode/state of a T-timer + oneOf: + - type: object + title: TTimerModeCountdown + description: Countdown timer mode - counts down from a duration + properties: + type: + type: string + const: countdown + startTime: + description: Unix timestamp when timer started (milliseconds). May be adjusted + when pausing/resuming. + type: number + pauseTime: + description: Unix timestamp when paused (milliseconds), or null if running + oneOf: + - type: number + - type: "null" + durationMs: + description: Total countdown duration in milliseconds + type: number + stopAtZero: + description: Whether timer stops at zero or continues into negative values + type: boolean + required: + - type + - startTime + - pauseTime + - durationMs + - stopAtZero + additionalProperties: false + examples: + - &a29 + type: countdown + startTime: 1706371800000 + pauseTime: null + durationMs: 120000 + stopAtZero: true + - type: object + title: TTimerModeFreeRun + description: Free-running timer mode - counts up from start time + properties: + type: + type: string + const: freeRun + startTime: + description: Unix timestamp when timer started (milliseconds). May be adjusted + when pausing/resuming. + type: number + pauseTime: + description: Unix timestamp when paused (milliseconds), or null if running + oneOf: + - type: number + - type: "null" + required: + - type + - startTime + - pauseTime + additionalProperties: false + examples: + - &a30 + type: freeRun + startTime: 1706371900000 + pauseTime: 1706372000000 + required: + - index + - label + - configured + additionalProperties: false + examples: + - index: 1 + label: Segment Timer + configured: true + mode: + type: countdown + startTime: 1706371800000 + pauseTime: null + durationMs: 120000 + stopAtZero: true + minItems: 3 + maxItems: 3 required: - event - id @@ -719,9 +828,10 @@ channels: - currentSegment - nextPart - timing + - tTimers additionalProperties: false examples: - - &a29 + - &a31 event: activePlaylist id: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ externalId: 1ZIYVYL1aEkNEJbeGsmRXr5s8wtkyxfPRjNSTxZfcoEI @@ -735,8 +845,21 @@ channels: category: Evening News timing: *a27 quickLoop: *a28 + tTimers: + - index: 1 + label: On Air Timer + configured: true + mode: *a29 + - index: 2 + label: "" + configured: false + mode: null + - index: 3 + label: Studio Clock + configured: true + mode: *a30 examples: - - payload: *a29 + - payload: *a31 activePieces: description: Topic for active pieces updates subscribe: @@ -761,20 +884,20 @@ channels: activePieces: description: Pieces that are currently active (on air) type: array - items: *a30 + items: *a32 required: - event - rundownPlaylistId - activePieces additionalProperties: false examples: - - &a31 + - &a33 event: activePieces rundownPlaylistId: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ activePieces: - *a13 examples: - - payload: *a31 + - payload: *a33 segments: description: Topic for Segment updates subscribe: @@ -803,7 +926,7 @@ channels: type: object title: Segment allOf: - - *a32 + - *a34 - type: object title: Segment properties: @@ -817,7 +940,7 @@ channels: name: description: Name of the segment type: string - timing: *a33 + timing: *a35 publicData: description: Optional arbitrary data required: @@ -830,7 +953,7 @@ channels: - name - timing examples: - - &a34 + - &a36 identifier: Segment 0 identifier rundownId: y9HauyWkcxQS3XaAOsW40BRLLsI_ name: Segment 0 @@ -846,13 +969,13 @@ channels: - rundownPlaylistId - segments examples: - - &a35 + - &a37 event: segments rundownPlaylistId: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ segments: - - *a34 + - *a36 examples: - - payload: *a35 + - payload: *a37 adLibs: description: Topic for AdLibs updates subscribe: @@ -882,7 +1005,7 @@ channels: items: title: AdLibStatus allOf: - - &a40 + - &a42 title: AdLibBase type: object properties: @@ -917,7 +1040,7 @@ channels: - label additionalProperties: false examples: - - &a36 + - &a38 name: pvw label: Preview tags: @@ -937,15 +1060,15 @@ channels: - sourceLayer - actionType examples: - - &a41 + - &a43 id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: Music video clip sourceLayer: Video Clip - actionType: &a37 - - *a36 - tags: &a38 + actionType: &a39 + - *a38 + tags: &a40 - music_video - publicData: &a39 + publicData: &a41 fileName: MV000123.mxf optionsSchema: '{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Play Video @@ -977,15 +1100,15 @@ channels: - segmentId - partId examples: - - &a42 + - &a44 segmentId: HsD8_QwE1ZmR5vN3XcK_Ab7y partId: JkL3_OpR6WxT1bF8Vq2_Zy9u id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: Music video clip sourceLayer: Video Clip - actionType: *a37 - tags: *a38 - publicData: *a39 + actionType: *a39 + tags: *a40 + publicData: *a41 optionsSchema: '{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Play Video Clip","type":"object","properties":{"type":"adlib_action_video_clip","label":{"type":"string"},"clipId":{"type":"string"},"vo":{"type":"boolean"},"target":{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Object @@ -1009,9 +1132,9 @@ channels: items: title: GlobalAdLibStatus allOf: - - *a40 + - *a42 examples: - - *a41 + - *a43 required: - event - rundownPlaylistId @@ -1019,15 +1142,15 @@ channels: - globalAdLibs additionalProperties: false examples: - - &a43 + - &a45 event: adLibs rundownPlaylistId: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ adLibs: - - *a42 + - *a44 globalAdLibs: - - *a41 + - *a43 examples: - - payload: *a43 + - payload: *a45 packages: description: Packages topic for websocket subscriptions. Packages are assets that need to be prepared by Sofie Package Manager or third-party systems @@ -1135,7 +1258,7 @@ channels: - pieceOrAdLibId additionalProperties: false examples: - - &a44 + - &a46 packageName: MV000123.mxf status: ok rundownId: y9HauyWkcxQS3XaAOsW40BRLLsI_ @@ -1153,7 +1276,7 @@ channels: - event: packages rundownPlaylistId: y9HauyWkcxQS3XaAOsW40BRLLsI_ packages: - - *a44 + - *a46 buckets: description: Buckets schema for websocket subscriptions subscribe: @@ -1189,7 +1312,7 @@ channels: items: title: BucketAdLibStatus allOf: - - *a40 + - *a42 - type: object title: BucketAdLibStatus properties: @@ -1200,14 +1323,14 @@ channels: required: - externalId examples: - - &a45 + - &a47 externalId: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: Music video clip sourceLayer: Video Clip - actionType: *a37 - tags: *a38 - publicData: *a39 + actionType: *a39 + tags: *a40 + publicData: *a41 optionsSchema: '{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Play Video Clip","type":"object","properties":{"type":"adlib_action_video_clip","label":{"type":"string"},"clipId":{"type":"string"},"vo":{"type":"boolean"},"target":{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Object @@ -1231,22 +1354,22 @@ channels: - adLibs additionalProperties: false examples: - - &a46 + - &a48 id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: My Bucket adLibs: - - *a45 + - *a47 required: - event - buckets additionalProperties: false examples: - - &a47 + - &a49 event: buckets buckets: - - *a46 + - *a48 examples: - - payload: *a47 + - payload: *a49 notifications: description: Notifications topic for websocket subscriptions. subscribe: @@ -1310,7 +1433,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: &a48 + enum: &a50 - rundown - playlist - partInstance @@ -1322,7 +1445,7 @@ channels: type: string additionalProperties: false examples: - - &a49 + - &a51 type: rundown studioId: studio01 rundownId: rd123 @@ -1338,14 +1461,14 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 studioId: type: string playlistId: type: string additionalProperties: false examples: - - &a50 + - &a52 type: playlist studioId: studio01 playlistId: pl456 @@ -1362,7 +1485,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 studioId: type: string rundownId: @@ -1371,7 +1494,7 @@ channels: type: string additionalProperties: false examples: - - &a51 + - &a53 type: partInstance studioId: studio01 rundownId: rd123 @@ -1390,7 +1513,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 studioId: type: string rundownId: @@ -1401,7 +1524,7 @@ channels: type: string additionalProperties: false examples: - - &a52 + - &a54 type: pieceInstance studioId: studio01 rundownId: rd123 @@ -1417,17 +1540,17 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 additionalProperties: false examples: - - &a53 + - &a55 type: unknown examples: - - *a49 - - *a50 - *a51 - *a52 - *a53 + - *a54 + - *a55 created: type: integer format: int64 @@ -1438,11 +1561,11 @@ channels: description: Unix timestamp of last modification additionalProperties: false examples: - - &a54 + - &a56 _id: notif123 severity: error message: disk.space.low - relatedTo: *a52 + relatedTo: *a54 created: 1694784932 modified: 1694784950 required: @@ -1450,9 +1573,9 @@ channels: - activeNotifications additionalProperties: false examples: - - &a55 + - &a57 event: notifications activeNotifications: - - *a54 + - *a56 examples: - - payload: *a55 + - payload: *a57 diff --git a/packages/live-status-gateway-api/src/generated/schema.ts b/packages/live-status-gateway-api/src/generated/schema.ts index b8f0970662a..8a53ce7033d 100644 --- a/packages/live-status-gateway-api/src/generated/schema.ts +++ b/packages/live-status-gateway-api/src/generated/schema.ts @@ -186,6 +186,10 @@ interface ActivePlaylistEvent { * Information about the current quickLoop, if any */ quickLoop?: ActivePlaylistQuickLoop + /** + * T-timers for the playlist. Always contains 3 elements (one for each timer slot). + */ + tTimers: TTimerStatus[] } interface CurrentPartStatus { @@ -454,6 +458,75 @@ enum QuickLoopMarkerType { PART = 'part', } +/** + * Status of a single T-timer in the playlist + */ +interface TTimerStatus { + /** + * Timer index (1-3). The playlist always has 3 T-timer slots. + */ + index: TTimerIndex + /** + * User-defined label for the timer + */ + label: string + /** + * Whether the timer has been configured (mode is not null) + */ + configured: boolean + /** + * Timer mode and timing state. Null if not configured. + */ + mode?: TTimerModeCountdown | TTimerModeFreeRun | null +} + +/** + * Timer index (1-3). The playlist always has 3 T-timer slots. + */ +enum TTimerIndex { + NUMBER_1 = 1, + NUMBER_2 = 2, + NUMBER_3 = 3, +} + +/** + * Countdown timer mode - counts down from a duration + */ +interface TTimerModeCountdown { + type: 'countdown' + /** + * Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + */ + startTime: number + /** + * Unix timestamp when paused (milliseconds), or null if running + */ + pauseTime: number | null + /** + * Total countdown duration in milliseconds + */ + durationMs: number + /** + * Whether timer stops at zero or continues into negative values + */ + stopAtZero: boolean +} + +/** + * Free-running timer mode - counts up from start time + */ +interface TTimerModeFreeRun { + type: 'freeRun' + /** + * Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + */ + startTime: number + /** + * Unix timestamp when paused (milliseconds), or null if running + */ + pauseTime: number | null +} + interface ActivePiecesEvent { event: 'activePieces' /** @@ -924,6 +997,10 @@ export { ActivePlaylistQuickLoop, QuickLoopMarker, QuickLoopMarkerType, + TTimerStatus, + TTimerIndex, + TTimerModeCountdown, + TTimerModeFreeRun, ActivePiecesEvent, SegmentsEvent, Segment, diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 702ee867c6d..8e08f69837a 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -19,6 +19,7 @@ import { ActivePlaylistEvent, ActivePlaylistTimingMode, SegmentCountdownType, + TTimerIndex, } from '@sofie-automation/live-status-gateway-api' function makeEmptyTestPartInstances(): SelectedPartInstances { @@ -63,6 +64,11 @@ describe('ActivePlaylistTopic', () => { timingMode: ActivePlaylistTimingMode.NONE, }, quickLoop: undefined, + tTimers: [ + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + ], } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -164,6 +170,11 @@ describe('ActivePlaylistTopic', () => { timingMode: ActivePlaylistTimingMode.NONE, }, quickLoop: undefined, + tTimers: [ + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + ], } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -270,6 +281,11 @@ describe('ActivePlaylistTopic', () => { timingMode: ActivePlaylistTimingMode.NONE, }, quickLoop: undefined, + tTimers: [ + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + ], } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -278,4 +294,84 @@ describe('ActivePlaylistTopic', () => { JSON.parse(JSON.stringify(expectedStatus)) ) }) + + it('transforms configured T-timers correctly', async () => { + const handlers = makeMockHandlers() + const topic = new ActivePlaylistTopic(makeMockLogger(), handlers) + const mockSubscriber = makeMockSubscriber() + + const playlist = makeTestPlaylist() + playlist.activationId = protectString('somethingRandom') + // Configure timers with different modes + playlist.tTimers = [ + { + index: 1, + label: 'Countdown Timer', + mode: { + type: 'countdown', + startTime: 1600000000000, + pauseTime: null, + duration: 60000, + stopAtZero: true, + }, + }, + { + index: 2, + label: 'Paused FreeRun', + mode: { + type: 'freeRun', + startTime: 1600000010000, + pauseTime: 1600000020000, + }, + }, + { index: 3, label: '', mode: null }, + ] + handlers.playlistHandler.notify(playlist) + + const testShowStyleBase = makeTestShowStyleBase() + handlers.showStyleBaseHandler.notify(testShowStyleBase as ShowStyleBaseExt) + + const testPartInstancesMap = makeEmptyTestPartInstances() + handlers.partInstancesHandler.notify(testPartInstancesMap) + + topic.addSubscriber(mockSubscriber) + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockSubscriber.send).toHaveBeenCalledTimes(1) + const receivedStatus = JSON.parse(mockSubscriber.send.mock.calls[0][0] as string) as ActivePlaylistEvent + + // Verify countdown timer transformation + expect(receivedStatus.tTimers[0]).toEqual({ + index: TTimerIndex.NUMBER_1, + label: 'Countdown Timer', + configured: true, + mode: { + type: 'countdown', + startTime: 1600000000000, + pauseTime: null, + durationMs: 60000, + stopAtZero: true, + }, + }) + + // Verify paused freeRun timer transformation + expect(receivedStatus.tTimers[1]).toEqual({ + index: TTimerIndex.NUMBER_2, + label: 'Paused FreeRun', + configured: true, + mode: { + type: 'freeRun', + startTime: 1600000010000, + pauseTime: 1600000020000, + }, + }) + + // Verify unconfigured timer + expect(receivedStatus.tTimers[2]).toEqual({ + index: TTimerIndex.NUMBER_3, + label: '', + configured: false, + mode: null, + }) + }) }) diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 23b70507c10..444acd33856 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -34,7 +34,11 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { studioId: protectString('STUDIO_1'), timing: { type: PlaylistTimingType.None }, publicData: { a: 'b' }, - tTimers: [] as any, + tTimers: [ + { index: 1, label: '', mode: null }, + { index: 2, label: '', mode: null }, + { index: 3, label: '', mode: null }, + ], } } diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index f1f29c940d4..0b6f857be98 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -5,6 +5,7 @@ import { DBRundownPlaylist, QuickLoopMarker, QuickLoopMarkerType, + RundownTTimer, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { assertNever, literal } from '@sofie-automation/shared-lib/dist/lib/lib' @@ -30,6 +31,8 @@ import { ActivePlaylistQuickLoop, QuickLoopMarker as QuickLoopMarkerStatus, QuickLoopMarkerType as QuickLoopMarkerStatusType, + TTimerStatus, + TTimerIndex, } from '@sofie-automation/live-status-gateway-api' import { CollectionHandlers } from '../liveStatusServer.js' @@ -50,6 +53,7 @@ const PLAYLIST_KEYS = [ 'timing', 'startedPlayback', 'quickLoop', + 'tTimers', ] as const type Playlist = PickKeys @@ -170,6 +174,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket ? this._activePlaylist.timing.expectedEnd : undefined, }, + tTimers: this.transformTTimers(this._activePlaylist.tTimers), }) : literal({ event: 'activePlaylist', @@ -185,6 +190,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket timing: { timingMode: ActivePlaylistTimingMode.NONE, }, + tTimers: this.transformTTimers(undefined), }) this.sendMessage(subscribers, message) @@ -204,6 +210,67 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket } } + /** + * Transform T-timers from database format to API status format + */ + private transformTTimers( + tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] | undefined + ): [TTimerStatus, TTimerStatus, TTimerStatus] { + if (!tTimers) { + // Return 3 unconfigured timers when no playlist is active + return [ + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + ] + } + + return [this.transformTTimer(tTimers[0]), this.transformTTimer(tTimers[1]), this.transformTTimer(tTimers[2])] + } + + /** + * Transform a single T-timer from database format to API status format + */ + private transformTTimer(timer: RundownTTimer): TTimerStatus { + const index = + timer.index === 1 ? TTimerIndex.NUMBER_1 : timer.index === 2 ? TTimerIndex.NUMBER_2 : TTimerIndex.NUMBER_3 + + if (!timer.mode) { + return { + index, + label: timer.label, + configured: false, + mode: null, + } + } + + if (timer.mode.type === 'countdown') { + return { + index, + label: timer.label, + configured: true, + mode: { + type: 'countdown', + startTime: timer.mode.startTime, + pauseTime: timer.mode.pauseTime, + durationMs: timer.mode.duration, + stopAtZero: timer.mode.stopAtZero, + }, + } + } else { + return { + index, + label: timer.label, + configured: true, + mode: { + type: 'freeRun', + startTime: timer.mode.startTime, + pauseTime: timer.mode.pauseTime, + }, + } + } + } + private transformQuickLoopMarkerStatus(marker: QuickLoopMarker | undefined): QuickLoopMarkerStatus | undefined { if (!marker) return undefined From 0e30c11e005799a8cbf2a65de2d6964a0445cc58 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Tue, 10 Feb 2026 05:24:11 +0100 Subject: [PATCH 2/2] SOFIE-261 | add estimate state info to LSG topic about t-timers --- .../tTimers/tTimerMode/tTimerMode.yaml | 44 +++++---- .../tTimerModeCountdown-example.yaml | 4 +- .../tTimerMode/tTimerModeFreeRun-example.yaml | 4 +- .../tTimerStatus/tTimerStatus-example.yaml | 8 +- .../tTimers/tTimerStatus/tTimerStatus.yaml | 28 ++++++ .../src/generated/asyncapi.yaml | 89 +++++++++++++------ .../src/generated/schema.ts | 53 +++++++++-- .../topics/__tests__/activePlaylist.spec.ts | 49 ++++++---- .../src/topics/__tests__/utils.ts | 6 +- .../src/topics/activePlaylistTopic.ts | 79 ++++++++++++---- 10 files changed, 267 insertions(+), 97 deletions(-) diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml index 12c29a0740a..1cb36d05a7a 100644 --- a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml @@ -7,21 +7,26 @@ $defs: type: type: string const: countdown - startTime: - description: Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + paused: + description: Whether the timer is currently paused + type: boolean + zeroTime: + description: >- + Unix timestamp (ms) when the timer reaches/reached zero. + Present when paused is false. The client calculates remaining time as zeroTime - Date.now(). + type: number + remainingMs: + description: >- + Frozen remaining duration in milliseconds. + Present when paused is true. type: number - pauseTime: - description: Unix timestamp when paused (milliseconds), or null if running - oneOf: - - type: number - - type: 'null' durationMs: - description: Total countdown duration in milliseconds + description: Total countdown duration in milliseconds (the original configured duration) type: number stopAtZero: description: Whether timer stops at zero or continues into negative values type: boolean - required: [type, startTime, pauseTime, durationMs, stopAtZero] + required: [type, paused, durationMs, stopAtZero] additionalProperties: false examples: - $ref: './tTimerModeCountdown-example.yaml' @@ -34,15 +39,20 @@ $defs: type: type: string const: freeRun - startTime: - description: Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + paused: + description: Whether the timer is currently paused + type: boolean + zeroTime: + description: >- + Unix timestamp (ms) when the timer was at zero (i.e. when it was started). + Present when paused is false. The client calculates elapsed time as Date.now() - zeroTime. + type: number + elapsedMs: + description: >- + Frozen elapsed time in milliseconds. + Present when paused is true. type: number - pauseTime: - description: Unix timestamp when paused (milliseconds), or null if running - oneOf: - - type: number - - type: 'null' - required: [type, startTime, pauseTime] + required: [type, paused] additionalProperties: false examples: - $ref: './tTimerModeFreeRun-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml index 3f76f9cfdf8..bcc642bbe7f 100644 --- a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml @@ -1,5 +1,5 @@ type: countdown -startTime: 1706371800000 -pauseTime: null +paused: false +zeroTime: 1706371920000 durationMs: 120000 stopAtZero: true diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml index 34cf8b5671a..1cad209ada8 100644 --- a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml @@ -1,3 +1,3 @@ type: freeRun -startTime: 1706371900000 -pauseTime: 1706372000000 +paused: false +zeroTime: 1706371800000 diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml index 6a95e8daa1c..03e3f3e8340 100644 --- a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml @@ -3,7 +3,11 @@ label: 'Segment Timer' configured: true mode: type: countdown - startTime: 1706371800000 - pauseTime: null + paused: false + zeroTime: 1706371920000 durationMs: 120000 stopAtZero: true +estimate: + paused: false + zeroTime: 1706371920000 +anchorPartId: 'part_break_1' diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml index 31007f3f86b..90b17e2e619 100644 --- a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml @@ -17,6 +17,34 @@ $defs: oneOf: - type: 'null' - $ref: '../tTimerMode/tTimerMode.yaml#/$defs/tTimerMode' + estimate: + description: >- + Estimated timing for when we expect to reach an anchor part. + Used to calculate over/under diff + oneOf: + - type: 'null' + - type: object + title: TTimerEstimate + description: >- + Estimate timing state for a T-timer + properties: + paused: + description: Whether the estimate is frozen + type: boolean + zeroTime: + description: >- + Unix timestamp in milliseconds of estimated arrival at the anchor part + type: number + durationMs: + description: >- + Frozen remaining duration estimate in milliseconds + type: number + required: [paused] + additionalProperties: false + anchorPartId: + description: >- + The Part ID that this timer is counting towards (the timing anchor) + type: string required: [index, label, configured] additionalProperties: false examples: diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index a83a698b6f8..2e0d68e2190 100644 --- a/packages/live-status-gateway-api/src/generated/asyncapi.yaml +++ b/packages/live-status-gateway-api/src/generated/asyncapi.yaml @@ -746,33 +746,37 @@ channels: type: type: string const: countdown - startTime: - description: Unix timestamp when timer started (milliseconds). May be adjusted - when pausing/resuming. + paused: + description: Whether the timer is currently paused + type: boolean + zeroTime: + description: Unix timestamp (ms) when the timer reaches/reached zero. Present + when paused is false. The client + calculates remaining time as zeroTime - + Date.now(). + type: number + remainingMs: + description: Frozen remaining duration in milliseconds. Present when paused is + true. type: number - pauseTime: - description: Unix timestamp when paused (milliseconds), or null if running - oneOf: - - type: number - - type: "null" durationMs: - description: Total countdown duration in milliseconds + description: Total countdown duration in milliseconds (the original configured + duration) type: number stopAtZero: description: Whether timer stops at zero or continues into negative values type: boolean required: - type - - startTime - - pauseTime + - paused - durationMs - stopAtZero additionalProperties: false examples: - &a29 type: countdown - startTime: 1706371800000 - pauseTime: null + paused: false + zeroTime: 1706371920000 durationMs: 120000 stopAtZero: true - type: object @@ -782,25 +786,52 @@ channels: type: type: string const: freeRun - startTime: - description: Unix timestamp when timer started (milliseconds). May be adjusted - when pausing/resuming. + paused: + description: Whether the timer is currently paused + type: boolean + zeroTime: + description: Unix timestamp (ms) when the timer was at zero (i.e. when it was + started). Present when paused is false. + The client calculates elapsed time as + Date.now() - zeroTime. + type: number + elapsedMs: + description: Frozen elapsed time in milliseconds. Present when paused is true. type: number - pauseTime: - description: Unix timestamp when paused (milliseconds), or null if running - oneOf: - - type: number - - type: "null" required: - type - - startTime - - pauseTime + - paused additionalProperties: false examples: - &a30 type: freeRun - startTime: 1706371900000 - pauseTime: 1706372000000 + paused: false + zeroTime: 1706371800000 + estimate: + description: Estimated timing for when we expect to reach an anchor part. Used + to calculate over/under diff + oneOf: + - type: "null" + - type: object + title: TTimerEstimate + description: Estimate timing state for a T-timer + properties: + paused: + description: Whether the estimate is frozen + type: boolean + zeroTime: + description: Unix timestamp in milliseconds of estimated arrival at the anchor + part + type: number + durationMs: + description: Frozen remaining duration estimate in milliseconds + type: number + required: + - paused + additionalProperties: false + anchorPartId: + description: The Part ID that this timer is counting towards (the timing anchor) + type: string required: - index - label @@ -812,10 +843,14 @@ channels: configured: true mode: type: countdown - startTime: 1706371800000 - pauseTime: null + paused: false + zeroTime: 1706371920000 durationMs: 120000 stopAtZero: true + estimate: + paused: false + zeroTime: 1706371920000 + anchorPartId: part_break_1 minItems: 3 maxItems: 3 required: diff --git a/packages/live-status-gateway-api/src/generated/schema.ts b/packages/live-status-gateway-api/src/generated/schema.ts index 8a53ce7033d..235e374de37 100644 --- a/packages/live-status-gateway-api/src/generated/schema.ts +++ b/packages/live-status-gateway-api/src/generated/schema.ts @@ -478,6 +478,14 @@ interface TTimerStatus { * Timer mode and timing state. Null if not configured. */ mode?: TTimerModeCountdown | TTimerModeFreeRun | null + /** + * Estimated timing for when we expect to reach an anchor part. Used to calculate over/under diff + */ + estimate?: TTimerEstimate | null + /** + * The Part ID that this timer is counting towards (the timing anchor) + */ + anchorPartId?: string } /** @@ -495,15 +503,19 @@ enum TTimerIndex { interface TTimerModeCountdown { type: 'countdown' /** - * Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + * Whether the timer is currently paused */ - startTime: number + paused: boolean /** - * Unix timestamp when paused (milliseconds), or null if running + * Unix timestamp (ms) when the timer reaches/reached zero. Present when paused is false. The client calculates remaining time as zeroTime - Date.now(). */ - pauseTime: number | null + zeroTime?: number /** - * Total countdown duration in milliseconds + * Frozen remaining duration in milliseconds. Present when paused is true. + */ + remainingMs?: number + /** + * Total countdown duration in milliseconds (the original configured duration) */ durationMs: number /** @@ -518,13 +530,35 @@ interface TTimerModeCountdown { interface TTimerModeFreeRun { type: 'freeRun' /** - * Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + * Whether the timer is currently paused */ - startTime: number + paused: boolean + /** + * Unix timestamp (ms) when the timer was at zero (i.e. when it was started). Present when paused is false. The client calculates elapsed time as Date.now() - zeroTime. + */ + zeroTime?: number + /** + * Frozen elapsed time in milliseconds. Present when paused is true. + */ + elapsedMs?: number +} + +/** + * Estimate timing state for a T-timer + */ +interface TTimerEstimate { + /** + * Whether the estimate is frozen + */ + paused: boolean + /** + * Unix timestamp in milliseconds of estimated arrival at the anchor part + */ + zeroTime?: number /** - * Unix timestamp when paused (milliseconds), or null if running + * Frozen remaining duration estimate in milliseconds */ - pauseTime: number | null + durationMs?: number } interface ActivePiecesEvent { @@ -1001,6 +1035,7 @@ export { TTimerIndex, TTimerModeCountdown, TTimerModeFreeRun, + TTimerEstimate, ActivePiecesEvent, SegmentsEvent, Segment, diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 8e08f69837a..7e173d89fb5 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -65,9 +65,9 @@ describe('ActivePlaylistTopic', () => { }, quickLoop: undefined, tTimers: [ - { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, estimate: null }, ], } @@ -171,9 +171,9 @@ describe('ActivePlaylistTopic', () => { }, quickLoop: undefined, tTimers: [ - { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, estimate: null }, ], } @@ -282,9 +282,9 @@ describe('ActivePlaylistTopic', () => { }, quickLoop: undefined, tTimers: [ - { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, estimate: null }, ], } @@ -309,22 +309,23 @@ describe('ActivePlaylistTopic', () => { label: 'Countdown Timer', mode: { type: 'countdown', - startTime: 1600000000000, - pauseTime: null, duration: 60000, stopAtZero: true, }, + state: { paused: false, zeroTime: 1600000060000 }, + estimateState: { paused: false, zeroTime: 1600000060000 }, + anchorPartId: protectString('PART_BREAK'), }, { index: 2, label: 'Paused FreeRun', mode: { type: 'freeRun', - startTime: 1600000010000, - pauseTime: 1600000020000, }, + state: { paused: true, duration: 10000 }, + estimateState: { paused: true, duration: 5000 }, }, - { index: 3, label: '', mode: null }, + { index: 3, label: '', mode: null, state: null }, ] handlers.playlistHandler.notify(playlist) @@ -340,18 +341,23 @@ describe('ActivePlaylistTopic', () => { expect(mockSubscriber.send).toHaveBeenCalledTimes(1) const receivedStatus = JSON.parse(mockSubscriber.send.mock.calls[0][0] as string) as ActivePlaylistEvent - // Verify countdown timer transformation + // Verify running countdown timer transformation expect(receivedStatus.tTimers[0]).toEqual({ index: TTimerIndex.NUMBER_1, label: 'Countdown Timer', configured: true, mode: { type: 'countdown', - startTime: 1600000000000, - pauseTime: null, + paused: false, + zeroTime: 1600000060000, durationMs: 60000, stopAtZero: true, }, + estimate: { + paused: false, + zeroTime: 1600000060000, + }, + anchorPartId: 'PART_BREAK', }) // Verify paused freeRun timer transformation @@ -361,8 +367,12 @@ describe('ActivePlaylistTopic', () => { configured: true, mode: { type: 'freeRun', - startTime: 1600000010000, - pauseTime: 1600000020000, + paused: true, + elapsedMs: 10000, + }, + estimate: { + paused: true, + durationMs: 5000, }, }) @@ -372,6 +382,7 @@ describe('ActivePlaylistTopic', () => { label: '', configured: false, mode: null, + estimate: null, }) }) }) diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 444acd33856..7afa37e1104 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -35,9 +35,9 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { timing: { type: PlaylistTimingType.None }, publicData: { a: 'b' }, tTimers: [ - { index: 1, label: '', mode: null }, - { index: 2, label: '', mode: null }, - { index: 3, label: '', mode: null }, + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, ], } } diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index 0b6f857be98..21563e79487 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -32,6 +32,9 @@ import { QuickLoopMarker as QuickLoopMarkerStatus, QuickLoopMarkerType as QuickLoopMarkerStatusType, TTimerStatus, + TTimerEstimate, + TTimerModeCountdown, + TTimerModeFreeRun, TTimerIndex, } from '@sofie-automation/live-status-gateway-api' @@ -219,9 +222,9 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket if (!tTimers) { // Return 3 unconfigured timers when no playlist is active return [ - { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, estimate: null }, ] } @@ -235,38 +238,82 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket const index = timer.index === 1 ? TTimerIndex.NUMBER_1 : timer.index === 2 ? TTimerIndex.NUMBER_2 : TTimerIndex.NUMBER_3 - if (!timer.mode) { + const estimate = this.transformTimerEstimate(timer.estimateState) + const anchorPartId = timer.anchorPartId ? unprotectString(timer.anchorPartId) : undefined + + if (!timer.mode || !timer.state) { return { index, label: timer.label, configured: false, mode: null, + estimate, + anchorPartId, } } if (timer.mode.type === 'countdown') { + const mode: TTimerModeCountdown = timer.state.paused + ? { + type: 'countdown', + paused: true, + remainingMs: timer.state.duration, + durationMs: timer.mode.duration, + stopAtZero: timer.mode.stopAtZero, + } + : { + type: 'countdown', + paused: false, + zeroTime: timer.state.zeroTime, + durationMs: timer.mode.duration, + stopAtZero: timer.mode.stopAtZero, + } return { index, label: timer.label, configured: true, - mode: { - type: 'countdown', - startTime: timer.mode.startTime, - pauseTime: timer.mode.pauseTime, - durationMs: timer.mode.duration, - stopAtZero: timer.mode.stopAtZero, - }, + mode, + estimate, + anchorPartId, } } else { + const mode: TTimerModeFreeRun = timer.state.paused + ? { + type: 'freeRun', + paused: true, + elapsedMs: timer.state.duration, + } + : { + type: 'freeRun', + paused: false, + zeroTime: timer.state.zeroTime, + } return { index, label: timer.label, configured: true, - mode: { - type: 'freeRun', - startTime: timer.mode.startTime, - pauseTime: timer.mode.pauseTime, - }, + mode, + estimate, + anchorPartId, + } + } + } + + /** + * Transform a TimerState from the data model to a TTimerEstimate for the API + */ + private transformTimerEstimate(estimateState: RundownTTimer['estimateState']): TTimerEstimate | null { + if (!estimateState) return null + + if (estimateState.paused) { + return { + paused: true, + durationMs: estimateState.duration, + } + } else { + return { + paused: false, + zeroTime: estimateState.zeroTime, } } }