import type { MediaAsset, TimelineItem } from './types.js';
import { hasOperationalTranscript } from '../transcript/types.js';
import { isWindowedMotionGraphic } from './motionGraphicTime.js';
type SourceItem = Pick<TimelineItem, 'kind' | 'src' | 'sourceAssetId' | 'playbackRate' | 'transcript' | 'transcriptStale' | 'designDurationInFrames'>;
export type SourceTimingItem = Pick<TimelineItem, 'srcInFrame' | 'playbackRate'>;
export interface SourceFrameWindow {
startFrame: number;
endFrame: number;
}
export function timelineFramesToSourceFrames(item: SourceTimingItem, frames: number): number {
return frames * Math.max(0.01, item.playbackRate ?? 1);
}
export function sourceFramesToTimelineFrames(item: SourceTimingItem, frames: number): number {
return frames / Math.max(0.01, item.playbackRate ?? 1);
}
export function sourceFrameAt(item: SourceTimingItem, localFrame: number): number {
return Math.max(0, (item.srcInFrame ?? 0) + timelineFramesToSourceFrames(item, localFrame));
}
export function sourceWindowForTimelineRange(
item: SourceTimingItem,
localStartFrame: number,
durationInFrames: number,
): SourceFrameWindow {
const startFrame = sourceFrameAt(item, localStartFrame);
const endFrame = sourceFrameAt(item, localStartFrame + Math.max(0, durationInFrames));
return { startFrame, endFrame: Math.max(startFrame, endFrame) };
}
* How many timeline frames can be played starting from `srcInFrame`; if the length cannot be determined, null (no limit) is returned.
*
* Only effective for real file media (video/audio): pictures, MG, text, and solid colors can be stretched arbitrarily.
* Word-driven audio (audio + transcript) is closed by retime itself according to the length of the word stream after editing. Do not go here.
* Otherwise the two sets of upper bounds will fight. playbackRate conversion: timeline frame = source frame / rate.
*/
export function remainingSourceFrames(
item: SourceItem,
srcInFrame: number,
assets: readonly MediaAsset[] | undefined,
): number | null {
if (isWindowedMotionGraphic(item)) {
return Math.max(1, Math.floor(item.designDurationInFrames! - Math.max(0, srcInFrame)));
}
if (item.kind !== 'video' && item.kind !== 'audio') return null;
if (item.kind === 'audio' && hasOperationalTranscript(item)) return null;
if (!item.src || !assets?.length) return null;
const sourceAsset = item.sourceAssetId
? assets.find((asset) => asset.id === item.sourceAssetId)
: assets.find((asset) => asset.src === item.src);
const total = sourceAsset?.durationInFrames ?? 0;
if (!(total > 0)) return null;
return Math.max(1, Math.floor(sourceFramesToTimelineFrames(item, total - Math.max(0, srcInFrame))));
}