Skip to content

Commit 6264e19

Browse files
committed
feat(blueprints): Add T-Timer duration/timing methods and expose rundown timing
Add comprehensive T-Timer manipulation methods to IPlaylistTTimer interface: - setDuration(duration) - Reset timer to a specific duration - setDuration(options) - Independently update original and/or current duration - original: Duration timer resets to on restart() - current: Current countdown value - Preserves elapsed time when only original is provided Add T-Timer query methods: - getCurrentDuration() - Get current timer value in milliseconds - getZeroTime() - Get absolute timestamp of timer's zero point - getProjectedDuration() - Get projected countdown value (for over/under calculation) - getProjectedZeroTime() - Get projected zero time timestamp Add shared utility functions in corelib: - timerStateToDuration() - Calculate current duration from TimerState (already existed) - timerStateToZeroTime() - Calculate zero time from TimerState (new) - Both shared between backend and frontend for consistent calculations Expose timing information to blueprints: - Add timing field to IBlueprintSegmentRundown interface - Exposes RundownPlaylistTiming via context.rundown.timing - Removes need for accessing private _rundown property Implementation: - PlaylistTTimerImpl implements all new methods using shared utilities - Update convertRundownToBlueprintSegmentRundown() to include timing - All methods properly handle paused/running states and edge cases Related to BBC-SOFIE-454
1 parent 7a9cbe8 commit 6264e19

File tree

5 files changed

+189
-0
lines changed

5 files changed

+189
-0
lines changed

packages/blueprints-integration/src/context/tTimersContext.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,29 @@ export interface IPlaylistTTimer {
150150
*/
151151
restart(): boolean
152152

153+
/**
154+
* Set the duration of a countdown timer
155+
* This resets both the original duration (what restart() resets to) and the current countdown value.
156+
* @param duration New duration in milliseconds
157+
* @throws If timer is not in countdown mode or not initialized
158+
*/
159+
setDuration(duration: number): void
160+
161+
/**
162+
* Update the original duration (reset-to value) and/or current duration of a countdown timer
163+
* This allows you to independently update:
164+
* - `original`: The duration the timer resets to when restart() is called
165+
* - `current`: The current countdown value (what's displayed now)
166+
*
167+
* If only `original` is provided, the current duration is recalculated to preserve elapsed time.
168+
* If only `current` is provided, just the current countdown is updated.
169+
* If both are provided, both values are updated independently.
170+
*
171+
* @param options Object with optional `original` and/or `current` duration in milliseconds
172+
* @throws If timer is not in countdown mode or not initialized
173+
*/
174+
setDuration(options: { original?: number; current?: number }): void
175+
153176
/**
154177
* Clear any projection (manual or anchor-based) for this timer
155178
* This removes both manual projections set via setProjectedTime/setProjectedDuration
@@ -194,4 +217,38 @@ export interface IPlaylistTTimer {
194217
* If false (default), we're progressing normally (projection counts down in real-time).
195218
*/
196219
setProjectedDuration(duration: number, paused?: boolean): void
220+
221+
/**
222+
* Get the current duration of the timer in milliseconds
223+
* For countdown timers, this returns how much time is remaining (can be negative if past zero)
224+
* For timeOfDay timers, this returns time until/since the target time
225+
* For freeRun timers, this returns how much time has elapsed
226+
* @returns Current duration in milliseconds, or null if timer is not initialized
227+
*/
228+
getDuration(): number | null
229+
230+
/**
231+
* Get the zero time (reference point) for the timer
232+
* - For countdown/timeOfDay timers: the absolute timestamp when the timer reaches zero
233+
* - For freeRun timers: the absolute timestamp when the timer started (what it counts from)
234+
* For paused timers, calculates when zero would be if resumed now.
235+
* @returns Unix timestamp in milliseconds, or null if timer is not initialized
236+
*/
237+
getZeroTime(): number | null
238+
239+
/**
240+
* Get the projected duration in milliseconds
241+
* This returns the projected timer value when we expect to reach the anchor part.
242+
* Used to calculate over/under (how far ahead or behind schedule we are).
243+
* @returns Projected duration in milliseconds, or null if no projection is set
244+
*/
245+
getProjectedDuration(): number | null
246+
247+
/**
248+
* Get the projected zero time (reference point)
249+
* This returns when we project the timer will reach zero based on scheduled durations.
250+
* For paused projections (when pushing/delayed), calculates when zero would be if resumed now.
251+
* @returns Unix timestamp in milliseconds, or null if no projection is set
252+
*/
253+
getProjectedZeroTime(): number | null
197254
}

packages/blueprints-integration/src/documents/rundown.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export interface IBlueprintRundownDBData {
5555
export interface IBlueprintSegmentRundown<TPrivateData = unknown, TPublicData = unknown> {
5656
externalId: string
5757

58+
/** Rundown timing information */
59+
timing: RundownPlaylistTiming
60+
5861
/** Arbitraty data storage for internal use in the blueprints */
5962
privateData?: TPrivateData
6063
/** Arbitraty data relevant for other systems, made available to them through APIs */

packages/corelib/src/dataModel/RundownPlaylist.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,29 @@ export function timerStateToDuration(state: TimerState, now: number): number {
121121
}
122122
}
123123

124+
/**
125+
* Get the zero time (reference timestamp) for a timer state.
126+
* - For countdown/timeOfDay timers: when the timer reaches zero
127+
* - For freeRun timers: when the timer started (what it counts from)
128+
* For paused timers, calculates when zero would be if resumed now.
129+
*
130+
* @param state The timer state
131+
* @param now Current timestamp in milliseconds
132+
* @returns The zero time timestamp in milliseconds
133+
*/
134+
export function timerStateToZeroTime(state: TimerState, now: number): number {
135+
if (state.paused) {
136+
// Calculate when zero would be if we resumed now
137+
return now + state.duration
138+
} else if (state.pauseTime && now >= state.pauseTime) {
139+
// Auto-pause at overrun (current part ended)
140+
return state.zeroTime - state.pauseTime + now
141+
} else {
142+
// Already have the zero time
143+
return state.zeroTime
144+
}
145+
}
146+
124147
export type RundownTTimerIndex = 1 | 2 | 3
125148

126149
export interface RundownTTimer {

packages/job-worker/src/blueprints/context/lib.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ export function convertRundownToBlueprintSegmentRundown(
443443
): IBlueprintSegmentRundown {
444444
const obj: Complete<IBlueprintSegmentRundown> = {
445445
externalId: rundown.externalId,
446+
timing: rundown.timing,
446447
privateData: skipClone ? rundown.privateData : clone(rundown.privateData),
447448
publicData: skipClone ? rundown.publicData : clone(rundown.publicData),
448449
}

packages/job-worker/src/blueprints/context/services/TTimersService.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import type {
77
RundownTTimer,
88
RundownTTimerIndex,
99
} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
10+
import {
11+
timerStateToDuration,
12+
timerStateToZeroTime,
13+
} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
1014
import type { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids'
1115
import { literal } from '@sofie-automation/corelib/dist/lib'
1216
import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString'
@@ -165,6 +169,75 @@ export class PlaylistTTimerImpl implements IPlaylistTTimer {
165169
return true
166170
}
167171

172+
setDuration(durationOrOptions: number | { original?: number; current?: number }): void {
173+
// Handle overloaded signatures
174+
if (typeof durationOrOptions === 'number') {
175+
// Simple case: reset timer to this duration
176+
return this.setDuration({ original: durationOrOptions, current: durationOrOptions })
177+
}
178+
179+
// Options case: independently update original and/or current
180+
const options = durationOrOptions
181+
182+
if (options.original !== undefined && options.original <= 0) {
183+
throw new Error('Original duration must be greater than zero')
184+
}
185+
if (options.current !== undefined && options.current <= 0) {
186+
throw new Error('Current duration must be greater than zero')
187+
}
188+
189+
if (!this.#timer.mode || this.#timer.mode.type !== 'countdown') {
190+
throw new Error('Timer must be in countdown mode to update duration')
191+
}
192+
193+
if (!this.#timer.state) {
194+
throw new Error('Timer is not initialized')
195+
}
196+
197+
if (!options.original && !options.current) {
198+
throw new Error('At least one of original or current duration must be provided')
199+
}
200+
201+
const now = getCurrentTime()
202+
const state = this.#timer.state
203+
204+
// Calculate current elapsed time
205+
const elapsed = state.paused
206+
? this.#timer.mode.duration - state.duration
207+
: now - (state.zeroTime - this.#timer.mode.duration)
208+
209+
let newOriginalDuration: number
210+
let newCurrentRemaining: number
211+
212+
if (options.original !== undefined && options.current !== undefined) {
213+
// Both specified: use both values independently
214+
newOriginalDuration = options.original
215+
newCurrentRemaining = options.current
216+
} else if (options.original !== undefined) {
217+
// Only original specified: preserve elapsed time
218+
newOriginalDuration = options.original
219+
newCurrentRemaining = Math.max(0, newOriginalDuration - elapsed)
220+
} else {
221+
// Only current specified: keep original unchanged
222+
newOriginalDuration = this.#timer.mode.duration
223+
newCurrentRemaining = options.current!
224+
}
225+
226+
// Update both mode and state
227+
this.#timer = {
228+
...this.#timer,
229+
mode: {
230+
...this.#timer.mode,
231+
duration: newOriginalDuration,
232+
},
233+
state: state.paused
234+
? { paused: true, duration: newCurrentRemaining }
235+
: { paused: false, zeroTime: now + newCurrentRemaining },
236+
}
237+
238+
this.#emitChange(this.#timer)
239+
}
240+
168241
clearProjected(): void {
169242
this.#timer = {
170243
...this.#timer,
@@ -218,4 +291,36 @@ export class PlaylistTTimerImpl implements IPlaylistTTimer {
218291
}
219292
this.#emitChange(this.#timer)
220293
}
294+
295+
getDuration(): number | null {
296+
if (!this.#timer.state) {
297+
return null
298+
}
299+
300+
return timerStateToDuration(this.#timer.state, getCurrentTime())
301+
}
302+
303+
getZeroTime(): number | null {
304+
if (!this.#timer.state) {
305+
return null
306+
}
307+
308+
return timerStateToZeroTime(this.#timer.state, getCurrentTime())
309+
}
310+
311+
getProjectedDuration(): number | null {
312+
if (!this.#timer.projectedState) {
313+
return null
314+
}
315+
316+
return timerStateToDuration(this.#timer.projectedState, getCurrentTime())
317+
}
318+
319+
getProjectedZeroTime(): number | null {
320+
if (!this.#timer.projectedState) {
321+
return null
322+
}
323+
324+
return timerStateToZeroTime(this.#timer.projectedState, getCurrentTime())
325+
}
221326
}

0 commit comments

Comments
 (0)