@hyperframes/core 0.8.165 → 0.8.167

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ export interface HfSeekEventDetail {
12
12
  * its preview-side capture here, so the CLI seek paths that already await this
13
13
  * barrier wait for a `self` chain's paint instead of racing it.
14
14
  */
15
- export declare function registerSeekCompletion(promise: Promise<unknown>): void;
15
+ export declare function registerSeekCompletion(promise: Promise<unknown>, kind?: "video-frame"): void;
16
16
  export declare function dispatchSeekEvent(time: number): void;
17
17
  /**
18
18
  * Force-dispatch a `"hf-seek"` event even if `time` equals the last dispatched
@@ -26,6 +26,9 @@ export declare function dispatchSeekEvent(time: number): void;
26
26
  */
27
27
  export declare function forceDispatchSeekEvent(time: number): void;
28
28
  export declare function isSeekCompletionBarrierActive(): boolean;
29
- export declare function waitForSeekCompletion(): Promise<void>;
29
+ /** `videoFrames: false` skips only the video-frame holds: a geometry read needs every other completion. */
30
+ export declare function waitForSeekCompletion(options?: {
31
+ videoFrames?: boolean;
32
+ }): Promise<void>;
30
33
  /** Reset internal state — used in tests to prevent cross-test contamination. */
31
34
  export declare function resetSeekDispatchState(): void;
@@ -18,6 +18,7 @@ import { swallow } from "../diagnostics.js";
18
18
  */
19
19
  let _lastDispatchedTime = -1;
20
20
  let _pendingCompletions = new Set();
21
+ let _videoFrameCompletions = new WeakSet();
21
22
  let _pendingFailure;
22
23
  let _activeCompletionBarriers = 0;
23
24
  function dispatch(time) {
@@ -55,11 +56,13 @@ function dispatch(time) {
55
56
  * its preview-side capture here, so the CLI seek paths that already await this
56
57
  * barrier wait for a `self` chain's paint instead of racing it.
57
58
  */
58
- export function registerSeekCompletion(promise) {
59
+ export function registerSeekCompletion(promise, kind) {
59
60
  const completion = promise
60
61
  .then(() => ({ status: "fulfilled" }))
61
62
  .catch((reason) => ({ status: "rejected", reason }));
62
63
  _pendingCompletions.add(completion);
64
+ if (kind === "video-frame")
65
+ _videoFrameCompletions.add(completion);
63
66
  void completion.then((result) => {
64
67
  if (!_pendingCompletions.delete(completion))
65
68
  return;
@@ -91,15 +94,17 @@ export function forceDispatchSeekEvent(time) {
91
94
  export function isSeekCompletionBarrierActive() {
92
95
  return _activeCompletionBarriers > 0;
93
96
  }
94
- export async function waitForSeekCompletion() {
97
+ /** `videoFrames: false` skips only the video-frame holds: a geometry read needs every other completion. */
98
+ export async function waitForSeekCompletion(options) {
99
+ const awaited = () => [..._pendingCompletions].filter((completion) => options?.videoFrames !== false || !_videoFrameCompletions.has(completion));
95
100
  _activeCompletionBarriers += 1;
96
101
  let failed = _pendingFailure;
97
102
  try {
98
103
  // Let concurrently started barriers snapshot the same retained failure
99
104
  // before either one consumes it.
100
105
  await Promise.resolve();
101
- while (_pendingCompletions.size > 0) {
102
- const results = await Promise.all([..._pendingCompletions]);
106
+ for (let pending = awaited(); pending.length > 0; pending = awaited()) {
107
+ const results = await Promise.all(pending);
103
108
  const rejected = results.find((result) => result.status === "rejected");
104
109
  if (failed === undefined && rejected?.status === "rejected") {
105
110
  failed = { reason: rejected.reason };
@@ -123,6 +128,7 @@ export async function waitForSeekCompletion() {
123
128
  export function resetSeekDispatchState() {
124
129
  _lastDispatchedTime = -1;
125
130
  _pendingCompletions = new Set();
131
+ _videoFrameCompletions = new WeakSet();
126
132
  _pendingFailure = undefined;
127
133
  _activeCompletionBarriers = 0;
128
134
  }
@@ -43,7 +43,7 @@ function holdSeekBarrierUntilVideoLands(el) {
43
43
  for (const type of HOLD_END_EVENTS)
44
44
  el.addEventListener(type, done, true);
45
45
  releaseHeldVideo.set(el, done);
46
- }));
46
+ }), "video-frame");
47
47
  }
48
48
  /**
49
49
  * Resolve a media element's timeline window without conflating a video's
@@ -48,7 +48,14 @@ export interface VolumeProbeOptions {
48
48
  * Preview callers omit this option and retain live automation discovery.
49
49
  */
50
50
  allowLiveTimelineSeek?: boolean;
51
+ movers?: () => VolumeMovers;
51
52
  }
53
+ export interface VolumeMovers {
54
+ elements: Set<unknown>;
55
+ anySetter: boolean;
56
+ unknown: boolean;
57
+ }
58
+ export declare function volumeMoversOf(timeline: RuntimeTimelineRef): VolumeMovers;
52
59
  /**
53
60
  * Probe a media element and, if volume automation is detected, store a
54
61
  * NORMALISED envelope in `cache`. Safe to call with a null timeline — returns
@@ -161,25 +161,31 @@ function hasSetter(target, key) {
161
161
  }
162
162
  return false;
163
163
  }
164
- /**
165
- * Whether seeking `timeline` can move `el.volume`: a tween on the element that names `volume`, or a tween
166
- * on a non-DOM object whose tweened property is a setter (a gain proxy). Spacers, calls and counters skip.
167
- */
168
- function timelineCanMoveVolume(timeline, el) {
164
+ export function volumeMoversOf(timeline) {
165
+ const unknown = { elements: new Set(), anySetter: false, unknown: true };
169
166
  if (typeof timeline.getChildren !== "function")
170
- return true;
167
+ return unknown;
171
168
  try {
172
- return timeline.getChildren(true, true, false).some((tween) => {
169
+ const movers = { elements: new Set(), anySetter: false, unknown: false };
170
+ for (const tween of timeline.getChildren(true, true, false)) {
173
171
  const targets = tween.targets?.() ?? [];
174
- return targets.some((target) => isDomNode(target)
175
- ? target === el && namesKey(tween.vars, (key) => key === "volume")
176
- : namesKey(tween.vars, (key) => hasSetter(target, key)));
177
- });
172
+ for (const target of targets) {
173
+ if (isDomNode(target)) {
174
+ if (namesKey(tween.vars, (key) => key === "volume"))
175
+ movers.elements.add(target);
176
+ }
177
+ else if (!movers.anySetter && namesKey(tween.vars, (key) => hasSetter(target, key))) {
178
+ movers.anySetter = true;
179
+ }
180
+ }
181
+ }
182
+ return movers;
178
183
  }
179
184
  catch {
180
- return true;
185
+ return unknown;
181
186
  }
182
187
  }
188
+ const canMoveVolume = (movers, el) => movers.unknown || movers.anySetter || movers.elements.has(el);
183
189
  /**
184
190
  * Probe a media element and, if volume automation is detected, store a
185
191
  * NORMALISED envelope in `cache`. Safe to call with a null timeline — returns
@@ -201,7 +207,7 @@ export function probeAndCacheElementVolume(mediaEl, timeline, compositionDuratio
201
207
  if (compositionDuration <= 0)
202
208
  return;
203
209
  // Sampling is ~60 whole-timeline seeks per second of clip, paid again on every rebind.
204
- if (!timelineCanMoveVolume(timeline, mediaEl))
210
+ if (!canMoveVolume(options.movers?.() ?? volumeMoversOf(timeline), mediaEl))
205
211
  return;
206
212
  const seekFn = (t) => {
207
213
  try {
@@ -4,4 +4,4 @@ export declare const ROOT_CSS_STACKING_CONTEXT_ID = "css:root";
4
4
  * the leaf itself: an element that establishes a context still compares its own
5
5
  * z-index with siblings in its parent's context.
6
6
  */
7
- export declare function resolveCssStackingContextId(node: Element): string;
7
+ export declare function resolveCssStackingContextId(node: Element, byParent?: Map<Element | null, string>): string;
@@ -94,7 +94,15 @@ function cssStackingContextPath(element) {
94
94
  * the leaf itself: an element that establishes a context still compares its own
95
95
  * z-index with siblings in its parent's context.
96
96
  */
97
- export function resolveCssStackingContextId(node) {
97
+ export function resolveCssStackingContextId(node, byParent) {
98
+ const known = byParent?.get(node.parentElement);
99
+ if (known !== undefined)
100
+ return known;
101
+ const id = nearestCssStackingContextId(node);
102
+ byParent?.set(node.parentElement, id);
103
+ return id;
104
+ }
105
+ function nearestCssStackingContextId(node) {
98
106
  const win = node.ownerDocument.defaultView;
99
107
  if (!win)
100
108
  return ROOT_CSS_STACKING_CONTEXT_ID;
@@ -207,6 +207,7 @@ function buildTimelineClipLabel(node, kind, ordinal) {
207
207
  return `${humanizeTimelineToken(kind)} ${ordinal + 1}`;
208
208
  }
209
209
  export function collectRuntimeTimelinePayload(params) {
210
+ const stackingByParent = new Map();
210
211
  const runtimeWindow = window;
211
212
  const timelineRegistry = runtimeWindow.__timelines ?? {};
212
213
  const startResolver = createRuntimeStartTimeResolver({
@@ -409,7 +410,7 @@ export function collectRuntimeTimelinePayload(params) {
409
410
  duration,
410
411
  track: parseAuthoredTrack(node, i),
411
412
  zIndex: readInlineZIndex(node),
412
- stackingContextId: resolveCssStackingContextId(node),
413
+ stackingContextId: resolveCssStackingContextId(node, stackingByParent),
413
414
  kind,
414
415
  tagName: tag,
415
416
  compositionId: node.getAttribute("data-composition-id"),
@@ -524,7 +525,7 @@ export function collectRuntimeTimelinePayload(params) {
524
525
  duration: clampedDuration,
525
526
  track: parseAuthoredTrack(el, gsapTrack),
526
527
  zIndex: readInlineZIndex(el),
527
- stackingContextId: resolveCssStackingContextId(el),
528
+ stackingContextId: resolveCssStackingContextId(el, stackingByParent),
528
529
  kind: "element",
529
530
  tagName: el.tagName.toLowerCase(),
530
531
  compositionId: el.getAttribute("data-composition-id"),
@@ -586,7 +587,7 @@ export function collectRuntimeTimelinePayload(params) {
586
587
  duration: clampedDuration,
587
588
  track: parseAuthoredTrack(el, overlayTrack),
588
589
  zIndex: readInlineZIndex(el),
589
- stackingContextId: resolveCssStackingContextId(el),
590
+ stackingContextId: resolveCssStackingContextId(el, stackingByParent),
590
591
  kind: "element",
591
592
  tagName: tag,
592
593
  compositionId: el.getAttribute("data-composition-id"),
@@ -1,3 +1,3 @@
1
1
  import type { RuntimeTimelineLike } from "./types.js";
2
- /** A registered timeline's finite duration(); a partial, throwing or non-numeric entry reads as null. */
3
- export declare function readTimelineDurationSeconds(timeline: Pick<RuntimeTimelineLike, "duration"> | null | undefined): number | null;
2
+ /** A timeline's finite length, counting paused child timelines; a partial or throwing entry reads as null. */
3
+ export declare function readTimelineDurationSeconds(timeline: Pick<RuntimeTimelineLike, "duration" | "getChildren"> | null | undefined): number | null;
@@ -1,10 +1,16 @@
1
1
  import { swallow } from "./diagnostics.js";
2
- /** A registered timeline's finite duration(); a partial, throwing or non-numeric entry reads as null. */
2
+ const pausedChildEnd = (child) => child.paused?.() === true
3
+ ? (child.startTime?.() ?? 0) +
4
+ (child.totalDuration?.() ?? 0) / Math.abs(child.timeScale?.() || 1)
5
+ : 0;
6
+ /** A timeline's finite length, counting paused child timelines; a partial or throwing entry reads as null. */
3
7
  export function readTimelineDurationSeconds(timeline) {
4
8
  try {
5
9
  if (typeof timeline?.duration !== "function")
6
10
  return null;
7
- const seconds = Number(timeline.duration());
11
+ const held = timeline.getChildren?.(false, false, true);
12
+ const children = Array.isArray(held) ? held : [];
13
+ const seconds = Math.max(Number(timeline.duration()), ...children.map(pausedChildEnd));
8
14
  return Number.isFinite(seconds) ? seconds : null;
9
15
  }
10
16
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/core",
3
- "version": "0.8.165",
3
+ "version": "0.8.167",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -377,9 +377,9 @@
377
377
  "linkedom": "^0.18.12",
378
378
  "postcss": "^8.5.8",
379
379
  "wawoff2": "^2.0.1",
380
- "@hyperframes/lint": "0.8.165",
381
- "@hyperframes/studio-server": "0.8.165",
382
- "@hyperframes/parsers": "0.8.165"
380
+ "@hyperframes/parsers": "0.8.167",
381
+ "@hyperframes/lint": "0.8.167",
382
+ "@hyperframes/studio-server": "0.8.167"
383
383
  },
384
384
  "devDependencies": {
385
385
  "@fontsource/archivo-black": "^5.2.8",