diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index e426fb3f8b..410f275fb7 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -130,6 +130,21 @@ 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 = | { @@ -137,14 +152,39 @@ export type TimerState = 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 { diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index b1c9b6192e..917aa31027 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -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() - 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}`) } @@ -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 @@ -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 @@ -335,10 +311,12 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl ? literal({ paused: true, duration: anchorTime, + pauseTime: null, // Already paused/pushing }) : literal({ paused: false, zeroTime: now + anchorTime, + pauseTime: now + currentPartRemainingTime, // When current part ends and pushing begins }) playoutModel.updateTTimer({ ...timer, estimateState })