Files
hyperframes/packages/core/src/mediaTiming.ts
T
Miguel Ángel 0a5c235173 feat(cli): hyperframes timeline prints a project's tracks and clips (#4138)
* feat(cli): hyperframes timeline prints a project's tracks and clips

* docs(cli): list the timeline command

* feat(cli): timeline command reports unreadable lanes; add skill docs

* fix(cli): timeline stays inside the project, shares one start cache, keeps small volumes

* fix(cli): timeline resolves symlinks before the project containment check

* fix(cli): timeline accepts project folders whose name starts with two dots

* refactor(cli): timeline and compositions share the engine's start resolver

* docs(skills): point agents at the timeline command where they decide how to read a project

* fix(cli): timeline prints absolute main-timeline times and the owning file for nested clips

Nested rows carry absStart/absEnd on the main timeline and their owning
file, alongside existing local start/end. Text prints absolute time first,
local time in parentheses. Adds jq/node one-liners for common queries,
tested against a fixture.

* fix(cli): timeline places nested media at the runtime start and tests its query one-liners

A nested media clip with a negative literal data-start was reported later than it plays,
because the raw start was clamped before the host offset was added. The skill's jq and node
one-liners now run against the fixture in the test suite.

* fix(cli): timeline clamps a negative media start to 0 when its host starts at 0

The runtime skips the host-offset formula for a host at t=0 and clamps the start, so the
timeline command now does the same.

* refactor(core): one media-start rule shared by the runtime and the timeline command

The runtime and the CLI each carried a copy of the rule for when a nested media clip's
start takes the host offset, and the copies had drifted. Both now call
resolveMediaStartSeconds.

* test(cli): timeline fixture for a media start written as an expression

parseNumeric and the runtime's parseStrictFiniteTimingNumber are the same function, so the
two callers cannot disagree on what counts as a literal; this pins the expression case.
2026-09-19 04:27:45 -04:00

41 lines
1.4 KiB
TypeScript

export const MEDIA_START_BASIS_ATTR = "data-hf-media-start-basis";
export type MediaStartBasis = "local" | "global";
export function readMediaStartBasis(value: string | null | undefined): MediaStartBasis {
return value?.trim().toLowerCase() === "global" ? "global" : "local";
}
/** Resolve authored media time onto the root timeline. Nested media is local
* by default; only an explicit compatibility marker preserves a legacy
* root-global value. */
export function resolveAbsoluteMediaStartSeconds(input: {
authoredStart: number;
hostStart: number;
basis?: string | null;
}): number {
return readMediaStartBasis(input.basis) === "global"
? input.authoredStart
: input.hostStart + input.authoredStart;
}
/** The one rule for a media element's root-timeline start; the runtime and the CLI both call it.
* With no literal start, an auto-injected start, or a host at t<=0 there is nothing for the basis
* to disambiguate, so the ordinary start resolution applies. */
export function resolveMediaStartSeconds(input: {
authoredStart: number | null;
hostStart: number;
hasAutoStart: boolean;
basis?: string | null;
ordinaryStart: () => number;
}): number {
if (input.hasAutoStart || input.authoredStart == null || input.hostStart <= 0) {
return input.ordinaryStart();
}
return resolveAbsoluteMediaStartSeconds({
authoredStart: input.authoredStart,
hostStart: input.hostStart,
basis: input.basis,
});
}