diff --git a/packages/core/src/runtime/clock.ts b/packages/core/src/runtime/clock.ts index 4d7deabfb3..be846c5989 100644 --- a/packages/core/src/runtime/clock.ts +++ b/packages/core/src/runtime/clock.ts @@ -1,5 +1,5 @@ import { timeAtSourceTime, type RateSpec } from "../speedRamp.js"; -import { MEDIA_HARD_SYNC_SECONDS } from "./media.js"; +import { MEDIA_HARD_SYNC_SECONDS } from "./mediaSteer.js"; import { playRangeHoldTime } from "./protocol.js"; export type TransportClockSnapshot = { diff --git a/packages/core/src/runtime/media.ts b/packages/core/src/runtime/media.ts index 0df53bfde2..36796f080c 100644 --- a/packages/core/src/runtime/media.ts +++ b/packages/core/src/runtime/media.ts @@ -2,11 +2,12 @@ import { swallow } from "./diagnostics"; import { isClipVisibleAt, isInClipWindow } from "./clipWindow"; import { mediaElementTime } from "./mediaClock"; import { sameInstant } from "../clipFacts"; -import { interpolateVolumeGain, type VolumeKeyframe } from "./mediaVolumeEnvelope.js"; +import { interpolateVolumeGain } from "./mediaVolumeEnvelope.js"; import { elementVolumeLaneGain } from "./audioAutomationVolume.js"; -import { fadeGain, NO_FADES, readElementFades, type AudioFades } from "../audioFade.js"; +import { fadeGain, NO_FADES, readElementFades } from "../audioFade.js"; import { readElementPlaybackRate, readElementRateSpec, readMediaStart } from "./playbackRate.js"; -import { rateAt, sourceTimeAt, timeAtSourceTime, type RateSpec } from "../speedRamp.js"; +import { rateAt, sourceTimeAt, timeAtSourceTime } from "../speedRamp.js"; +import type { RuntimeMediaClip } from "./types.js"; import { clampAudioGain, clipGain, gainTimesLane } from "../audioGain.js"; import { isMemberGroupHidden } from "../audioGroups.js"; import { findInjectedRenderFrame } from "./renderFrameSibling.js"; @@ -16,10 +17,12 @@ import { cutToHostSlots, type HostSlot } from "../mediaTiming.js"; import { forgetSteering, isSteering, + MEDIA_HARD_SYNC_SECONDS, MEDIA_SYNC_TOLERANCE_SECONDS, steerMedia, } from "./mediaSteer.js"; export { MEDIA_SYNC_TOLERANCE_SECONDS } from "./mediaSteer.js"; +export type { RuntimeMediaClip } from "./types.js"; export { readElementPlaybackRate, readElementRateSpec, @@ -85,32 +88,6 @@ export function resolveRuntimeMediaClipDuration(params: { return candidates.length > 0 ? Math.min(...candidates) : null; } -export type RuntimeMediaClip = { - el: HTMLVideoElement | HTMLAudioElement; - start: number; - /** The clip's own t=0 when a host's in-point cut its head; source time, lanes and fades run from it. */ - origin?: number; - mediaStart: number; - duration: number; - end: number; - volume: number | null; - playbackRate: number; - /** The rate lane when the clip has one; otherwise `playbackRate`. */ - rate?: RateSpec; - loop: boolean; - /** Source media duration in seconds (from el.duration). Used for loop wrapping. */ - sourceDuration: number | null; - /** - * Probed volume keyframes from the GSAP timeline (same probe the renderer - * uses). When present, `syncRuntimeMedia` drives volume from the envelope - * rather than from `data-volume` + GSAP-change tracking, eliminating the - * race between the 60 Hz transport tick and GSAP's own seek. - */ - volumeKeyframes?: VolumeKeyframe[]; - /** Clip-edge fades from `data-fade-in` / `data-fade-out`; see audioFade.ts. */ - fades?: AudioFades; -}; - export function refreshRuntimeMediaCache(params?: { resolveStartSeconds?: (element: Element) => number; resolveDurationSeconds?: (element: HTMLVideoElement | HTMLAudioElement) => number | null; @@ -273,7 +250,6 @@ export function hasMediaSyncStateForTest(el: HTMLMediaElement): boolean { ); } -export const MEDIA_HARD_SYNC_SECONDS = 0.5; export const MEDIA_PRESEEK_SECONDS = 1.5; export function mediaSourceTimeAt(clip: RuntimeMediaClip, timeSeconds: number): number { diff --git a/packages/core/src/runtime/mediaSteer.ts b/packages/core/src/runtime/mediaSteer.ts index ef72d9ab1e..c47fa910b3 100644 --- a/packages/core/src/runtime/mediaSteer.ts +++ b/packages/core/src/runtime/mediaSteer.ts @@ -1,5 +1,6 @@ /** Drift a playing element may carry before sync pulls it back onto the playhead. */ export const MEDIA_SYNC_TOLERANCE_SECONDS = 0.04; +export const MEDIA_HARD_SYNC_SECONDS = 0.5; // Steered by rate, not seeked (a seek resets a video's decoder and clicks in audio); steps keep rate writes rare. const MEDIA_STEER = 0.03; diff --git a/packages/core/src/runtime/state.ts b/packages/core/src/runtime/state.ts index 469766657d..ccdaef7512 100644 --- a/packages/core/src/runtime/state.ts +++ b/packages/core/src/runtime/state.ts @@ -1,5 +1,4 @@ -import type { RuntimeDeterministicAdapter, RuntimeTimelineLike } from "./types"; -import type { RuntimeMediaClip } from "./media"; +import type { RuntimeDeterministicAdapter, RuntimeMediaClip, RuntimeTimelineLike } from "./types"; import type { TransportClock } from "./clock"; export type RuntimeState = { diff --git a/packages/core/src/runtime/timeline.ts b/packages/core/src/runtime/timeline.ts index cf5ec0b976..80c0cea1c1 100644 --- a/packages/core/src/runtime/timeline.ts +++ b/packages/core/src/runtime/timeline.ts @@ -13,7 +13,7 @@ import { } from "./authoredTiming"; import { swallow } from "./diagnostics"; import { readTimelineDurationSeconds } from "./timelineDuration"; -import { readElementPlaybackRate, readElementPlaybackStart } from "./media"; +import { readElementPlaybackRate, readMediaStart } from "./playbackRate"; import { parseStrictFiniteTimingNumber, resolveMediaElementDurationSeconds, @@ -499,7 +499,7 @@ export function collectRuntimeTimelinePayload(params: { parentCompositionId: compositionContext.parentCompositionId, nodePath: null, compositionSrc: toAbsoluteAssetUrl(node.getAttribute("data-composition-src")), - playbackStart: readElementPlaybackStart(node), + playbackStart: readMediaStart(node), playbackRate: readElementPlaybackRate(node), assetUrl: resolveNodeAssetUrl(node), timelineRole: node.getAttribute("data-timeline-role"), @@ -629,7 +629,7 @@ export function collectRuntimeTimelinePayload(params: { parentCompositionId: rootCompositionIdForGsap, nodePath: null, compositionSrc: null, - playbackStart: readElementPlaybackStart(el), + playbackStart: readMediaStart(el), playbackRate: readElementPlaybackRate(el), assetUrl: null, timelineRole: el.getAttribute("data-timeline-role"), @@ -686,7 +686,7 @@ export function collectRuntimeTimelinePayload(params: { parentCompositionId: rootCompositionIdForGsap, nodePath: null, compositionSrc: null, - playbackStart: readElementPlaybackStart(el), + playbackStart: readMediaStart(el), playbackRate: readElementPlaybackRate(el), assetUrl: null, timelineRole, diff --git a/packages/core/src/runtime/types.ts b/packages/core/src/runtime/types.ts index 09aba58f1c..4e942aa203 100644 --- a/packages/core/src/runtime/types.ts +++ b/packages/core/src/runtime/types.ts @@ -1,5 +1,8 @@ import type { HfColorGradingTarget } from "../colorGrading"; import type { RuntimeAnalyticsEvent } from "./analytics"; +import type { AudioFades } from "../audioFade"; +import type { RateSpec } from "../speedRamp"; +import type { VolumeKeyframe } from "./mediaVolumeEnvelope"; export type RuntimeJson = | string @@ -456,3 +459,22 @@ type RuntimeBridgeControlAction = export type RuntimeBridgeControlMessage = RuntimeBridgeControlMessageBase & RuntimeDataControlFields; + +export type RuntimeMediaClip = { + el: HTMLVideoElement | HTMLAudioElement; + start: number; + /** The clip's own t=0 when a host's in-point cut its head; source time, lanes and fades run from it. */ + origin?: number; + mediaStart: number; + duration: number; + end: number; + volume: number | null; + playbackRate: number; + /** The rate lane when the clip has one; otherwise `playbackRate`. */ + rate?: RateSpec; + loop: boolean; + sourceDuration: number | null; + /** Probed from the GSAP timeline as the renderer does; when set they drive volume, not `data-volume`. */ + volumeKeyframes?: VolumeKeyframe[]; + fades?: AudioFades; +}; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 306f5fbc58..a73319e714 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -17,6 +17,7 @@ "src/runtime/compositionLength.ts", "src/runtime/customEase.ts", "src/runtime/mediaVolumeEnvelope.ts", + "src/runtime/mediaSteer.ts", "src/runtime/positionEdits.ts", "src/runtime/protocol.ts", "src/runtime/stackingContext.ts",