Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/blueprints-integration/src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './rundownContext.js'
export * from './showStyleContext.js'
export * from './studioContext.js'
export * from './syncIngestChangesContext.js'
export * from './tTimersContext.js'
4 changes: 4 additions & 0 deletions packages/blueprints-integration/src/context/rundownContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import type { IShowStyleContext } from './showStyleContext.js'
import type { IExecuteTSRActionsContext } from './executeTsrActionContext.js'
import type { IDataStoreMethods } from './adlibActionContext.js'
import { ITTimersContext } from './tTimersContext.js'
import type { Time } from '../common.js'

export interface IRundownContext extends IShowStyleContext {
readonly rundownId: string
readonly playlistId: string
readonly rundown: Readonly<IBlueprintSegmentRundown>

/** Actual time of playback starting for the playlist (undefined if not started) */
readonly startedPlayback?: Time
}

export interface IRundownUserContext extends IUserNotesContext, IRundownContext {}
Expand Down
170 changes: 130 additions & 40 deletions packages/blueprints-integration/src/context/tTimersContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,75 @@
export type IPlaylistTTimerIndex = 1 | 2 | 3

export type RundownTTimerMode = RundownTTimerModeFreeRun | RundownTTimerModeCountdown | RundownTTimerModeTimeOfDay

export interface RundownTTimerModeFreeRun {
readonly type: 'freeRun'
}
export interface RundownTTimerModeCountdown {
readonly type: 'countdown'
/**
* The original duration of the countdown in milliseconds, so that we know what value to reset to
*/
readonly duration: number

/**
* If the countdown should stop at zero, or continue into negative values
*/
readonly stopAtZero: boolean
}
export interface RundownTTimerModeTimeOfDay {
readonly type: 'timeOfDay'

/**
* The raw target string of the timer, as provided when setting the timer
* (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number)
*/
readonly targetRaw: string | number

/**
* If the countdown should stop at zero, or continue into negative values
*/
readonly stopAtZero: boolean
}

/**
* Timing state for a timer, optimized for efficient client rendering.
* When running, the client calculates current time from zeroTime.
* When paused, the duration is frozen and sent directly.
* pauseTime indicates when the timer should automatically pause (when current part ends and overrun begins).
*
* Client rendering logic:
* ```typescript
* if (state.paused === true) {
* // Manually paused by user or already pushing/overrun
* duration = state.duration
* } else if (state.pauseTime && now >= state.pauseTime) {
* // Auto-pause at overrun (current part ended)
* duration = state.zeroTime - state.pauseTime
* } else {
* // Running normally
* duration = state.zeroTime - now
* }
* ```
*/
export type TimerState =
| {
/** Whether the timer is paused */
paused: false
/** The absolute timestamp (ms) when the timer reaches/reached zero */
zeroTime: number
/** Optional timestamp when the timer should pause (when current part ends) */
pauseTime?: number | null
}
| {
/** Whether the timer is paused */
paused: true
/** The frozen duration value in milliseconds */
duration: number
/** Optional timestamp when the timer should pause (null when already paused/pushing) */
pauseTime?: number | null
}

export interface ITTimersContext {
/**
* Get a T-timer by its index
Expand All @@ -20,11 +90,19 @@ export interface IPlaylistTTimer {
/** The label of the T-timer */
readonly label: string

/**
* The current mode of the T-timer
* Null if the T-timer is not initialized
* This defines how the timer behaves
*/
readonly mode: RundownTTimerMode | null

/**
* The current state of the T-timer
* Null if the T-timer is not initialized
* This contains the timing information needed to calculate the current time of the timer
*/
readonly state: IPlaylistTTimerState | null
readonly state: TimerState | null

/** Set the label of the T-timer */
setLabel(label: string): void
Expand Down Expand Up @@ -72,6 +150,29 @@ export interface IPlaylistTTimer {
*/
restart(): boolean

/**
* Set the duration of a countdown timer
* This resets both the original duration (what restart() resets to) and the current countdown value.
* @param duration New duration in milliseconds
* @throws If timer is not in countdown mode or not initialized
*/
setDuration(duration: number): void

/**
* Update the original duration (reset-to value) and/or current duration of a countdown timer
* This allows you to independently update:
* - `original`: The duration the timer resets to when restart() is called
* - `current`: The current countdown value (what's displayed now)
*
* If only `original` is provided, the current duration is recalculated to preserve elapsed time.
* If only `current` is provided, just the current countdown is updated.
* If both are provided, both values are updated independently.
*
* @param options Object with optional `original` and/or `current` duration in milliseconds
* @throws If timer is not in countdown mode or not initialized
*/
setDuration(options: { original?: number; current?: number }): void

/**
* Clear any projection (manual or anchor-based) for this timer
* This removes both manual projections set via setProjectedTime/setProjectedDuration
Expand Down Expand Up @@ -116,49 +217,38 @@ export interface IPlaylistTTimer {
* If false (default), we're progressing normally (projection counts down in real-time).
*/
setProjectedDuration(duration: number, paused?: boolean): void
}

export type IPlaylistTTimerState =
| IPlaylistTTimerStateCountdown
| IPlaylistTTimerStateFreeRun
| IPlaylistTTimerStateTimeOfDay

export interface IPlaylistTTimerStateCountdown {
/** The mode of the T-timer */
readonly mode: 'countdown'
/** The current time of the countdown, in milliseconds */
readonly currentTime: number
/** The total duration of the countdown, in milliseconds */
readonly duration: number
/** Whether the timer is currently paused */
readonly paused: boolean

/** If the countdown is set to stop at zero, or continue into negative values */
readonly stopAtZero: boolean
}
export interface IPlaylistTTimerStateFreeRun {
/** The mode of the T-timer */
readonly mode: 'freeRun'
/** The current time of the freerun, in milliseconds */
readonly currentTime: number
/** Whether the timer is currently paused */
readonly paused: boolean
}
/**
* Get the current duration of the timer in milliseconds
* For countdown timers, this returns how much time is remaining (can be negative if past zero)
* For timeOfDay timers, this returns time until/since the target time
* For freeRun timers, this returns how much time has elapsed
* @returns Current duration in milliseconds, or null if timer is not initialized
*/
getDuration(): number | null

export interface IPlaylistTTimerStateTimeOfDay {
/** The mode of the T-timer */
readonly mode: 'timeOfDay'
/** The current remaining time of the timer, in milliseconds */
readonly currentTime: number
/** The target timestamp of the timer, in milliseconds */
readonly targetTime: number
/**
* Get the zero time (reference point) for the timer
* - For countdown/timeOfDay timers: the absolute timestamp when the timer reaches zero
* - For freeRun timers: the absolute timestamp when the timer started (what it counts from)
* For paused timers, calculates when zero would be if resumed now.
* @returns Unix timestamp in milliseconds, or null if timer is not initialized
*/
getZeroTime(): number | null

/**
* The raw target string of the timer, as provided when setting the timer
* (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number)
* Get the projected duration in milliseconds
* This returns the projected timer value when we expect to reach the anchor part.
* Used to calculate over/under (how far ahead or behind schedule we are).
* @returns Projected duration in milliseconds, or null if no projection is set
*/
readonly targetRaw: string | number
getProjectedDuration(): number | null

/** If the countdown is set to stop at zero, or continue into negative values */
readonly stopAtZero: boolean
/**
* Get the projected zero time (reference point)
* This returns when we project the timer will reach zero based on scheduled durations.
* For paused projections (when pushing/delayed), calculates when zero would be if resumed now.
* @returns Unix timestamp in milliseconds, or null if no projection is set
*/
getProjectedZeroTime(): number | null
}
3 changes: 3 additions & 0 deletions packages/blueprints-integration/src/documents/rundown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export interface IBlueprintRundownDBData {
export interface IBlueprintSegmentRundown<TPrivateData = unknown, TPublicData = unknown> {
externalId: string

/** Rundown timing information */
timing: RundownPlaylistTiming

/** Arbitraty data storage for internal use in the blueprints */
privateData?: TPrivateData
/** Arbitraty data relevant for other systems, made available to them through APIs */
Expand Down
101 changes: 30 additions & 71 deletions packages/corelib/src/dataModel/RundownPlaylist.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Time, TimelinePersistentState, RundownPlaylistTiming } from '@sofie-automation/blueprints-integration'
import {
Time,
TimelinePersistentState,
RundownPlaylistTiming,
RundownTTimerMode,
TimerState,
} from '@sofie-automation/blueprints-integration'
import {
PartId,
PieceInstanceInfiniteId,
Expand Down Expand Up @@ -94,76 +100,6 @@ export interface QuickLoopProps {
forceAutoNext: ForceQuickLoopAutoNext
}

export type RundownTTimerMode = RundownTTimerModeFreeRun | RundownTTimerModeCountdown | RundownTTimerModeTimeOfDay

export interface RundownTTimerModeFreeRun {
readonly type: 'freeRun'
}
export interface RundownTTimerModeCountdown {
readonly type: 'countdown'
/**
* The original duration of the countdown in milliseconds, so that we know what value to reset to
*/
readonly duration: number

/**
* If the countdown should stop at zero, or continue into negative values
*/
readonly stopAtZero: boolean
}
export interface RundownTTimerModeTimeOfDay {
readonly type: 'timeOfDay'

/**
* The raw target string of the timer, as provided when setting the timer
* (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number)
*/
readonly targetRaw: string | number

/**
* If the countdown should stop at zero, or continue into negative values
*/
readonly stopAtZero: boolean
}

/**
* Timing state for a timer, optimized for efficient client rendering.
* When running, the client calculates current time from zeroTime.
* When paused, the duration is frozen and sent directly.
* pauseTime indicates when the timer should automatically pause (when current part ends and overrun begins).
*
* Client rendering logic:
* ```typescript
* if (state.paused === true) {
* // Manually paused by user or already pushing/overrun
* duration = state.duration
* } else if (state.pauseTime && now >= state.pauseTime) {
* // Auto-pause at overrun (current part ended)
* duration = state.zeroTime - state.pauseTime
* } else {
* // Running normally
* duration = state.zeroTime - now
* }
* ```
*/
export type TimerState =
| {
/** Whether the timer is paused */
paused: false
/** The absolute timestamp (ms) when the timer reaches/reached zero */
zeroTime: number
/** Optional timestamp when the timer should pause (when current part ends) */
pauseTime?: number | null
}
| {
/** Whether the timer is paused */
paused: true
/** The frozen duration value in milliseconds */
duration: number
/** Optional timestamp when the timer should pause (null when already paused/pushing) */
pauseTime?: number | null
}

/**
* Calculate the current duration for a timer state.
* Handles paused, auto-pause (pauseTime), and running states.
Expand All @@ -185,6 +121,29 @@ export function timerStateToDuration(state: TimerState, now: number): number {
}
}

/**
* Get the zero time (reference timestamp) for a timer state.
* - For countdown/timeOfDay timers: when the timer reaches zero
* - For freeRun timers: when the timer started (what it counts from)
* For paused timers, calculates when zero would be if resumed now.
*
* @param state The timer state
* @param now Current timestamp in milliseconds
* @returns The zero time timestamp in milliseconds
*/
export function timerStateToZeroTime(state: TimerState, now: number): number {
if (state.paused) {
// Calculate when zero would be if we resumed now
return now + state.duration
} else if (state.pauseTime && now >= state.pauseTime) {
// Auto-pause at overrun (current part ended)
return state.zeroTime - state.pauseTime + now
} else {
// Already have the zero time
return state.zeroTime
}
}

export type RundownTTimerIndex = 1 | 2 | 3

export interface RundownTTimer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
IRundownActivationContext,
IRundownActivationContextState,
TSR,
Time,
} from '@sofie-automation/blueprints-integration'
import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model/Ids'
import { ReadonlyDeep } from 'type-fest'
Expand Down Expand Up @@ -58,6 +59,10 @@ export class RundownActivationContext extends RundownEventContext implements IRu
return this._currentState
}

get startedPlayback(): Time | undefined {
return this._playoutModel.playlist.startedPlayback
}

async listPlayoutDevices(): Promise<IBlueprintPlayoutDevice[]> {
return listPlayoutDevices(this._context, this._playoutModel)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
IBlueprintPartInstance,
SomeContent,
WithTimeline,
Time,
} from '@sofie-automation/blueprints-integration'
import { postProcessPieces, postProcessTimelineObjects } from '../postProcess.js'
import {
Expand Down Expand Up @@ -61,6 +62,10 @@ export class SyncIngestUpdateToPartInstanceContext
return Array.from(this.#changedTTimers.values())
}

public get startedPlayback(): Time | undefined {
return this.#playoutModel.playlist.startedPlayback
}

constructor(
context: JobContext,
playoutModel: PlayoutModel,
Expand Down
Loading
Loading