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 d09a8222ef..05ef11767a 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 c41fed04c0..48ccc7a8fd 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 0000000000..aab940cecd --- /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 0000000000..1cb36d05a7 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml @@ -0,0 +1,65 @@ +$defs: + tTimerModeCountdown: + type: object + title: TTimerModeCountdown + description: Countdown timer mode - counts down from a duration + properties: + type: + type: string + const: countdown + 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 + durationMs: + 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, paused, 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 + 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 + required: [type, paused] + 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 0000000000..bcc642bbe7 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml @@ -0,0 +1,5 @@ +type: countdown +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 new file mode 100644 index 0000000000..1cad209ada --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml @@ -0,0 +1,3 @@ +type: freeRun +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 new file mode 100644 index 0000000000..03e3f3e834 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml @@ -0,0 +1,13 @@ +index: 1 +label: 'Segment Timer' +configured: true +mode: + type: countdown + 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 new file mode 100644 index 0000000000..90b17e2e61 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml @@ -0,0 +1,51 @@ +$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' + 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: + - $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 b747e97a84..2e0d68e219 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,150 @@ 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 + 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 + durationMs: + 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 + - paused + - durationMs + - stopAtZero + additionalProperties: false + examples: + - &a29 + type: countdown + paused: false + zeroTime: 1706371920000 + durationMs: 120000 + stopAtZero: true + - type: object + title: TTimerModeFreeRun + description: Free-running timer mode - counts up from start time + properties: + type: + type: string + const: freeRun + 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 + required: + - type + - paused + additionalProperties: false + examples: + - &a30 + type: freeRun + 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 + - configured + additionalProperties: false + examples: + - index: 1 + label: Segment Timer + configured: true + mode: + type: countdown + paused: false + zeroTime: 1706371920000 + durationMs: 120000 + stopAtZero: true + estimate: + paused: false + zeroTime: 1706371920000 + anchorPartId: part_break_1 + minItems: 3 + maxItems: 3 required: - event - id @@ -719,9 +863,10 @@ channels: - currentSegment - nextPart - timing + - tTimers additionalProperties: false examples: - - &a29 + - &a31 event: activePlaylist id: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ externalId: 1ZIYVYL1aEkNEJbeGsmRXr5s8wtkyxfPRjNSTxZfcoEI @@ -735,8 +880,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 +919,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 +961,7 @@ channels: type: object title: Segment allOf: - - *a32 + - *a34 - type: object title: Segment properties: @@ -817,7 +975,7 @@ channels: name: description: Name of the segment type: string - timing: *a33 + timing: *a35 publicData: description: Optional arbitrary data required: @@ -830,7 +988,7 @@ channels: - name - timing examples: - - &a34 + - &a36 identifier: Segment 0 identifier rundownId: y9HauyWkcxQS3XaAOsW40BRLLsI_ name: Segment 0 @@ -846,13 +1004,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 +1040,7 @@ channels: items: title: AdLibStatus allOf: - - &a40 + - &a42 title: AdLibBase type: object properties: @@ -917,7 +1075,7 @@ channels: - label additionalProperties: false examples: - - &a36 + - &a38 name: pvw label: Preview tags: @@ -937,15 +1095,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 +1135,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 +1167,9 @@ channels: items: title: GlobalAdLibStatus allOf: - - *a40 + - *a42 examples: - - *a41 + - *a43 required: - event - rundownPlaylistId @@ -1019,15 +1177,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 +1293,7 @@ channels: - pieceOrAdLibId additionalProperties: false examples: - - &a44 + - &a46 packageName: MV000123.mxf status: ok rundownId: y9HauyWkcxQS3XaAOsW40BRLLsI_ @@ -1153,7 +1311,7 @@ channels: - event: packages rundownPlaylistId: y9HauyWkcxQS3XaAOsW40BRLLsI_ packages: - - *a44 + - *a46 buckets: description: Buckets schema for websocket subscriptions subscribe: @@ -1189,7 +1347,7 @@ channels: items: title: BucketAdLibStatus allOf: - - *a40 + - *a42 - type: object title: BucketAdLibStatus properties: @@ -1200,14 +1358,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 +1389,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 +1468,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: &a48 + enum: &a50 - rundown - playlist - partInstance @@ -1322,7 +1480,7 @@ channels: type: string additionalProperties: false examples: - - &a49 + - &a51 type: rundown studioId: studio01 rundownId: rd123 @@ -1338,14 +1496,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 +1520,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 studioId: type: string rundownId: @@ -1371,7 +1529,7 @@ channels: type: string additionalProperties: false examples: - - &a51 + - &a53 type: partInstance studioId: studio01 rundownId: rd123 @@ -1390,7 +1548,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 studioId: type: string rundownId: @@ -1401,7 +1559,7 @@ channels: type: string additionalProperties: false examples: - - &a52 + - &a54 type: pieceInstance studioId: studio01 rundownId: rd123 @@ -1417,17 +1575,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 +1596,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 +1608,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 b8f0970662..235e374de3 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,109 @@ 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 + /** + * 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 +} + +/** + * 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' + /** + * Whether the timer is currently paused + */ + paused: boolean + /** + * Unix timestamp (ms) when the timer reaches/reached zero. Present when paused is false. The client calculates remaining time as zeroTime - Date.now(). + */ + zeroTime?: number + /** + * Frozen remaining duration in milliseconds. Present when paused is true. + */ + remainingMs?: number + /** + * Total countdown duration in milliseconds (the original configured duration) + */ + 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' + /** + * Whether the timer is currently paused + */ + 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 + /** + * Frozen remaining duration estimate in milliseconds + */ + durationMs?: number +} + interface ActivePiecesEvent { event: 'activePieces' /** @@ -924,6 +1031,11 @@ export { ActivePlaylistQuickLoop, QuickLoopMarker, QuickLoopMarkerType, + TTimerStatus, + 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 702ee867c6..7e173d89fb 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, estimate: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, estimate: 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, estimate: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, estimate: 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, estimate: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, estimate: null }, + ], } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -278,4 +294,95 @@ 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', + 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', + }, + state: { paused: true, duration: 10000 }, + estimateState: { paused: true, duration: 5000 }, + }, + { index: 3, label: '', mode: null, state: 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 running countdown timer transformation + expect(receivedStatus.tTimers[0]).toEqual({ + index: TTimerIndex.NUMBER_1, + label: 'Countdown Timer', + configured: true, + mode: { + type: 'countdown', + paused: false, + zeroTime: 1600000060000, + durationMs: 60000, + stopAtZero: true, + }, + estimate: { + paused: false, + zeroTime: 1600000060000, + }, + anchorPartId: 'PART_BREAK', + }) + + // Verify paused freeRun timer transformation + expect(receivedStatus.tTimers[1]).toEqual({ + index: TTimerIndex.NUMBER_2, + label: 'Paused FreeRun', + configured: true, + mode: { + type: 'freeRun', + paused: true, + elapsedMs: 10000, + }, + estimate: { + paused: true, + durationMs: 5000, + }, + }) + + // Verify unconfigured timer + expect(receivedStatus.tTimers[2]).toEqual({ + index: TTimerIndex.NUMBER_3, + 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 23b70507c1..7afa37e110 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, 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 f1f29c940d..21563e7948 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,11 @@ import { ActivePlaylistQuickLoop, QuickLoopMarker as QuickLoopMarkerStatus, QuickLoopMarkerType as QuickLoopMarkerStatusType, + TTimerStatus, + TTimerEstimate, + TTimerModeCountdown, + TTimerModeFreeRun, + TTimerIndex, } from '@sofie-automation/live-status-gateway-api' import { CollectionHandlers } from '../liveStatusServer.js' @@ -50,6 +56,7 @@ const PLAYLIST_KEYS = [ 'timing', 'startedPlayback', 'quickLoop', + 'tTimers', ] as const type Playlist = PickKeys @@ -170,6 +177,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket ? this._activePlaylist.timing.expectedEnd : undefined, }, + tTimers: this.transformTTimers(this._activePlaylist.tTimers), }) : literal({ event: 'activePlaylist', @@ -185,6 +193,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket timing: { timingMode: ActivePlaylistTimingMode.NONE, }, + tTimers: this.transformTTimers(undefined), }) this.sendMessage(subscribers, message) @@ -204,6 +213,111 @@ 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, estimate: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, estimate: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, estimate: 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 + + 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, + 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, + 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, + } + } + } + private transformQuickLoopMarkerStatus(marker: QuickLoopMarker | undefined): QuickLoopMarkerStatus | undefined { if (!marker) return undefined