Skip to content
Draft
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
40 changes: 40 additions & 0 deletions packages/corelib/src/dataModel/RundownPlaylist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,21 +130,61 @@ export interface RundownTTimerModeTimeOfDay {
* 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.
*
* @param state The timer state
* @param now Current timestamp in milliseconds
* @returns The current duration in milliseconds
*/
export function timerStateToDuration(state: TimerState, now: number): number {
if (state.paused) {
// Manually paused by user or already pushing/overrun
return state.duration
} else if (state.pauseTime && now >= state.pauseTime) {
// Auto-pause at overrun (current part ended)
return state.zeroTime - state.pauseTime
} else {
// Running normally
return state.zeroTime - now
}
}

export type RundownTTimerIndex = 1 | 2 | 3

export interface RundownTTimer {
Expand Down
34 changes: 6 additions & 28 deletions packages/job-worker/src/playout/tTimers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,11 @@ import { literal } from '@sofie-automation/corelib/dist/lib'
import { getCurrentTime } from '../lib/index.js'
import type { ReadonlyDeep } from 'type-fest'
import * as chrono from 'chrono-node'
import { PartId, SegmentId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { JobContext } from '../jobs/index.js'
import { PlayoutModel } from './model/PlayoutModel.js'
import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio'
import { logger } from '../logging.js'
import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError'
import { getOrderedPartsAfterPlayhead } from './lookahead/util.js'

/**
* Map of active setTimeout timeouts by studioId
* Used to clear previous timeout when recalculation is triggered before the timeout fires
*/
const activeTimeouts = new Map<StudioId, NodeJS.Timeout>()

export function validateTTimerIndex(index: number): asserts index is RundownTTimerIndex {
if (isNaN(index) || index < 1 || index > 3) throw new Error(`T-timer index out of range: ${index}`)
}
Expand Down Expand Up @@ -203,13 +194,6 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl

const playlist = playoutModel.playlist

// Clear any existing timeout for this studio
const existingTimeout = activeTimeouts.get(playlist.studioId)
if (existingTimeout) {
clearTimeout(existingTimeout)
activeTimeouts.delete(playlist.studioId)
}

const tTimers = playlist.tTimers

// Find which timers have anchors that need calculation
Expand Down Expand Up @@ -288,19 +272,11 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl
totalAccumulator = currentSegmentBudget
}
}

// Schedule next recalculation
if (!isPushing && !currentPartInstance.part.autoNext) {
const delay = totalAccumulator + 5
const timeoutId = setTimeout(() => {
context.queueStudioJob(StudioJobs.RecalculateTTimerEstimates, undefined, undefined).catch((err) => {
logger.error(`Failed to queue T-Timer recalculation: ${stringifyError(err)}`)
})
}, delay)
activeTimeouts.set(playlist.studioId, timeoutId)
}
}

// Save remaining current part time for pauseTime calculation
const currentPartRemainingTime = totalAccumulator

// Single pass through parts
for (const part of playablePartsSlice) {
// Detect segment boundary
Expand Down Expand Up @@ -335,10 +311,12 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl
? literal<TimerState>({
paused: true,
duration: anchorTime,
pauseTime: null, // Already paused/pushing
})
: literal<TimerState>({
paused: false,
zeroTime: now + anchorTime,
pauseTime: now + currentPartRemainingTime, // When current part ends and pushing begins
})

playoutModel.updateTTimer({ ...timer, estimateState })
Expand Down
Loading