gesso-core 0.1.0 → 0.2.0

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.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as prepareInputSurface, $i as textContentOf, $n as nextWordEnd, $r as borderRadius, $t as createPaintState, A as accumulatedOffsetTo, Aa as UiBeforeInputEvent, Ai as raiseContrast, An as editorFor, Ar as pathValuesEqual, At as EditableLayout, B as parseFlexDirection, Ba as UiWheelEvent, Bi as boxShadowArraysEqual, Bn as caretRectFor, Br as radialGradient, Bt as textMeasureRequest, C as describeLength, Ca as writeOverrideProperty, Ci as hasThemeExtension, Cn as linkOf, Cr as statesEqual, Ct as wordRangeIn, D as formatNumber, Da as visualState, Di as withThemeExtension, Dn as CARET_WIDTH, Dr as validateSubgrid, Dt as ELLIPSIS, E as formatExplanation, Ea as hasVisualState, Ei as themeExtensionsEqual, En as CARET_BLINK_MS, Er as UI_SUBGRID_AXES, Et as spannedRunsFor, F as constraintsEqual, Fa as UiKeyboardEvent, Fi as spacingEqual, Fn as nextCaretToggle, Fr as transformsEqual, Ft as fillTextRects, G as MainAxisAlignment, Ga as UiBinding, Gi as defaultTypography, Gn as offsetAtX, Gr as isFrLength, Gt as fontStackFor, H as sizeGridTracks, Ha as wheelDeltaYOf, Hi as defaultShapes, Hn as lineIndexForOffset, Hr as auto, Ht as textRunDecorations, I as tightenConstraints, Ia as UiPinchEvent, Ii as spacingSteps, In as syncEditorValue, Ir as MAX_GRADIENT_STOPS, It as layoutTextLines, J as parseMainAxisAlignment, Ji as defaultTextStyle, Jn as EditableTextModel, Jr as minmax, Jt as DEFAULT_FONT_SIZE, K as parseAlignContent, Ki as isTypographyRole, Kn as offsetForVerticalMove, Kr as isMinMaxTrack, Kt as registerFontStack, L as MAX_MEASURES_PER_CHILD, La as UiPointerEvent, Li as defaultShadows, Ln as resolveColorValue, Lr as gradientPaint, Lt as runCanvasStyleInto, M as LayoutRecord, Ma as UiFocusEvent, Mi as densityFactors, Mn as isEditableNode, Mr as parseTransform, Mt as buildFontString, N as Constraints, Na as UiGestureEvent, Ni as defaultSpacing, Nn as isMultiline, Nr as transform, Nt as createCanvasTextStyle, O as labelNode, Oa as visualStatesEqual, Oi as contrastRatio, On as EDITOR_PROP, Or as inputsEqual, Ot as layoutParagraph, P as clampSize, Pa as UiInputEvent, Pi as scaleSpacing, Pn as isReadOnly, Pr as transformIsIdentity, Pt as drawText, Q as pointerDeviceOf, Qi as spannedTextOf, Qn as nextGraphemeEnd, Qr as UiBorderRadiuses, Qt as computeObjectFitRect, R as isLayoutProtocol, Ra as UiTextChangeEvent, Ri as shadowsEqual, Rn as isVideoSurface, Rr as gradientsEqual, Rt as runFontString, S as SCROLLBAR_LINGER_MS, Sa as writeDeclaredProperty, Si as defineThemeExtension, Sn as linkHoverOf, Sr as normalizeStates, St as selectionRectsIn, T as formatConstraints, Ta as defaultVisualState, Ti as themeExtension, Tn as setLinkHover, Tr as validateStates, Tt as ParagraphTextMeasurer, U as AlignContent, Ua as UiNodeType, Ui as isShapeName, Un as lineLimit, Ur as fr, Ut as bumpFontStack, V as placeGridItems, Va as noKeyModifiers, Vi as boxShadowsEqual, Vn as lineIndexAtY, Vr as validateGradient, Vt as textRunBackgrounds, W as CrossAxisAlignment, Wa as DirtyFlags, Wi as shapesEqual, Wn as offsetAtPoint, Wr as isAutoLength, Wt as clearFontStacks, X as UiPlatformAdapter, Xi as resolvedSpansOf, Xn as lineEndAt, Xr as repeat, Xt as DEFAULT_TEXT_COLOR, Y as CanvasPlatformSurface, Yi as flattenTextSpans, Yn as graphemeBoundaries, Yr as percent, Yt as DEFAULT_FONT_WEIGHT, Z as capturePointer, Zi as spanAtOffset, Zn as lineStartAt, Zr as resolveLength, Zt as colorToCss, _ as LABEL_PADDING_X, _a as UiNode, _i as darkTheme, _n as isNodeInert, _r as UiProperties, _t as UiInputDispatcher, a as PaintPictureCache, aa as UiBasicColors, ai as uniformBorderRadius, an as TEXT_MATCHES_PROP, ar as resolveProperty, at as isScrollContainer, b as LayoutEngine, ba as overrideSources, bi as withContrast, bn as clearLinkHover, br as isUiRole, bt as paragraphGeometry, c as EMPTY_RECORDING, ca as colorValuesEqual, ci as UiEnvironmentKeys, cn as setMatchRanges, cr as UiGraph, ct as UiFocusManager, d as tracePathData, da as parseColor, di as noInsets, dn as selectableTextNodes, dr as getPropertyNames, dt as SCROLLBAR_INSET, ea as textSpansEqual, ei as borderRadiusCorners, en as isPaintVisible, er as previousGraphemeStart, et as touchActionFor, f as ScaledImageCache, fa as rgb8, fi as observeViewportInsets, fn as selectableTextOf, fr as inheritedPropertyFlags, ft as SCROLLBAR_MIN_THUMB, g as paintsAfterChildren, ga as UiEnvironment, gi as unknownContainerSize, gn as isNodeHitTestable, gr as propertyValuesEqualByName, gt as scrollbarZoneAt, h as hasDecorationPhase, ha as findEnvironmentKey, hi as containerBands, hn as isNodeFocusable, hr as propertyIsInherited, ht as scrollbarThumbs, i as traceRoundedRect, ia as lightColors, ii as normalizeBorderRadius, in as DEFAULT_LINE_HEIGHT_FACTOR, ir as resolveNumber, it as UiWheelController, j as contentOffset, ja as UiEventType, ji as relativeLuminance, jn as editorOf, jr as UiTransforms, jt as applyCanvasTextStyle, k as withOverrideSource, ka as MOUSE_POINTER, ki as highContrastColors, kn as caretVisibleAt, kr as paintValuesEqual, kt as proportionalFontMetrics, l as PaintRecorder, la as colorsEqual, li as UiInsetRegistry, ln as TEXT_SELECTION_PROP, lr as closestPropertyName, lt as UiHitTester, m as decorationRect, ma as createEnvironmentKey, mi as bandOf, mn as setSelectionRange, mr as propertyEffects, mt as scrollbarThumb, n as createCanvasSurface, na as colorsEqualPalette, ni as borderRadiusIsZero, nn as normalizeVerticalAlign, nr as wordRangeAt, nt as UiGestureRecognizer, o as paintPictures, oa as colorToHex, oi as defineProperty, on as clearMatchRanges, or as resolvePropertyByName, ot as UiKeyboardController, p as decorationColor, pa as rgba, pi as UiContainerSizeSource, pn as selectionRangeOf, pr as inheritedPropertyNames, pt as SCROLLBAR_THICKNESS, q as parseCrossAxisAlignment, qi as typographyEqual, qn as selectionRects, qr as isPercentLength, qt as DEFAULT_FONT_FAMILY, r as Canvas2DRenderer, ra as darkColors, ri as borderRadiusValuesEqual, rn as resolvePaintState, rr as resolveBoolean, rt as UiPinchRecognizer, s as PaintTarget, sa as colorToRgba, si as propertyValuesEqual, sn as matchRangesOf, sr as resolveString, st as UiPointerController, t as CanvasSurface, ta as textStylesEqual, ti as borderRadiusEqual, tn as normalizeTextAlign, tr as previousWordStart, tt as UiTouchScroller, u as replayPaint, ua as normalizeColor, ui as insetsEqual, un as clearSelectionRange, ur as findPropertyDefinition, ut as SCROLLBAR_HOVER_ZONE, v as drawOverlayShapes, va as DirtyNodeSet, vi as lightTheme, vn as isNodeSelectable, vr as UI_ROLES, vt as hasDrawnText, w as describeOverrides, wa as UiVisualState, wi as noThemeExtensions, wn as linkRunAtPointIn, wr as validateRole, wt as CharacterCountTextMeasurer, x as SCROLLBAR_FADE_MS, xa as resetOverrideWarnings, xi as withDensity, xn as hasTextLinks, xr as isUiSemanticState, xt as paragraphGeometryFrom, y as labelOrigin, ya as clearOverrideProperty, yi as themesEqual, yn as resolveCursor, yr as UI_SEMANTIC_STATES, yt as offsetAtPointIn, z as FlexDirection, za as UiWheelDeltaMode, zi as boxShadow, zn as videoFrameSize, zr as linearGradient, zt as spanOf } from "./CanvasSurface-CrG-uJ5o.js";
1
+ import { $ as prepareInputSurface, $i as textContentOf, $n as nextWordEnd, $r as borderRadius, $t as createPaintState, A as accumulatedOffsetTo, Aa as UiBeforeInputEvent, Ai as raiseContrast, An as editorFor, Ar as pathValuesEqual, At as EditableLayout, B as parseFlexDirection, Ba as UiWheelEvent, Bi as boxShadowArraysEqual, Bn as caretRectFor, Br as radialGradient, Bt as textMeasureRequest, C as describeLength, Ca as writeOverrideProperty, Ci as hasThemeExtension, Cn as linkOf, Cr as statesEqual, Ct as wordRangeIn, D as formatNumber, Da as visualState, Di as withThemeExtension, Dn as CARET_WIDTH, Dr as validateSubgrid, Dt as ELLIPSIS, E as formatExplanation, Ea as hasVisualState, Ei as themeExtensionsEqual, En as CARET_BLINK_MS, Er as UI_SUBGRID_AXES, Et as spannedRunsFor, F as constraintsEqual, Fa as UiKeyboardEvent, Fi as spacingEqual, Fn as nextCaretToggle, Fr as transformsEqual, Ft as fillTextRects, G as MainAxisAlignment, Ga as UiBinding, Gi as defaultTypography, Gn as offsetAtX, Gr as isFrLength, Gt as fontStackFor, H as sizeGridTracks, Ha as wheelDeltaYOf, Hi as defaultShapes, Hn as lineIndexForOffset, Hr as auto, Ht as textRunDecorations, I as tightenConstraints, Ia as UiPinchEvent, Ii as spacingSteps, In as syncEditorValue, Ir as MAX_GRADIENT_STOPS, It as layoutTextLines, J as parseMainAxisAlignment, Ji as defaultTextStyle, Jn as EditableTextModel, Jr as minmax, Jt as DEFAULT_FONT_SIZE, K as parseAlignContent, Ki as isTypographyRole, Kn as offsetForVerticalMove, Kr as isMinMaxTrack, Kt as registerFontStack, L as MAX_MEASURES_PER_CHILD, La as UiPointerEvent, Li as defaultShadows, Ln as resolveColorValue, Lr as gradientPaint, Lt as runCanvasStyleInto, M as LayoutRecord, Ma as UiFocusEvent, Mi as densityFactors, Mn as isEditableNode, Mr as parseTransform, Mt as buildFontString, N as Constraints, Na as UiGestureEvent, Ni as defaultSpacing, Nn as isMultiline, Nr as transform, Nt as createCanvasTextStyle, O as labelNode, Oa as visualStatesEqual, Oi as contrastRatio, On as EDITOR_PROP, Or as inputsEqual, Ot as layoutParagraph, P as clampSize, Pa as UiInputEvent, Pi as scaleSpacing, Pn as isReadOnly, Pr as transformIsIdentity, Pt as drawText, Q as pointerDeviceOf, Qi as spannedTextOf, Qn as nextGraphemeEnd, Qr as UiBorderRadiuses, Qt as computeObjectFitRect, R as isLayoutProtocol, Ra as UiTextChangeEvent, Ri as shadowsEqual, Rn as isVideoSurface, Rr as gradientsEqual, Rt as runFontString, S as SCROLLBAR_LINGER_MS, Sa as writeDeclaredProperty, Si as defineThemeExtension, Sn as linkHoverOf, Sr as normalizeStates, St as selectionRectsIn, T as formatConstraints, Ta as defaultVisualState, Ti as themeExtension, Tn as setLinkHover, Tr as validateStates, Tt as ParagraphTextMeasurer, U as AlignContent, Ua as UiNodeType, Ui as isShapeName, Un as lineLimit, Ur as fr, Ut as bumpFontStack, V as placeGridItems, Va as noKeyModifiers, Vi as boxShadowsEqual, Vn as lineIndexAtY, Vr as validateGradient, Vt as textRunBackgrounds, W as CrossAxisAlignment, Wa as DirtyFlags, Wi as shapesEqual, Wn as offsetAtPoint, Wr as isAutoLength, Wt as clearFontStacks, X as UiPlatformAdapter, Xi as resolvedSpansOf, Xn as lineEndAt, Xr as repeat, Xt as DEFAULT_TEXT_COLOR, Y as CanvasPlatformSurface, Yi as flattenTextSpans, Yn as graphemeBoundaries, Yr as percent, Yt as DEFAULT_FONT_WEIGHT, Z as capturePointer, Zi as spanAtOffset, Zn as lineStartAt, Zr as resolveLength, Zt as colorToCss, _ as LABEL_PADDING_X, _a as UiNode, _i as darkTheme, _n as isNodeInert, _r as UiProperties, _t as UiInputDispatcher, a as PaintPictureCache, aa as UiBasicColors, ai as uniformBorderRadius, an as TEXT_MATCHES_PROP, ar as resolveProperty, at as isScrollContainer, b as LayoutEngine, ba as overrideSources, bi as withContrast, bn as clearLinkHover, br as isUiRole, bt as paragraphGeometry, c as EMPTY_RECORDING, ca as colorValuesEqual, ci as UiEnvironmentKeys, cn as setMatchRanges, cr as UiGraph, ct as UiFocusManager, d as tracePathData, da as parseColor, di as noInsets, dn as selectableTextNodes, dr as getPropertyNames, dt as SCROLLBAR_INSET, ea as textSpansEqual, ei as borderRadiusCorners, en as isPaintVisible, er as previousGraphemeStart, et as touchActionFor, f as ScaledImageCache, fa as rgb8, fi as observeViewportInsets, fn as selectableTextOf, fr as inheritedPropertyFlags, ft as SCROLLBAR_MIN_THUMB, g as paintsAfterChildren, ga as UiEnvironment, gi as unknownContainerSize, gn as isNodeHitTestable, gr as propertyValuesEqualByName, gt as scrollbarZoneAt, h as hasDecorationPhase, ha as findEnvironmentKey, hi as containerBands, hn as isNodeFocusable, hr as propertyIsInherited, ht as scrollbarThumbs, i as traceRoundedRect, ia as lightColors, ii as normalizeBorderRadius, in as DEFAULT_LINE_HEIGHT_FACTOR, ir as resolveNumber, it as UiWheelController, j as contentOffset, ja as UiEventType, ji as relativeLuminance, jn as editorOf, jr as UiTransforms, jt as applyCanvasTextStyle, k as withOverrideSource, ka as MOUSE_POINTER, ki as highContrastColors, kn as caretVisibleAt, kr as paintValuesEqual, kt as proportionalFontMetrics, l as PaintRecorder, la as colorsEqual, li as UiInsetRegistry, ln as TEXT_SELECTION_PROP, lr as closestPropertyName, lt as UiHitTester, m as decorationRect, ma as createEnvironmentKey, mi as bandOf, mn as setSelectionRange, mr as propertyEffects, mt as scrollbarThumb, n as createCanvasSurface, na as colorsEqualPalette, ni as borderRadiusIsZero, nn as normalizeVerticalAlign, nr as wordRangeAt, nt as UiGestureRecognizer, o as paintPictures, oa as colorToHex, oi as defineProperty, on as clearMatchRanges, or as resolvePropertyByName, ot as UiKeyboardController, p as decorationColor, pa as rgba, pi as UiContainerSizeSource, pn as selectionRangeOf, pr as inheritedPropertyNames, pt as SCROLLBAR_THICKNESS, q as parseCrossAxisAlignment, qi as typographyEqual, qn as selectionRects, qr as isPercentLength, qt as DEFAULT_FONT_FAMILY, r as Canvas2DRenderer, ra as darkColors, ri as borderRadiusValuesEqual, rn as resolvePaintState, rr as resolveBoolean, rt as UiPinchRecognizer, s as PaintTarget, sa as colorToRgba, si as propertyValuesEqual, sn as matchRangesOf, sr as resolveString, st as UiPointerController, t as CanvasSurface, ta as textStylesEqual, ti as borderRadiusEqual, tn as normalizeTextAlign, tr as previousWordStart, tt as UiTouchScroller, u as replayPaint, ua as normalizeColor, ui as insetsEqual, un as clearSelectionRange, ur as findPropertyDefinition, ut as SCROLLBAR_HOVER_ZONE, v as drawOverlayShapes, va as DirtyNodeSet, vi as lightTheme, vn as isNodeSelectable, vr as UI_ROLES, vt as hasDrawnText, w as describeOverrides, wa as UiVisualState, wi as noThemeExtensions, wn as linkRunAtPointIn, wr as validateRole, wt as CharacterCountTextMeasurer, x as SCROLLBAR_FADE_MS, xa as resetOverrideWarnings, xi as withDensity, xn as hasTextLinks, xr as isUiSemanticState, xt as paragraphGeometryFrom, y as labelOrigin, ya as clearOverrideProperty, yi as themesEqual, yn as resolveCursor, yr as UI_SEMANTIC_STATES, yt as offsetAtPointIn, z as FlexDirection, za as UiWheelDeltaMode, zi as boxShadow, zn as videoFrameSize, zr as linearGradient, zt as spanOf } from "./CanvasSurface-5dU-U3qQ.js";
2
2
  import { BehaviorSubject, EMPTY, Subject, combineLatest, distinctUntilChanged, isObservable as isObservable$1, map, of } from "rxjs";
3
3
  //#region src/animation/UiEasing.ts
4
4
  /** Progress equals time. What a spinner and a sweep want. */
@@ -175,7 +175,29 @@ var UiAnimation = class {
175
175
  startedAt = 0;
176
176
  begun = false;
177
177
  subject = new Subject();
178
- lastSampledAt = null;
178
+ /**
179
+ * When this animation next *wants* a sample, on the ideal timeline.
180
+ *
181
+ * The ideal one, and that word is carrying the whole fix. This used
182
+ * to be derived at the point of asking, as `lastSampledAt + stepMs`,
183
+ * which sounds equivalent and is not: frames arrive on the display's
184
+ * refreshes, so a sample is nearly always served a little after it
185
+ * was due, and measuring the next step from when it was *served*
186
+ * rounds the period up to a whole refresh and keeps the rounding.
187
+ *
188
+ * The arithmetic is unforgiving. A 24fps clip wants 41.67ms; on a
189
+ * 60Hz display the refresh at 33.3ms is too early and the one at
190
+ * 50ms serves it, so the next step is measured from 50, wants 91.67,
191
+ * and is served at 100. The period is not 41.67ms but 50ms, which is
192
+ * 20fps: one frame in six dropped, for ever, on the commonest clip
193
+ * rate there is. Measured, not reasoned about; see the spec beside
194
+ * this file.
195
+ *
196
+ * Advancing the ideal time by exactly `stepMs` instead lets the
197
+ * served times alternate between 33.3 and 50 and average out to the
198
+ * rate that was asked for.
199
+ */
200
+ nextDueAt = null;
179
201
  lastWritten;
180
202
  finished = false;
181
203
  constructor(cell, stepMs, reducedMotionPolicy, delayMs = 0) {
@@ -217,7 +239,7 @@ var UiAnimation = class {
217
239
  }
218
240
  dueAt(now) {
219
241
  if (this.begun && now - this.startedAt < this.delayMs) return this.startedAt + this.delayMs;
220
- return this.lastSampledAt === null ? now : this.lastSampledAt + this.stepMs;
242
+ return this.nextDueAt ?? now;
221
243
  }
222
244
  /**
223
245
  * Begins at `now`, on the first frame that advances it.
@@ -243,12 +265,9 @@ var UiAnimation = class {
243
265
  if (this.finished) return;
244
266
  this.begin(now);
245
267
  const elapsed = now - this.startedAt;
246
- if (elapsed < this.delayMs) {
247
- this.lastSampledAt = now;
248
- return;
249
- }
268
+ if (elapsed < this.delayMs) return;
250
269
  const { value, done } = this.sample(now, elapsed - this.delayMs);
251
- this.lastSampledAt = now;
270
+ this.scheduleNext(now);
252
271
  if (this.lastWritten === void 0 || !this.valuesEqual(this.lastWritten, value)) {
253
272
  this.lastWritten = value;
254
273
  this.cell.value = value;
@@ -256,6 +275,25 @@ var UiAnimation = class {
256
275
  }
257
276
  if (done) this.finish();
258
277
  }
278
+ /**
279
+ * Moves the ideal timeline on by one step.
280
+ *
281
+ * Resynchronised rather than caught up when a whole step has already
282
+ * gone by: that means the application stalled, or the tab was
283
+ * hidden, and the samples that were missed are not worth taking now.
284
+ * For a video they are frames already past, and presenting is a pure
285
+ * function of a position, so chasing the backlog would sample
286
+ * several times in a row to arrive exactly where a single sample
287
+ * arrives anyway.
288
+ */
289
+ scheduleNext(now) {
290
+ if (this.stepMs <= 0) {
291
+ this.nextDueAt = now;
292
+ return;
293
+ }
294
+ const next = (this.nextDueAt ?? now) + this.stepMs;
295
+ this.nextDueAt = next > now ? next : now + this.stepMs;
296
+ }
259
297
  /** Writes the target and completes, for reduced motion and for `duration: 0`. */
260
298
  snap() {
261
299
  if (this.finished) return;
@@ -1704,6 +1742,9 @@ var Host = class {
1704
1742
  flowBox() {
1705
1743
  return this.services.layout?.flowBox(this.node) ?? null;
1706
1744
  }
1745
+ viewportBox() {
1746
+ return this.services.layout?.viewport?.() ?? null;
1747
+ }
1707
1748
  onLayout(listener) {
1708
1749
  const layout = this.services.layout;
1709
1750
  if (layout === void 0) {
@@ -6892,6 +6933,27 @@ function warnGeometryNeedsPositioning(name, position, reason) {
6892
6933
  //#endregion
6893
6934
  //#region src/modifiers/video.ts
6894
6935
  /**
6936
+ * The slowest and fastest a clip may be asked to run.
6937
+ *
6938
+ * Not a decoder limit — a decoder will run as fast as it is fed — but
6939
+ * a *sanity* one. Zero would be a pause spelled as a rate and would
6940
+ * divide by itself below; a negative rate is a request to run the
6941
+ * decoder backwards, which is not a thing it does; and past about four
6942
+ * the frame queue cannot stay ahead and the clip judders instead of
6943
+ * speeding up. Clamped rather than rejected, because a rate is the
6944
+ * kind of thing an application binds to a slider.
6945
+ */
6946
+ const MIN_RATE = .0625;
6947
+ const MAX_RATE = 4;
6948
+ /**
6949
+ * How close to the end counts as having reached it.
6950
+ *
6951
+ * A tween's last sample lands on its target exactly, but a clip whose
6952
+ * tween was cancelled a frame early — by a pause, by a seek — must not
6953
+ * be mistaken for one that finished and be looped back to the start.
6954
+ */
6955
+ const END_TOLERANCE_MS = 1;
6956
+ /**
6895
6957
  * Plays a video onto the node's `video` property.
6896
6958
  *
6897
6959
  * A modifier rather than a subscription in a component body for the
@@ -6903,20 +6965,29 @@ function warnGeometryNeedsPositioning(name, position, reason) {
6903
6965
  *
6904
6966
  * **Time comes from the animation driver, not from a timer.** The
6905
6967
  * playback is a pure function of a position, and the position is
6906
- * driven by a repeating linear tween over the video's duration — an
6907
- * ordinary animation on an ordinary cell. So a video is scheduled by
6908
- * the same `ticks` phase as everything else, keeps its own frames
6909
- * coming through the same `nextTickAt`, drops frames rather than
6910
- * falling behind under load, and stops dead when the runtime does.
6911
- * Nothing new had to be added to the runtime to play a video.
6912
- *
6913
- * **It keeps moving under reduced motion**, and that is deliberate.
6914
- * The rule this framework applies (`UiReducedMotionPolicy`) is to stop
6915
- * only when standing still would not state something false, and a
6916
- * video that has frozen on its first frame is a video that has
6917
- * finished loading badly. The media tier makes the same call for the
6918
- * `Spinner`. An app that wants a still under reduced motion passes
6919
- * `autoplay: false` and decides for itself.
6968
+ * driven by a linear tween over the video's duration — an ordinary
6969
+ * animation on an ordinary cell. So a video is scheduled by the same
6970
+ * `ticks` phase as everything else, keeps its own frames coming
6971
+ * through the same `nextTickAt`, drops frames rather than falling
6972
+ * behind under load, and stops dead when the runtime does. Nothing new
6973
+ * had to be added to the runtime to play a video.
6974
+ *
6975
+ * **Unless something else owns time.** A `clock` in the arguments
6976
+ * replaces the tween, and is how a clip keeps step with sound that is
6977
+ * being played somewhere this thread cannot reach. The rest of this
6978
+ * file does not care which of the two it is driven by: both end at the
6979
+ * same cell.
6980
+ *
6981
+ * **It keeps moving under reduced motion**, and that is deliberate for
6982
+ * a clip with no transport. The rule this framework applies
6983
+ * (`UiReducedMotionPolicy`) is to stop only when standing still would
6984
+ * not state something false, and a video that has frozen on its first
6985
+ * frame is a video that has finished loading badly. The media tier
6986
+ * makes the same call for the `Spinner`. A clip the reader can *drive*
6987
+ * is the other case — there the stillness is not a failure, it is a
6988
+ * clip waiting to be played — so `Video` declines to autoplay under
6989
+ * reduced motion when it has been given controls, and this modifier
6990
+ * is told so through `autoplay`.
6920
6991
  *
6921
6992
  * **Playback is shared by source, not by node.** The resolver
6922
6993
  * reference-counts, so two nodes pointed at one file watch one
@@ -6938,10 +7009,14 @@ function warnGeometryNeedsPositioning(name, position, reason) {
6938
7009
  * The video appeared to freeze, the morph juddered, and when the old
6939
7010
  * screen finally left, the clip carried on from the beginning.
6940
7011
  *
6941
- * So the position this node presents is its own tween plus whatever
6942
- * the playback had already reached when it resolved. Both holders then
6943
- * agree to within a frame, no seek fires, and the continuity this
6944
- * comment claims is actually true.
7012
+ * So a node starts its tween at whatever the playback had already
7013
+ * reached, and the cell holds an absolute position in the clip rather
7014
+ * than a distance from where this node joined. Both holders then agree
7015
+ * to within a frame, no seek fires, and the continuity this comment
7016
+ * claims is actually true. The earlier version of this kept a separate
7017
+ * offset and wrapped the sum; holding the position outright is the
7018
+ * same arithmetic with one fewer thing to get wrong, and it is what
7019
+ * makes `seek` a single assignment.
6945
7020
  */
6946
7021
  const videoSource = defineModifier({
6947
7022
  name: "videoSource",
@@ -6958,16 +7033,33 @@ function load(host, args) {
6958
7033
  let live = true;
6959
7034
  let playback = null;
6960
7035
  let stopErrors = null;
7036
+ let stopFrames = null;
7037
+ let stopClock = null;
6961
7038
  let positionMs = 0;
6962
- /**
6963
- * Where the playback already was when this element joined it, so the
6964
- * tween's zero means "here" rather than "the start of the file".
6965
- */
6966
- let offsetMs = 0;
6967
7039
  /** The clip's length, known only once it has been read. */
6968
7040
  let durationMs = 0;
7041
+ let paused = args.autoplay === false;
7042
+ let rate = clampRate(args.rate ?? 1);
7043
+ let volume = clampVolume(args.volume ?? 1);
7044
+ let muted = args.muted === true;
7045
+ let state = "loading";
7046
+ /**
7047
+ * Whether the node is somewhere a reader could see it. Assumed true
7048
+ * until layout says otherwise, so a clip is never held back waiting
7049
+ * for a measurement that a headless graph will never take.
7050
+ */
7051
+ let onScreen = true;
7052
+ const changeListeners = /* @__PURE__ */ new Set();
6969
7053
  args.onState?.("loading");
6970
7054
  host.clear("video");
7055
+ const announce = (next, error) => {
7056
+ state = next;
7057
+ args.onState?.(next, error);
7058
+ changed();
7059
+ };
7060
+ const changed = () => {
7061
+ for (const listener of changeListeners) listener();
7062
+ };
6971
7063
  const position = {
6972
7064
  get value() {
6973
7065
  return positionMs;
@@ -6975,39 +7067,235 @@ function load(host, args) {
6975
7067
  set value(next) {
6976
7068
  positionMs = next;
6977
7069
  if (playback === null) return;
6978
- const at = durationMs > 0 ? (next + offsetMs) % durationMs : next;
6979
- if (playback.present(at)) host.requestFrame();
7070
+ if (playback.present(next)) host.requestFrame();
6980
7071
  }
6981
7072
  };
6982
- args.resolver.resolve(args.source).then((resolved) => {
6983
- if (!live) return;
6984
- playback = resolved;
6985
- durationMs = Math.max(1, resolved.duration * 1e3);
6986
- offsetMs = resolved.positionMs;
6987
- host.set("video", resolved.surface);
6988
- stopErrors = resolved.onError((error) => args.onState?.("failed", error));
6989
- args.onState?.("playing");
6990
- if (args.autoplay === false) {
6991
- resolved.present(offsetMs);
6992
- host.requestFrame();
7073
+ /**
7074
+ * Runs the clip from where it stands to its end.
7075
+ *
7076
+ * A fresh tween each time rather than one repeating tween for the
7077
+ * life of the clip, which is what this used to be. A repeating tween
7078
+ * always starts from the value the cell held when it was built and
7079
+ * always runs the whole duration, so there is no way to begin one
7080
+ * part-way through — which is to say no way to seek, no way to
7081
+ * resume after a pause, and no way to change rate without the clip
7082
+ * jumping. Taking the loop apart into "play to the end, then decide"
7083
+ * costs one subscription per lap and buys all three.
7084
+ */
7085
+ const run = () => {
7086
+ if (playback === null || durationMs <= 0) return;
7087
+ const remaining = durationMs - positionMs;
7088
+ if (remaining <= END_TOLERANCE_MS) {
7089
+ finish();
6993
7090
  return;
6994
7091
  }
6995
7092
  host.animate(position, durationMs, {
6996
- duration: durationMs,
7093
+ duration: remaining / rate,
6997
7094
  easing: linear,
6998
- repeat: args.loop !== false,
6999
7095
  reducedMotion: "keep",
7000
- stepMs: resolved.frameDurationMs
7001
- });
7096
+ stepMs: playback.frameDurationMs / rate
7097
+ }).subscribe({ complete: () => {
7098
+ if (live && !paused && durationMs - positionMs <= END_TOLERANCE_MS) finish();
7099
+ } });
7100
+ };
7101
+ /** The clip reached its end: back to the start, or stop on the last frame. */
7102
+ const finish = () => {
7103
+ if (args.loop === false) {
7104
+ paused = true;
7105
+ changed();
7106
+ return;
7107
+ }
7108
+ position.value = 0;
7109
+ run();
7110
+ changed();
7111
+ };
7112
+ /** Whether time should be moving, given everything that can stop it. */
7113
+ const shouldRun = () => live && !paused && onScreen && playback !== null;
7114
+ /**
7115
+ * Brings the driver in line with `shouldRun`.
7116
+ *
7117
+ * Every one of pause, play, seek, rate and visibility ends here
7118
+ * rather than each starting and stopping the tween itself, because
7119
+ * the question they all really ask is the same one and the answer
7120
+ * has to survive them happening together: a clip scrolled off screen
7121
+ * while paused must not start playing when it comes back into view.
7122
+ */
7123
+ const sync = () => {
7124
+ host.stopAnimation(position);
7125
+ if (shouldRun()) run();
7126
+ };
7127
+ const transport = {
7128
+ get duration() {
7129
+ return durationMs / 1e3;
7130
+ },
7131
+ get position() {
7132
+ return positionMs / 1e3;
7133
+ },
7134
+ get paused() {
7135
+ return paused;
7136
+ },
7137
+ get rate() {
7138
+ return rate;
7139
+ },
7140
+ get volume() {
7141
+ return volume;
7142
+ },
7143
+ get muted() {
7144
+ return muted;
7145
+ },
7146
+ get seeking() {
7147
+ return playback?.seeking ?? false;
7148
+ },
7149
+ get state() {
7150
+ return state;
7151
+ },
7152
+ play() {
7153
+ if (!paused) return;
7154
+ paused = false;
7155
+ if (durationMs > 0 && durationMs - positionMs <= END_TOLERANCE_MS) position.value = 0;
7156
+ sync();
7157
+ changed();
7158
+ },
7159
+ pause() {
7160
+ if (paused) return;
7161
+ paused = true;
7162
+ sync();
7163
+ changed();
7164
+ },
7165
+ seek(seconds) {
7166
+ const wanted = Math.min(Math.max(0, seconds * 1e3), durationMs);
7167
+ position.value = wanted;
7168
+ sync();
7169
+ changed();
7170
+ },
7171
+ setRate(next) {
7172
+ const clamped = clampRate(next);
7173
+ if (clamped === rate) return;
7174
+ rate = clamped;
7175
+ sync();
7176
+ changed();
7177
+ },
7178
+ setVolume(next) {
7179
+ const clamped = clampVolume(next);
7180
+ const unmuted = clamped > 0 ? false : muted;
7181
+ if (clamped === volume && unmuted === muted) return;
7182
+ volume = clamped;
7183
+ muted = unmuted;
7184
+ args.onVolume?.(volume, muted);
7185
+ changed();
7186
+ },
7187
+ setMuted(next) {
7188
+ if (next === muted) return;
7189
+ muted = next;
7190
+ args.onVolume?.(volume, muted);
7191
+ changed();
7192
+ },
7193
+ retry() {
7194
+ if (!live) return;
7195
+ live = false;
7196
+ host.stopAnimation(position);
7197
+ stopErrors?.();
7198
+ stopFrames?.();
7199
+ stopClock?.();
7200
+ args.resolver.release(args.source);
7201
+ host.clear("video");
7202
+ load(host, args);
7203
+ },
7204
+ onChange(listener) {
7205
+ changeListeners.add(listener);
7206
+ return () => changeListeners.delete(listener);
7207
+ }
7208
+ };
7209
+ args.resolver.resolve(args.source).then((resolved) => {
7210
+ if (!live) return;
7211
+ playback = resolved;
7212
+ durationMs = Math.max(1, resolved.duration * 1e3);
7213
+ positionMs = Math.min(resolved.positionMs, durationMs);
7214
+ host.set("video", resolved.surface);
7215
+ stopErrors = resolved.onError((error) => announce("failed", error));
7216
+ stopFrames = resolved.onFrame?.(() => host.requestFrame()) ?? null;
7217
+ announce("playing");
7218
+ resolved.present(positionMs);
7219
+ host.requestFrame();
7220
+ attachClock();
7221
+ sync();
7222
+ args.onReady?.(transport);
7002
7223
  }).catch((error) => {
7003
- if (live) args.onState?.("failed", error);
7224
+ if (live) announce("failed", error);
7004
7225
  });
7226
+ /**
7227
+ * Follows an external clock, when there is one.
7228
+ *
7229
+ * The tween is never built in this case: `sync` still decides
7230
+ * whether time should be moving, but what moves it is the clock's
7231
+ * own progress, sampled once per frame through the same cell the
7232
+ * tween would have written.
7233
+ */
7234
+ function attachClock() {
7235
+ const clock = args.clock;
7236
+ if (clock === void 0) return;
7237
+ const followClock = () => {
7238
+ position.value = Math.min(Math.max(0, clock.positionSeconds() * 1e3), durationMs);
7239
+ };
7240
+ stopClock = clock.onChange(() => {
7241
+ paused = !clock.running;
7242
+ followClock();
7243
+ changed();
7244
+ });
7245
+ paused = !clock.running;
7246
+ followClock();
7247
+ }
7005
7248
  host.own(() => {
7006
7249
  live = false;
7007
7250
  host.stopAnimation(position);
7008
7251
  stopErrors?.();
7252
+ stopFrames?.();
7253
+ stopClock?.();
7254
+ changeListeners.clear();
7009
7255
  args.resolver.release(args.source);
7010
7256
  });
7257
+ /**
7258
+ * Whether the node is somewhere worth decoding for.
7259
+ *
7260
+ * A clip scrolled out of the viewport still fetches, decodes,
7261
+ * converts to a bitmap and uploads a texture sixty times a second
7262
+ * for nobody, and the only thing that stops it today is the tab
7263
+ * being hidden — which the runtime already handles, at the driver.
7264
+ * Within a visible page nothing did, so a page of clips cost the sum
7265
+ * of all of them however few were on screen.
7266
+ *
7267
+ * The test is the node's box against the root's, with no margin: a
7268
+ * clip is either somewhere a reader could see it or it is not, and a
7269
+ * clip that starts a frame late coming into view is a far smaller
7270
+ * fault than one that never stops. What it must not do is *seek* —
7271
+ * the position is left exactly where it stopped, so coming back into
7272
+ * view resumes rather than reloads, and the decoder is not reset.
7273
+ */
7274
+ function watchVisibility() {
7275
+ if (args.pauseWhenHidden === false) return;
7276
+ const check = () => {
7277
+ const box = host.layoutBox();
7278
+ const viewport = host.viewportBox();
7279
+ if (box === null || viewport === null) return;
7280
+ if (box.width <= 0 || box.height <= 0) return;
7281
+ const visible = box.x < viewport.x + viewport.width && box.x + box.width > viewport.x && box.y < viewport.y + viewport.height && box.y + box.height > viewport.y;
7282
+ if (visible === onScreen) return;
7283
+ onScreen = visible;
7284
+ sync();
7285
+ changed();
7286
+ };
7287
+ host.onLayout(check);
7288
+ check();
7289
+ }
7290
+ watchVisibility();
7291
+ }
7292
+ function clampVolume(volume) {
7293
+ if (!Number.isFinite(volume)) return 1;
7294
+ return Math.min(1, Math.max(0, volume));
7295
+ }
7296
+ function clampRate(rate) {
7297
+ if (!Number.isFinite(rate)) return 1;
7298
+ return Math.min(MAX_RATE, Math.max(MIN_RATE, rate));
7011
7299
  }
7012
7300
  //#endregion
7013
7301
  //#region src/modifiers/scroll.ts
@@ -11324,6 +11612,13 @@ function readFullBoxVersion(reader) {
11324
11612
  reader.skip(3);
11325
11613
  return version;
11326
11614
  }
11615
+ /** The same, for the boxes whose flags say which fields are present. */
11616
+ function readFullBox(reader) {
11617
+ return {
11618
+ version: reader.u8(),
11619
+ flags: reader.u8() << 16 | reader.u8() << 8 | reader.u8()
11620
+ };
11621
+ }
11327
11622
  /**
11328
11623
  * Reads the first video track out of an MP4.
11329
11624
  *
@@ -11333,25 +11628,13 @@ function readFullBoxVersion(reader) {
11333
11628
  * not understand this file", and those want different fixes.
11334
11629
  */
11335
11630
  function demuxMp4Video(data) {
11336
- const reader = new Reader(new DataView(data));
11337
- const limit = data.byteLength;
11338
- let moov = null;
11339
- let sawFragment = false;
11340
- let sawFtyp = false;
11341
- walkBoxes(reader, 0, limit, (box) => {
11342
- if (box.type === "ftyp") sawFtyp = true;
11343
- else if (box.type === "moov") moov = box;
11344
- else if (box.type === "moof" || box.type === "sidx") sawFragment = true;
11345
- });
11631
+ const { reader, moov, moofs, sawFtyp } = openMp4(data);
11346
11632
  if (!sawFtyp && moov === null) throw new Error("Not an MP4: no ftyp or moov box at the top level of the file.");
11347
11633
  if (moov === null) throw new Error("This MP4 has no moov box, so there is no sample table to read.");
11348
- const table = findVideoTrack(reader, moov);
11349
- if (table === null) {
11350
- if (sawFragment) throw new Error("This is a fragmented MP4 (it has moof boxes), which this demuxer does not read. Remux it to a progressive file: `ffmpeg -i in.mp4 -c copy -movflags +faststart out.mp4`.");
11351
- throw new Error("This MP4 has no video track, or none this demuxer recognises.");
11352
- }
11353
- const samples = buildSamples(table);
11354
- if (samples.length === 0) throw new Error(sawFragment ? "This MP4 has an empty sample table and moof boxes: it is fragmented, which this demuxer does not read." : "This MP4 video track has no samples.");
11634
+ const table = findTrack(reader, moov, "vide");
11635
+ if (table === null) throw new Error("This MP4 has no video track, or none this demuxer recognises.");
11636
+ const samples = samplesOf(reader, table, moov, moofs);
11637
+ if (samples.length === 0) throw new Error(moofs.length > 0 ? "This fragmented MP4 has no samples for its video track. Its fragments may be in other files: this reads the fragments in the buffer it was given, not an index of segments to fetch." : "This MP4 video track has no samples.");
11355
11638
  let durationUs = 0;
11356
11639
  let frameDurationUs = Infinity;
11357
11640
  for (const sample of samples) {
@@ -11368,24 +11651,99 @@ function demuxMp4Video(data) {
11368
11651
  frameDurationUs: Number.isFinite(frameDurationUs) ? frameDurationUs : Math.round(1e6 / 60)
11369
11652
  };
11370
11653
  }
11371
- function findVideoTrack(reader, moov) {
11654
+ /** The top-level walk both entry points start from. */
11655
+ function openMp4(data) {
11656
+ const reader = new Reader(new DataView(data));
11657
+ const limit = data.byteLength;
11658
+ let moov = null;
11659
+ const moofs = [];
11660
+ let sawFtyp = false;
11661
+ walkBoxes(reader, 0, limit, (box) => {
11662
+ if (box.type === "ftyp") sawFtyp = true;
11663
+ else if (box.type === "moov") moov = box;
11664
+ else if (box.type === "moof") moofs.push(box);
11665
+ });
11666
+ return {
11667
+ reader,
11668
+ moov,
11669
+ moofs,
11670
+ sawFtyp
11671
+ };
11672
+ }
11673
+ /**
11674
+ * Moves a track's presentation times so the first picture is at zero.
11675
+ *
11676
+ * **A file's earliest presentation timestamp is not required to be
11677
+ * zero, and for anything encoded with B-frames it is not.** A `ctts`
11678
+ * shifts each sample's composition time forward of its decode time,
11679
+ * and the shift applied to the first sample is the reorder depth: two
11680
+ * frames, for the clips x264 writes by default. So a plain six second
11681
+ * clip at 24fps has its first picture at 83333us and its last ending
11682
+ * at 6.083s, and a player that believed those numbers would report a
11683
+ * duration 83ms too long and, far worse, have no picture at all to
11684
+ * show for position zero.
11685
+ *
11686
+ * That last part is not hypothetical. It is exactly what a paused
11687
+ * clip looked like before this existed: `present(0)` found nothing
11688
+ * due, because nothing *was* due, and since a paused clip never asks
11689
+ * again the still stayed blank forever. A playing one hid it by
11690
+ * advancing past 83ms within two frames.
11691
+ *
11692
+ * Subtracting a constant keeps every sample's order, duration and
11693
+ * relationship to its neighbours; all it changes is where the clock
11694
+ * starts. Which is what an edit list would usually be doing, and is
11695
+ * the part of one worth having.
11696
+ */
11697
+ function rebaseToZero(samples) {
11698
+ if (samples.length === 0) return samples;
11699
+ let earliest = Infinity;
11700
+ for (const sample of samples) earliest = Math.min(earliest, sample.timestampUs);
11701
+ if (earliest <= 0 || !Number.isFinite(earliest)) return samples;
11702
+ return samples.map((sample) => ({
11703
+ ...sample,
11704
+ timestampUs: sample.timestampUs - earliest
11705
+ }));
11706
+ }
11707
+ /**
11708
+ * A track's samples, from wherever this file keeps them.
11709
+ *
11710
+ * The two shapes are not a spectrum: a progressive file's samples are
11711
+ * in tables in the `moov` and a fragmented file's are described inline
11712
+ * in each `moof`, and a file is one or the other. `mvex` is the
11713
+ * declaration — a file that has it is fragmented even if this buffer
11714
+ * happens to hold no fragments yet — and the sample tables are the
11715
+ * fallback, so a file with neither comes back empty rather than
11716
+ * half-read.
11717
+ */
11718
+ function samplesOf(reader, table, moov, moofs) {
11719
+ const defaults = readTrackExtends(reader, moov);
11720
+ return rebaseToZero(defaults.size === 0 && moofs.length === 0 ? buildSamples(table) : buildFragmentedSamples(reader, moofs, table, defaults));
11721
+ }
11722
+ function findTrack(reader, moov, kind) {
11372
11723
  let found = null;
11373
11724
  walkBoxes(reader, moov.start, moov.end, (trak) => {
11374
11725
  if (trak.type !== "trak" || found !== null) return;
11375
- const table = readTrack(reader, trak);
11726
+ const table = readTrack(reader, trak, kind);
11376
11727
  if (table !== null) found = table;
11377
11728
  });
11378
11729
  return found;
11379
11730
  }
11380
- function readTrack(reader, trak) {
11731
+ function readTrack(reader, trak, kind) {
11381
11732
  let mdia = null;
11733
+ let trackId = 0;
11382
11734
  walkBoxes(reader, trak.start, trak.end, (box) => {
11735
+ if (box.type === "tkhd") {
11736
+ const version = readFullBoxVersion(reader);
11737
+ reader.skip(version === 1 ? 16 : 8);
11738
+ trackId = reader.u32();
11739
+ return;
11740
+ }
11383
11741
  if (box.type === "mdia") mdia = box;
11384
11742
  });
11385
11743
  if (mdia === null) return null;
11386
11744
  const media = mdia;
11387
11745
  let timescale = 0;
11388
- let isVideo = false;
11746
+ let matches = false;
11389
11747
  let minf = null;
11390
11748
  walkBoxes(reader, media.start, media.end, (box) => {
11391
11749
  if (box.type === "mdhd") {
@@ -11401,20 +11759,22 @@ function readTrack(reader, trak) {
11401
11759
  if (box.type === "hdlr") {
11402
11760
  readFullBoxVersion(reader);
11403
11761
  reader.skip(4);
11404
- isVideo = reader.ascii(4) === "vide";
11762
+ matches = reader.ascii(4) === kind;
11405
11763
  return;
11406
11764
  }
11407
11765
  if (box.type === "minf") minf = box;
11408
11766
  });
11409
- if (!isVideo || minf === null || timescale <= 0) return null;
11767
+ if (!matches || minf === null || timescale <= 0) return null;
11410
11768
  let stbl = null;
11411
11769
  walkBoxes(reader, minf.start, minf.end, (box) => {
11412
11770
  if (box.type === "stbl") stbl = box;
11413
11771
  });
11414
11772
  if (stbl === null) return null;
11415
- return readSampleTable(reader, stbl, timescale);
11773
+ const table = readSampleTable(reader, stbl, timescale, kind);
11774
+ if (table !== null) table.trackId = trackId;
11775
+ return table;
11416
11776
  }
11417
- function readSampleTable(reader, stbl, timescale) {
11777
+ function readSampleTable(reader, stbl, timescale, kind) {
11418
11778
  const table = {
11419
11779
  timescale,
11420
11780
  deltas: [],
@@ -11425,12 +11785,17 @@ function readSampleTable(reader, stbl, timescale) {
11425
11785
  syncSamples: [],
11426
11786
  codec: "",
11427
11787
  codedWidth: 0,
11428
- codedHeight: 0
11788
+ codedHeight: 0,
11789
+ trackId: 0,
11790
+ sampleRate: 0,
11791
+ channels: 0,
11792
+ sampleSize: 0
11429
11793
  };
11430
11794
  walkBoxes(reader, stbl.start, stbl.end, (box) => {
11431
11795
  switch (box.type) {
11432
11796
  case "stsd":
11433
- readSampleDescription(reader, box, table);
11797
+ if (kind === "soun") readAudioSampleDescription(reader, box, table);
11798
+ else readSampleDescription(reader, box, table);
11434
11799
  break;
11435
11800
  case "stts": {
11436
11801
  readFullBoxVersion(reader);
@@ -11548,17 +11913,322 @@ function readSampleDescription(reader, stsd, table) {
11548
11913
  table.codec = format;
11549
11914
  return;
11550
11915
  }
11551
- if (child.type === "av1C" || child.type === "vpcC") {
11552
- table.description = reader.bytes(child.end - child.start);
11553
- table.codec = format;
11916
+ if (child.type === "av1C") {
11917
+ const record = reader.bytes(child.end - child.start);
11918
+ table.codec = av1CodecString(format, record);
11919
+ return;
11920
+ }
11921
+ if (child.type === "vpcC") {
11922
+ const record = reader.bytes(child.end - child.start);
11923
+ table.codec = vp9CodecString(format, record);
11554
11924
  }
11555
11925
  });
11556
11926
  if (table.codec === "") table.codec = format;
11557
11927
  }
11928
+ /**
11929
+ * The audio sample entry, and the codec configuration inside it.
11930
+ *
11931
+ * The fixed fields are `AudioSampleEntry`'s: six reserved bytes and a
11932
+ * data reference index, then eight bytes that were a version, a
11933
+ * revision and a vendor in QuickTime and are reserved here, the
11934
+ * channel count, the sample size, two pre-defined and two reserved,
11935
+ * and a sample rate written as 16.16 fixed point — of which only the
11936
+ * whole part is ever meaningful, because no encoder writes a
11937
+ * fractional sample rate.
11938
+ *
11939
+ * **Version 1 and 2 entries are not read past their fixed fields.**
11940
+ * A version 1 `AudioSampleEntry` carries four extra fields and a
11941
+ * version 2 carries a different layout entirely; both are rare outside
11942
+ * QuickTime, and the child boxes this actually wants are found by
11943
+ * walking from the end of whichever header was read. Getting the
11944
+ * header length wrong would make that walk start mid-box and find
11945
+ * nothing, which is why the version is read rather than assumed.
11946
+ */
11947
+ function readAudioSampleDescription(reader, stsd, table) {
11948
+ readFullBoxVersion(reader);
11949
+ if (reader.u32() === 0) return;
11950
+ const entry = readBoxHeader(reader, stsd.end);
11951
+ if (entry === null) return;
11952
+ reader.skip(6);
11953
+ reader.skip(2);
11954
+ const version = reader.u16();
11955
+ reader.skip(6);
11956
+ table.channels = reader.u16();
11957
+ table.sampleSize = reader.u16();
11958
+ reader.skip(2);
11959
+ reader.skip(2);
11960
+ table.sampleRate = reader.u16();
11961
+ reader.skip(2);
11962
+ if (version === 1) reader.skip(16);
11963
+ else if (version === 2) reader.skip(36);
11964
+ const format = entry.type;
11965
+ table.codec = format;
11966
+ walkBoxes(reader, reader.offset, entry.end, (child) => {
11967
+ if (child.type === "esds") {
11968
+ readElementaryStreamDescriptor(reader, child, table, format);
11969
+ return;
11970
+ }
11971
+ if (child.type === "dOps" || child.type === "alac" || child.type === "dfLa") table.description = reader.bytes(child.end - child.start);
11972
+ });
11973
+ }
11974
+ /**
11975
+ * The `esds` box, for the two numbers a codec string needs.
11976
+ *
11977
+ * An ES descriptor is a little tag-length-value tree, and this walks
11978
+ * exactly as far into it as `mp4a.40.2` requires: the
11979
+ * `DecoderConfigDescriptor` for the object type indication (`40` for
11980
+ * MPEG-4 audio), and the `DecoderSpecificInfo` inside it, whose first
11981
+ * five bits are the audio object type (`2` for AAC-LC). That inner
11982
+ * payload is also the `AudioSpecificConfig` that `AudioDecoder` wants
11983
+ * as its description, so it is kept whole.
11984
+ *
11985
+ * Lengths are written in a variable-length form where the top bit of
11986
+ * each byte says another follows — the same encoding MIDI uses for
11987
+ * delta times and MP4 inherited from MPEG-4 systems.
11988
+ */
11989
+ function readElementaryStreamDescriptor(reader, esds, table, format) {
11990
+ readFullBoxVersion(reader);
11991
+ const end = esds.end;
11992
+ const readLength = () => {
11993
+ let length = 0;
11994
+ for (let index = 0; index < 4; index++) {
11995
+ const byte = reader.u8();
11996
+ length = length << 7 | byte & 127;
11997
+ if ((byte & 128) === 0) break;
11998
+ }
11999
+ return length;
12000
+ };
12001
+ while (reader.offset < end) {
12002
+ const tag = reader.u8();
12003
+ const length = readLength();
12004
+ const payloadEnd = reader.offset + length;
12005
+ if (payloadEnd > end) return;
12006
+ if (tag === 3) {
12007
+ reader.skip(2);
12008
+ const flags = reader.u8();
12009
+ if ((flags & 128) !== 0) reader.skip(2);
12010
+ if ((flags & 64) !== 0) reader.skip(reader.u8());
12011
+ if ((flags & 32) !== 0) reader.skip(2);
12012
+ continue;
12013
+ }
12014
+ if (tag === 4) {
12015
+ const objectType = reader.u8();
12016
+ reader.skip(12);
12017
+ table.codec = `${format}.${objectType.toString(16)}`;
12018
+ continue;
12019
+ }
12020
+ if (tag === 5) {
12021
+ const config = reader.bytes(length);
12022
+ table.description = config;
12023
+ const first = config[0];
12024
+ if (first !== void 0) {
12025
+ const audioObjectType = first >> 3;
12026
+ if (audioObjectType !== 0 && audioObjectType !== 31) table.codec = `${table.codec}.${audioObjectType}`;
12027
+ }
12028
+ return;
12029
+ }
12030
+ reader.offset = payloadEnd;
12031
+ }
12032
+ }
12033
+ /**
12034
+ * Reads the first audio track out of an MP4.
12035
+ *
12036
+ * Null rather than a throw for a file that simply has no audio, which
12037
+ * is the ordinary case for the clips this framework plays and is not
12038
+ * an error in any sense. A file that *has* an audio track this cannot
12039
+ * read still throws, for the reason `demuxMp4Video` gives: "no audio
12040
+ * track" and "an audio track I do not understand" want different
12041
+ * fixes.
12042
+ */
12043
+ function demuxMp4Audio(data) {
12044
+ const { reader, moov, moofs } = openMp4(data);
12045
+ if (moov === null) return null;
12046
+ const table = findTrack(reader, moov, "soun");
12047
+ if (table === null) return null;
12048
+ const samples = samplesOf(reader, table, moov, moofs);
12049
+ if (samples.length === 0) throw new Error("This MP4 has an audio track with no samples this demuxer can read.");
12050
+ let durationUs = 0;
12051
+ for (const sample of samples) durationUs = Math.max(durationUs, sample.timestampUs + sample.durationUs);
12052
+ return {
12053
+ codec: table.codec,
12054
+ sampleRate: table.sampleRate,
12055
+ channels: table.channels,
12056
+ sampleSize: table.sampleSize,
12057
+ description: table.description,
12058
+ samples,
12059
+ durationUs
12060
+ };
12061
+ }
12062
+ /**
12063
+ * `av01.0.04M.08`, out of the `av1C` record.
12064
+ *
12065
+ * The bare format is not a codec string for AV1 — `VideoDecoder`
12066
+ * answers `supported: false` for `av01` alone — so a file whose codec
12067
+ * was reported as just the format never played at all. Every field
12068
+ * below is in the record's first three bytes, which is the whole of
12069
+ * the fixed part of an `AV1CodecConfigurationRecord`:
12070
+ *
12071
+ * byte 0: a marker bit and a version
12072
+ * byte 1: seq_profile (3 bits), seq_level_idx_0 (5 bits)
12073
+ * byte 2: seq_tier_0, high_bitdepth, twelve_bit, monochrome,
12074
+ * chroma subsampling and sample position
12075
+ *
12076
+ * The string wants the profile, the level as two decimal digits, the
12077
+ * tier as `M` or `H`, and the bit depth as two decimal digits. Falls
12078
+ * back to the format for a record too short to read, where claiming a
12079
+ * precise string would be worse than claiming none.
12080
+ */
12081
+ function av1CodecString(format, record) {
12082
+ const second = record[1];
12083
+ const third = record[2];
12084
+ if (second === void 0 || third === void 0) return format;
12085
+ const profile = second >> 5;
12086
+ const level = second & 31;
12087
+ const tier = (third & 128) !== 0 ? "H" : "M";
12088
+ const highBitDepth = (third & 64) !== 0;
12089
+ const depth = (third & 32) !== 0 && profile === 2 ? 12 : highBitDepth ? 10 : 8;
12090
+ return `${format}.${profile}.${pad2(level)}${tier}.${pad2(depth)}`;
12091
+ }
12092
+ /**
12093
+ * `vp09.00.10.08`, out of the `vpcC` record.
12094
+ *
12095
+ * Here for the same reason the AV1 one is, and simpler: `vpcC` is a
12096
+ * full box, so a version and three flag bytes come first, and then the
12097
+ * profile, the level and a byte whose top four bits are the bit depth.
12098
+ */
12099
+ function vp9CodecString(format, record) {
12100
+ const profile = record[4];
12101
+ const level = record[5];
12102
+ const packed = record[6];
12103
+ if (profile === void 0 || level === void 0 || packed === void 0) return format;
12104
+ return `${format}.${pad2(profile)}.${pad2(level)}.${pad2(packed >> 4)}`;
12105
+ }
12106
+ function pad2(value) {
12107
+ return value.toString(10).padStart(2, "0");
12108
+ }
11558
12109
  function hex2(value) {
11559
12110
  return value.toString(16).padStart(2, "0").toUpperCase();
11560
12111
  }
11561
12112
  /**
12113
+ * The `trex` defaults, one per track, out of `moov`/`mvex`.
12114
+ *
12115
+ * `mvex`'s presence is also the honest test for a fragmented file. The
12116
+ * `moof` boxes are the fragments, but a file may legitimately have its
12117
+ * first fragment far enough in that a parser has not reached one yet,
12118
+ * whereas `mvex` is in the `moov` and says up front that fragments are
12119
+ * how this file is built.
12120
+ */
12121
+ function readTrackExtends(reader, moov) {
12122
+ const defaults = /* @__PURE__ */ new Map();
12123
+ walkBoxes(reader, moov.start, moov.end, (box) => {
12124
+ if (box.type !== "mvex") return;
12125
+ walkBoxes(reader, box.start, box.end, (child) => {
12126
+ if (child.type !== "trex") return;
12127
+ readFullBoxVersion(reader);
12128
+ const trackId = reader.u32();
12129
+ reader.skip(4);
12130
+ defaults.set(trackId, {
12131
+ trackId,
12132
+ defaultSampleDuration: reader.u32(),
12133
+ defaultSampleSize: reader.u32(),
12134
+ defaultSampleFlags: reader.u32()
12135
+ });
12136
+ });
12137
+ });
12138
+ return defaults;
12139
+ }
12140
+ /**
12141
+ * Builds a track's samples out of the file's fragments.
12142
+ *
12143
+ * A fragmented MP4 says nothing up front about where its samples are:
12144
+ * there is no `stco`, no `stsz` and no `stts`, and the `stbl` in the
12145
+ * `moov` is a shell holding only the `stsd`. Instead each `moof`
12146
+ * carries a `traf` per track, and each `traf` a `trun` per run of
12147
+ * samples, describing sizes and durations inline — with anything
12148
+ * omitted falling back to a default from `tfhd`, and failing that from
12149
+ * `trex`. So this is a walk rather than a table lookup, and it is why
12150
+ * the two shapes could not share `buildSamples`.
12151
+ *
12152
+ * **Offsets are the part that goes wrong.** A sample's position is
12153
+ * `base_data_offset` plus the `trun`'s own `data_offset` plus the
12154
+ * sizes of the samples before it in that run, and `base_data_offset`
12155
+ * is one of three things: written into the `tfhd`, or the start of
12156
+ * the enclosing `moof` when `default-base-is-moof` is set, or — for
12157
+ * the first `traf` — the `moof` again by the specification's older
12158
+ * rule. The second of those is what every fragmenter writes today and
12159
+ * what makes a fragment self-contained; taking the first `traf` rule
12160
+ * as the general one puts every fragment after the first at the wrong
12161
+ * offset, which decodes as noise rather than as an error.
12162
+ *
12163
+ * **Times accumulate across fragments.** `tfdt` states the decode time
12164
+ * a fragment starts at, and where it is absent the fragment continues
12165
+ * from wherever the last one ended.
12166
+ */
12167
+ function buildFragmentedSamples(reader, moofs, table, defaults) {
12168
+ const samples = [];
12169
+ const trex = defaults.get(table.trackId);
12170
+ let decodeTime = 0;
12171
+ for (const moof of moofs) {
12172
+ const moofStart = moof.start - 8;
12173
+ walkBoxes(reader, moof.start, moof.end, (traf) => {
12174
+ if (traf.type !== "traf") return;
12175
+ let trackId = 0;
12176
+ let baseDataOffset = moofStart;
12177
+ let defaultDuration = trex?.defaultSampleDuration ?? 0;
12178
+ let defaultSize = trex?.defaultSampleSize ?? 0;
12179
+ let defaultFlags = trex?.defaultSampleFlags ?? 0;
12180
+ const runs = [];
12181
+ let fragmentTime = null;
12182
+ walkBoxes(reader, traf.start, traf.end, (child) => {
12183
+ if (child.type === "tfhd") {
12184
+ const { flags } = readFullBox(reader);
12185
+ trackId = reader.u32();
12186
+ if ((flags & 1) !== 0) baseDataOffset = reader.u64();
12187
+ if ((flags & 2) !== 0) reader.skip(4);
12188
+ if ((flags & 8) !== 0) defaultDuration = reader.u32();
12189
+ if ((flags & 16) !== 0) defaultSize = reader.u32();
12190
+ if ((flags & 32) !== 0) defaultFlags = reader.u32();
12191
+ return;
12192
+ }
12193
+ if (child.type === "tfdt") {
12194
+ fragmentTime = readFullBoxVersion(reader) === 1 ? reader.u64() : reader.u32();
12195
+ return;
12196
+ }
12197
+ if (child.type === "trun") runs.push({
12198
+ header: child,
12199
+ at: child.start
12200
+ });
12201
+ });
12202
+ if (trackId !== table.trackId) return;
12203
+ if (fragmentTime !== null) decodeTime = fragmentTime;
12204
+ for (const run of runs) {
12205
+ reader.offset = run.at;
12206
+ const { version, flags } = readFullBox(reader);
12207
+ const count = reader.u32();
12208
+ let offset = baseDataOffset;
12209
+ if ((flags & 1) !== 0) offset += reader.i32();
12210
+ const firstFlags = (flags & 4) !== 0 ? reader.u32() : null;
12211
+ for (let index = 0; index < count; index++) {
12212
+ const duration = (flags & 256) !== 0 ? reader.u32() : defaultDuration;
12213
+ const size = (flags & 512) !== 0 ? reader.u32() : defaultSize;
12214
+ const sampleFlags = (flags & 1024) !== 0 ? reader.u32() : index === 0 && firstFlags !== null ? firstFlags : defaultFlags;
12215
+ const composition = (flags & 2048) !== 0 ? version === 0 ? reader.i32() : reader.i32() : 0;
12216
+ samples.push({
12217
+ offset,
12218
+ size,
12219
+ timestampUs: Math.round((decodeTime + composition) * 1e6 / table.timescale),
12220
+ durationUs: Math.round(duration * 1e6 / table.timescale),
12221
+ isKey: (sampleFlags & 65536) === 0
12222
+ });
12223
+ offset += size;
12224
+ decodeTime += duration;
12225
+ }
12226
+ }
12227
+ });
12228
+ }
12229
+ return samples;
12230
+ }
12231
+ /**
11562
12232
  * Walks the sample tables into a flat list.
11563
12233
  *
11564
12234
  * The one piece of real arithmetic in this file: `stsc` describes runs
@@ -11600,6 +12270,148 @@ function buildSamples(table) {
11600
12270
  return samples;
11601
12271
  }
11602
12272
  //#endregion
12273
+ //#region src/media/ByteSource.ts
12274
+ /** A file already in memory, which is every file this framework read before. */
12275
+ function bufferSource(data) {
12276
+ return {
12277
+ size: data.byteLength,
12278
+ read(offset, size) {
12279
+ if (offset < 0 || offset + size > data.byteLength) return null;
12280
+ return new Uint8Array(data, offset, size);
12281
+ },
12282
+ request() {
12283
+ return Promise.resolve();
12284
+ },
12285
+ close() {}
12286
+ };
12287
+ }
12288
+ /**
12289
+ * How much is fetched at a time.
12290
+ *
12291
+ * Big enough that a request is worth its round trip — a quarter of a
12292
+ * megabyte is a second or two of ordinary video, so a clip playing
12293
+ * forwards asks for one every second or so rather than every frame —
12294
+ * and small enough that a seek does not drag a great deal of video
12295
+ * nobody will watch.
12296
+ *
12297
+ * It is also what a reader's first request should be, so that the
12298
+ * bytes it read to find the header land on a block boundary and can
12299
+ * be kept; see `prefetched`. A megabyte, the first value here, was
12300
+ * larger than the whole of a short file, so the header probe could
12301
+ * never seed a block and the front of the file was always fetched
12302
+ * twice.
12303
+ */
12304
+ const DEFAULT_BLOCK_SIZE = 262144;
12305
+ /**
12306
+ * How many blocks to keep.
12307
+ *
12308
+ * Eight megabytes, which is enough that playing forwards never evicts
12309
+ * a block it is about to want, and a bound rather than a cache that
12310
+ * grows to the size of the file — which would make the whole exercise
12311
+ * pointless.
12312
+ */
12313
+ const BLOCK_CAPACITY = 32;
12314
+ /**
12315
+ * A file fetched a block at a time.
12316
+ *
12317
+ * Blocks rather than exact ranges, and aligned to their own size, so
12318
+ * that two samples in the same part of the file share one request and
12319
+ * so the cache can be keyed on a number. A sample straddling a block
12320
+ * boundary is copied out of the two blocks either side of it; every
12321
+ * other read is a view, which matters because the common case is a
12322
+ * frame's worth of bytes on the way to a decoder.
12323
+ *
12324
+ * **Eviction is by last use, not by position.** A viewer who seeks
12325
+ * backwards into what they just watched should find it still here, and
12326
+ * a strictly forward-looking policy would have thrown it away.
12327
+ */
12328
+ function rangeSource(source, options) {
12329
+ const blockSize = options.blockSize ?? 262144;
12330
+ const capacity = options.capacity ?? BLOCK_CAPACITY;
12331
+ const blocks = /* @__PURE__ */ new Map();
12332
+ const inFlight = /* @__PURE__ */ new Map();
12333
+ let closed = false;
12334
+ const blockOf = (offset) => Math.floor(offset / blockSize);
12335
+ /** Slices whatever the caller already had into blocks this can serve. */
12336
+ const seed = (start, data) => {
12337
+ const bytes = new Uint8Array(data);
12338
+ const end = start + bytes.length;
12339
+ for (let index = Math.ceil(start / blockSize);; index++) {
12340
+ const blockStart = index * blockSize;
12341
+ const blockEnd = Math.min(blockStart + blockSize, options.size);
12342
+ if (blockEnd > end) break;
12343
+ blocks.set(index, bytes.subarray(blockStart - start, blockEnd - start));
12344
+ if (blockEnd === options.size) break;
12345
+ }
12346
+ };
12347
+ /** Marks a block as the most recently used, which a Map's order gives us for free. */
12348
+ const touch = (index, bytes) => {
12349
+ blocks.delete(index);
12350
+ blocks.set(index, bytes);
12351
+ while (blocks.size > capacity) {
12352
+ const oldest = blocks.keys().next();
12353
+ if (oldest.done === true) break;
12354
+ blocks.delete(oldest.value);
12355
+ }
12356
+ };
12357
+ const fetchBlock = (index) => {
12358
+ const existing = inFlight.get(index);
12359
+ if (existing !== void 0) return existing;
12360
+ const start = index * blockSize;
12361
+ const end = Math.min(start + blockSize, options.size);
12362
+ const request = options.fetchRange(source, start, end).then((buffer) => {
12363
+ if (closed) return;
12364
+ touch(index, new Uint8Array(buffer));
12365
+ }).finally(() => inFlight.delete(index));
12366
+ inFlight.set(index, request);
12367
+ return request;
12368
+ };
12369
+ if (options.prefetched !== void 0) seed(options.prefetched.start, options.prefetched.data);
12370
+ return {
12371
+ size: options.size,
12372
+ read(offset, size) {
12373
+ if (offset < 0 || size < 0 || offset + size > options.size) return null;
12374
+ const first = blockOf(offset);
12375
+ const last = blockOf(offset + Math.max(0, size - 1));
12376
+ const head = blocks.get(first);
12377
+ if (head === void 0) return null;
12378
+ if (first === last) {
12379
+ touch(first, head);
12380
+ return head.subarray(offset - first * blockSize, offset - first * blockSize + size);
12381
+ }
12382
+ const out = new Uint8Array(size);
12383
+ let written = 0;
12384
+ for (let index = first; index <= last; index++) {
12385
+ const block = blocks.get(index);
12386
+ if (block === void 0) return null;
12387
+ const blockStart = index * blockSize;
12388
+ const from = Math.max(0, offset - blockStart);
12389
+ const take = Math.min(block.length - from, size - written);
12390
+ out.set(block.subarray(from, from + take), written);
12391
+ written += take;
12392
+ }
12393
+ for (let index = first; index <= last; index++) {
12394
+ const block = blocks.get(index);
12395
+ if (block !== void 0) touch(index, block);
12396
+ }
12397
+ return written === size ? out : null;
12398
+ },
12399
+ async request(offset, size) {
12400
+ if (closed || offset < 0 || offset >= options.size) return;
12401
+ const first = blockOf(offset);
12402
+ const last = blockOf(Math.min(offset + Math.max(0, size - 1), options.size - 1));
12403
+ const wanted = [];
12404
+ for (let index = first; index <= last; index++) if (!blocks.has(index)) wanted.push(fetchBlock(index));
12405
+ await Promise.all(wanted);
12406
+ },
12407
+ close() {
12408
+ closed = true;
12409
+ blocks.clear();
12410
+ inFlight.clear();
12411
+ }
12412
+ };
12413
+ }
12414
+ //#endregion
11603
12415
  //#region src/media/VideoResolver.ts
11604
12416
  /** Whether this thread can decode video at all. */
11605
12417
  function canDecodeVideo() {
@@ -11657,10 +12469,12 @@ var DefaultVideoResolver = class {
11657
12469
  evictable = [];
11658
12470
  capacity;
11659
12471
  fetchBuffer;
12472
+ fetchRange;
11660
12473
  disposed = false;
11661
12474
  constructor(options = {}) {
11662
12475
  this.capacity = options.capacity ?? DEFAULT_CAPACITY;
11663
12476
  this.fetchBuffer = options.fetch ?? defaultFetch;
12477
+ this.fetchRange = options.fetchRange;
11664
12478
  }
11665
12479
  /** Live or in-flight sources, for specs and the inspector. */
11666
12480
  get size() {
@@ -11716,13 +12530,103 @@ var DefaultVideoResolver = class {
11716
12530
  }
11717
12531
  async open(source) {
11718
12532
  if (!canDecodeVideo()) throw new Error("This thread has no WebCodecs VideoDecoder, so video cannot be played here. It is available in Chrome 94+ and in a worker; Safari and Firefox support varies.");
11719
- const data = await this.fetchBuffer(source);
11720
- const playback = new Mp4Playback(demuxMp4Video(data), data);
12533
+ const bytes = this.fetchRange === void 0 ? await this.openWhole(source) : await this.openRanged(source);
12534
+ const playback = new Mp4Playback(bytes.track, bytes.data);
11721
12535
  await playback.start();
11722
12536
  return playback;
11723
12537
  }
12538
+ async openWhole(source) {
12539
+ const data = await this.fetchBuffer(source);
12540
+ return {
12541
+ track: demuxMp4Video(data),
12542
+ data: bufferSource(data)
12543
+ };
12544
+ }
12545
+ /**
12546
+ * Finds the `moov`, reads it, and leaves the media where it is.
12547
+ *
12548
+ * The top-level structure of an MP4 is a flat list of boxes, each of
12549
+ * which states its own length, so the whole of it can be walked by
12550
+ * reading eight bytes at a time and jumping — which means finding
12551
+ * the `moov` costs a couple of small requests whether it is at the
12552
+ * front of the file (`-movflags +faststart`, and every file served
12553
+ * for streaming) or at the back (the default, and every file written
12554
+ * by a camera).
12555
+ *
12556
+ * The `moov` is then demuxed *on its own*, which works because the
12557
+ * offsets in a sample table are absolute positions in the file
12558
+ * rather than relative to anything: the tables are as correct read
12559
+ * out of a two hundred kilobyte buffer as out of a gigabyte one.
12560
+ */
12561
+ async openRanged(source) {
12562
+ const fetchRange = this.fetchRange;
12563
+ const first = await fetchRange(source, 0, HEADER_PROBE_BYTES);
12564
+ const total = first.total;
12565
+ if (!Number.isFinite(total) || total <= 0) throw new Error(`Fetching a range of '${source}' did not report how long the file is.`);
12566
+ const plain = (start, end) => fetchRange(source, start, end).then((response) => response.data);
12567
+ const bytes = rangeSource(source, {
12568
+ fetchRange: (_source, start, end) => plain(start, end),
12569
+ size: total,
12570
+ prefetched: {
12571
+ start: 0,
12572
+ data: first.data
12573
+ }
12574
+ });
12575
+ const moov = await findTopLevelBox(bytes, "moov", total);
12576
+ if (moov === null) throw new Error(`'${source}' has no moov box, so there is no sample table to read. A ranged read walks the top-level boxes; a file this cannot walk is not an MP4.`);
12577
+ const held = bytes.read(moov.start, moov.end - moov.start);
12578
+ return {
12579
+ track: demuxMp4Video(held === null ? await plain(moov.start, moov.end) : held.slice().buffer),
12580
+ data: bytes
12581
+ };
12582
+ }
11724
12583
  };
11725
12584
  /**
12585
+ * How much of the front of the file to ask for first.
12586
+ *
12587
+ * Exactly one block, and that is the whole point of the constant
12588
+ * rather than a number here: a probe that does not land on a block
12589
+ * boundary cannot be kept, so the front of the file would be fetched
12590
+ * once to find the header and again as the first block. It is also
12591
+ * enough to hold the `ftyp` and, in a faststart file, the whole `moov`
12592
+ * for a clip of ordinary length, so the common case is a single
12593
+ * request for the whole header.
12594
+ */
12595
+ const HEADER_PROBE_BYTES = DEFAULT_BLOCK_SIZE;
12596
+ /**
12597
+ * Walks the top-level boxes looking for one, reading only their
12598
+ * headers.
12599
+ *
12600
+ * A box states its own length, so the walk is a jump per box and the
12601
+ * bytes it touches are sixteen at a time. `size === 1` means the real
12602
+ * length is a 64-bit number after the type, and `size === 0` means the
12603
+ * box runs to the end of the file — both of which `mdat` is written
12604
+ * with often enough to matter.
12605
+ */
12606
+ async function findTopLevelBox(bytes, wanted, total) {
12607
+ let at = 0;
12608
+ while (at + 8 <= total) {
12609
+ await bytes.request(at, 16);
12610
+ const header = bytes.read(at, Math.min(16, total - at));
12611
+ if (header === null || header.length < 8) return null;
12612
+ const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
12613
+ let size = view.getUint32(0);
12614
+ let payload = at + 8;
12615
+ if (size === 1) {
12616
+ if (header.length < 16) return null;
12617
+ size = view.getUint32(8) * 4294967296 + view.getUint32(12);
12618
+ payload = at + 16;
12619
+ } else if (size === 0) size = total - at;
12620
+ if (size < 8 || at + size > total) return null;
12621
+ if (String.fromCharCode(header[4], header[5], header[6], header[7]) === wanted) return {
12622
+ start: payload - 8,
12623
+ end: at + size
12624
+ };
12625
+ at += size;
12626
+ }
12627
+ return null;
12628
+ }
12629
+ /**
11726
12630
  * One MP4, decoded and presented against the runtime's frame clock.
11727
12631
  *
11728
12632
  * The clock matters. Nothing here uses `setInterval` or the decoder's
@@ -11746,10 +12650,35 @@ var Mp4Playback = class {
11746
12650
  positionUs = 0;
11747
12651
  closed = false;
11748
12652
  errorListeners = /* @__PURE__ */ new Set();
12653
+ frameListeners = /* @__PURE__ */ new Set();
12654
+ /**
12655
+ * The sync samples, by presentation time, so a seek can find where
12656
+ * to start decoding with a binary search rather than a scan.
12657
+ *
12658
+ * Built once, because `samples` never changes and a scrub drags
12659
+ * across it many times a second. Sorted by `timestampUs` rather than
12660
+ * left in decode order: a keyframe carries a composition offset like
12661
+ * any other sample, so the order they are *shown* in is the order a
12662
+ * seek searches, and it is not quite the order they are decoded in.
12663
+ */
12664
+ syncPoints;
12665
+ /**
12666
+ * Where a seek is aiming, in microseconds, or null when playing
12667
+ * forwards.
12668
+ *
12669
+ * Frames that arrive before this are decoded only because the
12670
+ * pictures after them refer to them, and are closed on arrival
12671
+ * instead of being queued — see `receive`. Cleared as soon as a
12672
+ * frame at or past it has been decoded.
12673
+ */
12674
+ seekTargetUs = null;
12675
+ /** Whether a fetch for missing bytes is already out; see `await`. */
12676
+ pendingBytes = false;
11749
12677
  constructor(track, data) {
11750
12678
  this.track = track;
11751
12679
  this.data = data;
11752
- this.surface = new MutableVideoSurface(track.codedWidth, track.codedHeight);
12680
+ this.surface = new MutableVideoSurface(track.codedWidth, track.codedHeight, () => this.announcePicture());
12681
+ this.syncPoints = buildSyncPoints(track.samples);
11753
12682
  this.decoder = new VideoDecoder({
11754
12683
  output: (frame) => this.receive(frame),
11755
12684
  error: (error) => this.fail(error)
@@ -11770,6 +12699,9 @@ var Mp4Playback = class {
11770
12699
  get positionMs() {
11771
12700
  return this.positionUs / 1e3;
11772
12701
  }
12702
+ get seeking() {
12703
+ return this.seekTargetUs !== null;
12704
+ }
11773
12705
  async start() {
11774
12706
  const config = {
11775
12707
  codec: this.track.codec,
@@ -11795,7 +12727,7 @@ var Mp4Playback = class {
11795
12727
  if (this.closed) return false;
11796
12728
  const wanted = Math.max(0, positionMs * 1e3);
11797
12729
  if (wanted === this.positionUs) return false;
11798
- if (wanted + SEEK_BACK_TOLERANCE_US < this.positionUs) this.rewind();
12730
+ if (this.needsSeek(wanted)) this.seekTo(wanted);
11799
12731
  this.positionUs = wanted;
11800
12732
  const changed = this.presentDue();
11801
12733
  this.fill();
@@ -11805,6 +12737,10 @@ var Mp4Playback = class {
11805
12737
  this.errorListeners.add(listener);
11806
12738
  return () => this.errorListeners.delete(listener);
11807
12739
  }
12740
+ onFrame(listener) {
12741
+ this.frameListeners.add(listener);
12742
+ return () => this.frameListeners.delete(listener);
12743
+ }
11808
12744
  /** Frees the decoder and every frame still queued. */
11809
12745
  close() {
11810
12746
  if (this.closed) return;
@@ -11814,6 +12750,8 @@ var Mp4Playback = class {
11814
12750
  this.surface.clear();
11815
12751
  if (this.decoder.state !== "closed") this.decoder.close();
11816
12752
  this.errorListeners.clear();
12753
+ this.frameListeners.clear();
12754
+ this.data.close();
11817
12755
  }
11818
12756
  presentDue() {
11819
12757
  let shown = false;
@@ -11831,14 +12769,30 @@ var Mp4Playback = class {
11831
12769
  }
11832
12770
  return shown;
11833
12771
  }
11834
- /** Keeps the decoder a few frames ahead, and no further. */
12772
+ /**
12773
+ * Keeps the decoder a few frames ahead, and no further.
12774
+ *
12775
+ * **Stops at the first sample whose bytes are not here yet**, which
12776
+ * is the whole of what a ranged source costs this loop. `read`
12777
+ * answers synchronously or not at all — see `ByteSource` for why it
12778
+ * must — so a sample that is still on the wire ends the fill, a
12779
+ * request goes out for the region around it, and that request
12780
+ * calls `fill` again when it lands. In the ordinary case, a file
12781
+ * already in memory, `read` never answers null and none of this
12782
+ * runs.
12783
+ */
11835
12784
  fill() {
11836
12785
  if (this.closed || this.decoder.state !== "configured") return;
11837
12786
  while (this.nextSample < this.track.samples.length && this.queue.length + this.decoder.decodeQueueSize < FRAME_QUEUE_LIMIT) {
11838
12787
  const sample = this.track.samples[this.nextSample];
12788
+ const bytes = this.data.read(sample.offset, sample.size);
12789
+ if (bytes === null) {
12790
+ this.await(sample);
12791
+ return;
12792
+ }
11839
12793
  this.nextSample++;
11840
12794
  try {
11841
- this.decoder.decode(new EncodedVideoChunk(chunkOf(sample, this.data)));
12795
+ this.decoder.decode(new EncodedVideoChunk(chunkOf(sample, bytes)));
11842
12796
  } catch (error) {
11843
12797
  this.fail(error);
11844
12798
  return;
@@ -11846,21 +12800,77 @@ var Mp4Playback = class {
11846
12800
  }
11847
12801
  }
11848
12802
  /**
11849
- * Back to the start of the stream.
12803
+ * Asks for the bytes around a sample, and resumes when they arrive.
12804
+ *
12805
+ * The window is deliberately wider than the one sample: a request
12806
+ * per frame would be a request every sixteen milliseconds, and the
12807
+ * point of reading a file in pieces is to read it in *few* pieces.
12808
+ * So it asks for everything from this sample to the end of the
12809
+ * decoder's lookahead, which the block size below it will round up
12810
+ * to something worth a round trip anyway.
12811
+ */
12812
+ await(sample) {
12813
+ if (this.pendingBytes) return;
12814
+ const last = this.track.samples[Math.min(this.nextSample + FRAME_QUEUE_LIMIT, this.track.samples.length - 1)];
12815
+ const through = last === void 0 ? sample.offset + sample.size : last.offset + last.size;
12816
+ this.pendingBytes = true;
12817
+ this.data.request(sample.offset, Math.max(sample.size, through - sample.offset)).then(() => {
12818
+ this.pendingBytes = false;
12819
+ if (this.closed) return;
12820
+ this.fill();
12821
+ this.announceFrame();
12822
+ }).catch((error) => {
12823
+ this.pendingBytes = false;
12824
+ this.fail(error);
12825
+ });
12826
+ }
12827
+ /**
12828
+ * Whether reaching `wanted` means starting the decoder somewhere
12829
+ * else.
12830
+ *
12831
+ * Two cases, and they are not symmetric. **Backwards** past the
12832
+ * tolerance is always a seek: a decoder cannot run in reverse, so
12833
+ * every frame in flight belongs to where we no longer are. It is
12834
+ * also what a loop is, which is why looping costs a keyframe.
12835
+ *
12836
+ * **Forwards** is a seek only when the jump clears the samples
12837
+ * already submitted. During ordinary playback the decoder runs a few
12838
+ * frames ahead of the picture, so the keyframe covering the position
12839
+ * is always one it has long since passed — `sample <= nextSample`,
12840
+ * and nothing happens. A jump far enough ahead to land beyond the
12841
+ * decoder's own head is the only forward case worth a reset, and
12842
+ * without this test it would instead decode every frame in between
12843
+ * at playback speed, which is a scrub that crawls.
12844
+ */
12845
+ needsSeek(wanted) {
12846
+ if (wanted + SEEK_BACK_TOLERANCE_US < this.positionUs) return true;
12847
+ if (wanted <= this.positionUs) return false;
12848
+ return syncSampleFor(this.syncPoints, wanted) > this.nextSample;
12849
+ }
12850
+ /**
12851
+ * Starts the decoder again at the sync sample covering `wanted`.
11850
12852
  *
11851
12853
  * The decoder is reset and reconfigured rather than merely refilled,
11852
12854
  * because a decoder mid-GOP holds reference frames for where it was
11853
- * and the first sample of the file is a keyframe for where it is
11854
- * going. That costs one keyframe decode at the loop point — a
11855
- * measurable hitch on a long GOP, invisible on the two-second ones
11856
- * a looping clip is usually encoded with — and it is the honest
11857
- * price of looping a progressive file rather than a fragmented one.
11858
- */
11859
- rewind() {
12855
+ * and the sample it is about to be given is a keyframe for where it
12856
+ * is going.
12857
+ *
12858
+ * What lands is the keyframe *at or before* the wanted position,
12859
+ * never after: a seek that jumped forward to the next keyframe would
12860
+ * show a picture from later than the moment asked for, and a scrubber
12861
+ * that overshoots its own thumb is worse than one that is coarse.
12862
+ * The frames between the keyframe and the target are decoded — the
12863
+ * ones after it refer to them — and closed on arrival rather than
12864
+ * queued, which is what `seekTargetUs` is for. So the cost of a seek
12865
+ * is the GOP length, paid in decodes nobody sees, and the reason a
12866
+ * clip encoded with two-second keyframes scrubs well and one encoded
12867
+ * with ten-second keyframes does not.
12868
+ */
12869
+ seekTo(wanted) {
11860
12870
  for (const frame of this.queue) frame.close();
11861
12871
  this.queue.length = 0;
11862
- this.nextSample = 0;
11863
- this.positionUs = 0;
12872
+ this.nextSample = syncSampleFor(this.syncPoints, wanted);
12873
+ this.seekTargetUs = wanted > 0 ? wanted : null;
11864
12874
  if (this.config !== null && this.decoder.state !== "closed") {
11865
12875
  this.decoder.reset();
11866
12876
  this.decoder.configure(this.config);
@@ -11871,20 +12881,92 @@ var Mp4Playback = class {
11871
12881
  frame.close();
11872
12882
  return;
11873
12883
  }
12884
+ const target = this.seekTargetUs;
12885
+ if (target !== null) {
12886
+ if (frame.timestamp + this.track.frameDurationUs < target) {
12887
+ frame.close();
12888
+ this.fill();
12889
+ return;
12890
+ }
12891
+ this.enqueue(frame);
12892
+ if (!(frame.timestamp >= target || this.nextSample >= this.track.samples.length)) {
12893
+ this.fill();
12894
+ return;
12895
+ }
12896
+ this.seekTargetUs = null;
12897
+ this.announceFrame();
12898
+ return;
12899
+ }
12900
+ this.enqueue(frame);
12901
+ this.announceFrame();
12902
+ }
12903
+ /** Queued in presentation order; see `presentDue` for why that matters. */
12904
+ enqueue(frame) {
11874
12905
  this.queue.push(frame);
11875
12906
  this.queue.sort((a, b) => a.timestamp - b.timestamp);
11876
12907
  }
12908
+ /**
12909
+ * Presents whatever is due, and lets the surface say if that put a
12910
+ * picture up: see the constructor for why the two are not the same
12911
+ * moment.
12912
+ */
12913
+ announceFrame() {
12914
+ this.presentDue();
12915
+ }
12916
+ announcePicture() {
12917
+ for (const listener of this.frameListeners) listener();
12918
+ }
11877
12919
  fail(error) {
11878
12920
  for (const listener of this.errorListeners) listener(error);
11879
12921
  this.close();
11880
12922
  }
11881
12923
  };
11882
- function chunkOf(sample, data) {
12924
+ /**
12925
+ * The track's sync samples, in the order they are shown.
12926
+ *
12927
+ * `samples` is in decode order and stays that way — feeding a decoder
12928
+ * anything else is how you submit a frame before the one it refers to
12929
+ * — so the search a seek does needs its own view. Only the sync
12930
+ * samples are in it, which for a two-second GOP is one entry per two
12931
+ * seconds: a fifteen-minute film indexes in a few hundred numbers.
12932
+ */
12933
+ function buildSyncPoints(samples) {
12934
+ const points = [];
12935
+ for (const [sample, entry] of samples.entries()) if (entry.isKey) points.push({
12936
+ timestampUs: entry.timestampUs,
12937
+ sample
12938
+ });
12939
+ points.sort((a, b) => a.timestampUs - b.timestampUs);
12940
+ return points;
12941
+ }
12942
+ /**
12943
+ * Which sample to start decoding at to have a picture for `wanted`.
12944
+ *
12945
+ * The last sync sample at or before the position, by binary search.
12946
+ * Zero when the position is before the first one, or when the track
12947
+ * declared no sync samples at all — an all-intra track, where every
12948
+ * sample is one and `buildSyncPoints` returned all of them anyway.
12949
+ */
12950
+ function syncSampleFor(points, wantedUs) {
12951
+ let low = 0;
12952
+ let high = points.length - 1;
12953
+ let found = 0;
12954
+ while (low <= high) {
12955
+ const middle = low + high >> 1;
12956
+ const point = points[middle];
12957
+ if (point.timestampUs <= wantedUs) {
12958
+ found = point.sample;
12959
+ low = middle + 1;
12960
+ } else high = middle - 1;
12961
+ }
12962
+ return found;
12963
+ }
12964
+ function chunkOf(sample, bytes) {
11883
12965
  return {
11884
12966
  type: sample.isKey ? "key" : "delta",
11885
12967
  timestamp: sample.timestampUs,
11886
12968
  duration: sample.durationUs,
11887
- data: new Uint8Array(data, sample.offset, sample.size)
12969
+ data: bytes
11888
12970
  };
11889
12971
  }
11890
12972
  /**
@@ -11897,11 +12979,13 @@ function chunkOf(sample, data) {
11897
12979
  var MutableVideoSurface = class {
11898
12980
  width;
11899
12981
  height;
12982
+ onPicture;
11900
12983
  frame = null;
11901
12984
  version = 0;
11902
- constructor(width, height) {
12985
+ constructor(width, height, onPicture = () => {}) {
11903
12986
  this.width = width;
11904
12987
  this.height = height;
12988
+ this.onPicture = onPicture;
11905
12989
  }
11906
12990
  /**
11907
12991
  * Which conversion is the newest, so a slower one that finishes
@@ -11957,6 +13041,7 @@ var MutableVideoSurface = class {
11957
13041
  this.frame?.close?.();
11958
13042
  this.frame = frame;
11959
13043
  this.version++;
13044
+ this.onPicture();
11960
13045
  }
11961
13046
  clear() {
11962
13047
  this.pending++;
@@ -11966,6 +13051,154 @@ var MutableVideoSurface = class {
11966
13051
  }
11967
13052
  };
11968
13053
  //#endregion
13054
+ //#region src/media/WebVtt.ts
13055
+ const EMPTY_SETTINGS = Object.freeze({});
13056
+ /**
13057
+ * Reads a `.vtt` file.
13058
+ *
13059
+ * Returns an empty track rather than throwing for a file that is not
13060
+ * WebVTT at all, for the reason the docblock gives: a caption track is
13061
+ * an enhancement, and an application that has to guard every one of
13062
+ * these with a `try` will end up not showing captions at all.
13063
+ */
13064
+ function parseWebVtt(source) {
13065
+ const lines = source.replace(/^/, "").split(/\r\n|\r|\n/);
13066
+ const first = lines[0] ?? "";
13067
+ if (!first.startsWith("WEBVTT")) return {
13068
+ cues: [],
13069
+ header: "",
13070
+ skipped: 0
13071
+ };
13072
+ const header = first.slice(6).trim();
13073
+ const cues = [];
13074
+ let skipped = 0;
13075
+ let at = 1;
13076
+ while (at < lines.length) {
13077
+ while (at < lines.length && (lines[at] ?? "").trim() === "") at++;
13078
+ if (at >= lines.length) break;
13079
+ const blockStart = at;
13080
+ const block = [];
13081
+ while (at < lines.length && (lines[at] ?? "").trim() !== "") {
13082
+ block.push(lines[at]);
13083
+ at++;
13084
+ }
13085
+ const cue = readBlock(block);
13086
+ if (cue === null) {
13087
+ if (!isIgnorableBlock(block)) skipped++;
13088
+ } else cues.push(cue);
13089
+ if (at === blockStart) at++;
13090
+ }
13091
+ cues.sort((a, b) => a.start - b.start || a.end - b.end);
13092
+ return {
13093
+ cues,
13094
+ header,
13095
+ skipped
13096
+ };
13097
+ }
13098
+ function isIgnorableBlock(block) {
13099
+ const first = (block[0] ?? "").trim();
13100
+ return first === "NOTE" || first.startsWith("NOTE ") || first === "STYLE" || first === "REGION";
13101
+ }
13102
+ function readBlock(block) {
13103
+ if (block.length === 0 || isIgnorableBlock(block)) return null;
13104
+ const timingIndex = block.findIndex((line) => line.includes("-->"));
13105
+ if (timingIndex === -1 || timingIndex > 1) return null;
13106
+ const id = timingIndex === 1 ? block[0].trim() : void 0;
13107
+ const timing = block[timingIndex];
13108
+ const arrow = timing.indexOf("-->");
13109
+ const start = parseTimestamp(timing.slice(0, arrow));
13110
+ const rest = timing.slice(arrow + 3).trim();
13111
+ const space = rest.search(/\s/);
13112
+ const end = parseTimestamp(space === -1 ? rest : rest.slice(0, space));
13113
+ if (start === null || end === null || end < start) return null;
13114
+ const settings = space === -1 ? EMPTY_SETTINGS : parseSettings(rest.slice(space + 1));
13115
+ const payload = block.slice(timingIndex + 1);
13116
+ if (payload.length === 0) return null;
13117
+ return {
13118
+ id,
13119
+ start,
13120
+ end,
13121
+ text: stripMarkup(payload.join("\n")),
13122
+ settings
13123
+ };
13124
+ }
13125
+ /**
13126
+ * `hh:mm:ss.mmm` or `mm:ss.mmm`, into seconds.
13127
+ *
13128
+ * Null rather than NaN for anything else, so a caller cannot
13129
+ * accidentally propagate a number that compares false against
13130
+ * everything.
13131
+ */
13132
+ function parseTimestamp(text) {
13133
+ const match = /^\s*(?:(\d+):)?([0-5]\d):([0-5]\d)[.,](\d{1,3})\s*$/.exec(text);
13134
+ if (match === null) return null;
13135
+ const hours = match[1] === void 0 ? 0 : Number(match[1]);
13136
+ const minutes = Number(match[2]);
13137
+ const seconds = Number(match[3]);
13138
+ const fraction = Number(match[4].padEnd(3, "0")) / 1e3;
13139
+ return hours * 3600 + minutes * 60 + seconds + fraction;
13140
+ }
13141
+ function parseSettings(text) {
13142
+ const settings = {};
13143
+ for (const pair of text.split(/\s+/)) {
13144
+ const colon = pair.indexOf(":");
13145
+ if (colon <= 0) continue;
13146
+ settings[pair.slice(0, colon)] = pair.slice(colon + 1);
13147
+ }
13148
+ return Object.keys(settings).length === 0 ? EMPTY_SETTINGS : settings;
13149
+ }
13150
+ /**
13151
+ * Takes the tags out and leaves the words.
13152
+ *
13153
+ * Entities are decoded for the five WebVTT names only. There is no
13154
+ * general entity table here on purpose: the full set belongs to HTML,
13155
+ * a caption file that carries one is relying on a document context
13156
+ * this has none of, and the five below are the ones a caption actually
13157
+ * needs — a stray `<` in dialogue has to be escaped, and `&nbsp;` is
13158
+ * written by tools that think they are writing HTML.
13159
+ */
13160
+ function stripMarkup(text) {
13161
+ return text.replace(/<[^>]*>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&lrm;|&rlm;/g, "").replace(/&nbsp;/g, "\xA0").replace(/&amp;/g, "&").trim();
13162
+ }
13163
+ /**
13164
+ * The cue to show at a position, or null between cues.
13165
+ *
13166
+ * A linear scan from a hint rather than a binary search, and that is
13167
+ * the right shape here rather than a concession: this is called once
13168
+ * per frame with a position that has almost always moved by one frame,
13169
+ * so the answer is nearly always the cue it was last time or the one
13170
+ * after it. `from` is that hint — pass back the index this returned —
13171
+ * and the scan is then O(1) for playback and O(n) only for a scrub,
13172
+ * where a caption is not what is costing anything.
13173
+ *
13174
+ * **Overlapping cues.** WebVTT permits them and a transcript of two
13175
+ * people talking over each other has them. This answers the one that
13176
+ * *starts* latest among those covering the position, which is what a
13177
+ * single-line caption should show: the newest thing said.
13178
+ */
13179
+ function cueAt(cues, seconds, from = 0) {
13180
+ if (cues.length === 0) return {
13181
+ cue: null,
13182
+ index: 0
13183
+ };
13184
+ let index = Math.min(Math.max(0, from), cues.length - 1);
13185
+ while (index > 0 && cues[index].start > seconds) index--;
13186
+ let best = null;
13187
+ let bestIndex = index;
13188
+ for (let n = index; n < cues.length; n++) {
13189
+ const cue = cues[n];
13190
+ if (cue.start > seconds) break;
13191
+ if (cue.end > seconds) {
13192
+ best = cue;
13193
+ bestIndex = n;
13194
+ }
13195
+ }
13196
+ return {
13197
+ cue: best,
13198
+ index: best === null ? index : bestIndex
13199
+ };
13200
+ }
13201
+ //#endregion
11969
13202
  //#region src/scheduler/PerformanceMarks.ts
11970
13203
  /**
11971
13204
  * Gesso's work, in the browser's own profiler
@@ -12393,20 +13626,38 @@ var UiTimerFrameClock = class {
12393
13626
  /**
12394
13627
  * A clock whose frames arrive from somewhere else.
12395
13628
  *
12396
- * The one thing a worker cannot do for itself. `requestAnimationFrame`
12397
- * is tied to the compositor and exists only on the main thread, so a
12398
- * render worker's frames were paced by `UiTimerFrameClock` at a fixed
12399
- * 16ms — roughly sixty a second on any display, aligned to none of
12400
- * them. On a 165Hz monitor that is not merely slower than it could be:
12401
- * an unaligned timer lands two frames inside one refresh, or none, so
12402
- * the pacing is uneven as well as capped.
12403
- *
12404
- * This clock does no timing at all. It reports when it wants frames
12405
- * and delivers whatever ticks it is handed, which lets the shell —
12406
- * the one thread with a `requestAnimationFrame` — supply the display's
12407
- * own cadence. That is the narrow exception to keeping the shell
12408
- * uninvolved: not work moved back onto the main thread for
12409
- * convenience, but the single fact a worker has no way to observe.
13629
+ * A render worker's frames were once paced by `UiTimerFrameClock` at a
13630
+ * fixed 16ms — roughly sixty a second on any display, aligned to none
13631
+ * of them. On a 165Hz monitor that is not merely slower than it could
13632
+ * be: an unaligned timer lands two frames inside one refresh, or none,
13633
+ * so the pacing is uneven as well as capped.
13634
+ *
13635
+ * **A dedicated worker can ask the compositor itself**, and this clock
13636
+ * does so when it can. `DedicatedWorkerGlobalScope.requestAnimationFrame`
13637
+ * has been in Chrome since 69, Firefox since 99 and Safari since 16.4,
13638
+ * which makes it baseline as of March 2023; this file used to say it
13639
+ * "exists only on the main thread", which was true when it was written
13640
+ * and is not now. Measured in the render worker: a median interval of
13641
+ * 16.7ms with a spread of 0.2, which is vsync and not a timer. Its
13642
+ * timestamps are on this thread's own `performance.now()` timeline, so
13643
+ * nothing has to be translated.
13644
+ *
13645
+ * That path costs the shell nothing at all: no loop, and no message
13646
+ * per refresh. `onActive` is never called, so a host that was
13647
+ * forwarding ticks simply stops being asked to.
13648
+ *
13649
+ * **The hosted path remains for where it is not available**, which is
13650
+ * an older browser and, on Chromium, a *nested* worker. Then this
13651
+ * clock does no timing of its own: it reports when it wants frames and
13652
+ * delivers whatever ticks it is handed, which lets the shell supply
13653
+ * the display's cadence on its behalf. It is the same contract either
13654
+ * way, which is why the choice can be made at construction and
13655
+ * forgotten.
13656
+ *
13657
+ * Both paths stop for a hidden tab, and that is not a coincidence: a
13658
+ * worker's animation frames are serviced by its **owner window's**
13659
+ * rendering, and a window that is not rendering services none. So the
13660
+ * battery properties the runtime relies on hold whichever path runs.
12410
13661
  *
12411
13662
  * **Ticks are free-running while frames are wanted, not requested one
12412
13663
  * at a time.** A tick per request would cost a round trip inside every
@@ -12417,6 +13668,18 @@ var UiTimerFrameClock = class {
12417
13668
  * one comparison and makes the timing robust to whichever side is
12418
13669
  * late.
12419
13670
  */
13671
+ /**
13672
+ * This thread's `requestAnimationFrame`, if it has a real one.
13673
+ *
13674
+ * Checked for rather than assumed, because the answer differs by
13675
+ * thread and by browser: a window always has one, a dedicated worker
13676
+ * has had one for years, and a *nested* worker on Chromium has not.
13677
+ * The feature test is the whole guard; there is no version to compare.
13678
+ */
13679
+ function localAnimationFrame() {
13680
+ const host = globalThis;
13681
+ return typeof host.requestAnimationFrame === "function" ? host.requestAnimationFrame.bind(host) : null;
13682
+ }
12420
13683
  var UiHostFrameClock = class {
12421
13684
  onFrame;
12422
13685
  onActive;
@@ -12455,6 +13718,9 @@ var UiHostFrameClock = class {
12455
13718
  stallMs;
12456
13719
  minStallMs;
12457
13720
  now;
13721
+ /** This thread's own `requestAnimationFrame`, or null where there is none. */
13722
+ local;
13723
+ localHandle = null;
12458
13724
  constructor(onFrame, onActive, options = {}) {
12459
13725
  this.onFrame = onFrame;
12460
13726
  this.onActive = onActive;
@@ -12462,9 +13728,21 @@ var UiHostFrameClock = class {
12462
13728
  this.stallMs = options.stallMs ?? 100;
12463
13729
  this.minStallMs = options.minStallMs ?? 12;
12464
13730
  this.now = options.now ?? (() => performance.now());
13731
+ this.local = options.requestAnimationFrame === void 0 ? localAnimationFrame() : options.requestAnimationFrame;
13732
+ }
13733
+ /** Whether this clock is driving itself from the compositor. */
13734
+ get isLocal() {
13735
+ return this.local !== null;
12465
13736
  }
12466
13737
  requestFrame() {
12467
13738
  this.pending = true;
13739
+ if (this.local !== null) {
13740
+ if (this.localHandle === null) this.localHandle = this.local((time) => {
13741
+ this.localHandle = null;
13742
+ if (this.pending) this.deliver(time);
13743
+ });
13744
+ return;
13745
+ }
12468
13746
  this.setActive(true);
12469
13747
  this.armTimer();
12470
13748
  }
@@ -12573,7 +13851,7 @@ var UiHostFrameClock = class {
12573
13851
  cancelFrame() {
12574
13852
  this.pending = false;
12575
13853
  this.clearFallback();
12576
- this.setActive(false);
13854
+ if (this.local === null) this.setActive(false);
12577
13855
  }
12578
13856
  clearFallback() {
12579
13857
  if (this.handle !== null) {
@@ -13108,6 +14386,39 @@ const PRESENTATIONAL_CHILDREN = /* @__PURE__ */ new Set([
13108
14386
  * hidden is not.
13109
14387
  */
13110
14388
  function buildSemanticsTree(root) {
14389
+ return walk(root, null, false);
14390
+ }
14391
+ /**
14392
+ * The records one subtree contributes, as if walked in place.
14393
+ *
14394
+ * For re-describing what changed without rebuilding the whole tree.
14395
+ * `node` keeps the `parent` and `index` its existing record has, and
14396
+ * everything below it is walked exactly as `buildSemanticsTree` would:
14397
+ * a descendant's index is counted among the semantic children of
14398
+ * whatever inside this subtree claims it, so the numbering is the same
14399
+ * one a full walk produces.
14400
+ *
14401
+ * **That only holds when `node` itself carries a record.** A
14402
+ * transparent node's children attach to its nearest semantic ancestor
14403
+ * and are numbered among that ancestor's *other* children too — nodes
14404
+ * outside this subtree — so a walk that started here would number them
14405
+ * from zero and disagree with the tree. Null says so, and the caller
14406
+ * has to rebuild the whole tree instead.
14407
+ *
14408
+ * `inert` is whether an ancestor above this subtree is disabled, which
14409
+ * only the caller can know.
14410
+ */
14411
+ function buildSemanticsSubtree(node, parent, index, inert) {
14412
+ const records = walk(node, parent, inert);
14413
+ const own = records.get(node.id);
14414
+ if (own === void 0) return null;
14415
+ records.set(node.id, {
14416
+ ...own,
14417
+ index
14418
+ });
14419
+ return records;
14420
+ }
14421
+ function walk(root, rootParent, rootInert) {
13111
14422
  const records = /* @__PURE__ */ new Map();
13112
14423
  const childCounts = /* @__PURE__ */ new Map();
13113
14424
  const nextIndex = (parent) => {
@@ -13133,18 +14444,29 @@ function buildSemanticsTree(root) {
13133
14444
  visit(child, record.id, disabled);
13134
14445
  }
13135
14446
  };
13136
- visit(root, null, false);
14447
+ visit(root, rootParent, rootInert);
13137
14448
  return records;
13138
14449
  }
13139
14450
  /**
14451
+ * Whether an ancestor of `node` is disabled, which every record in its
14452
+ * subtree inherits.
14453
+ *
14454
+ * A full walk carries this down; a subtree walk has to look it up.
14455
+ */
14456
+ function semanticsInertAbove(node) {
14457
+ for (let current = node.parent; current !== null; current = current.parent) if (current.properties.get("disabled") === true) return true;
14458
+ return false;
14459
+ }
14460
+ /**
13140
14461
  * The record for a node, or null when the node is transparent.
13141
14462
  */
13142
14463
  function describe(node, parent, disabled, nextIndex) {
13143
14464
  const role = node.properties.get("role") ?? IMPLICIT_ROLES[node.type];
13144
14465
  const declaredLabel = node.properties.get("label");
13145
- const name = declaredLabel ?? accessibleText(node);
13146
14466
  const isText = node.type === "text";
13147
- if (role === void 0 && declaredLabel === void 0 && !(isText && name !== void 0)) return null;
14467
+ if (role === void 0 && declaredLabel === void 0 && !isText) return null;
14468
+ const name = declaredLabel ?? accessibleText(node);
14469
+ if (role === void 0 && declaredLabel === void 0 && name === void 0) return null;
13148
14470
  const states = node.properties.get("states");
13149
14471
  return prune({
13150
14472
  id: node.id,
@@ -13345,6 +14667,6 @@ function recordsEqual(a, b) {
13345
14667
  return true;
13346
14668
  }
13347
14669
  //#endregion
13348
- export { AlignContent, AnimationDriver, BUTTON_INTERACTION, Box, Button, CARET_BLINK_MS, CARET_WIDTH, CLIP_STRIDE_BYTES, CLIP_STRIDE_FLOATS, Canvas2DRenderer, CanvasPlatformSurface, CanvasSurface, CanvasTextMeasurer, CharacterCountTextMeasurer, Column, CommandKind, Constraints, CrossAxisAlignment, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DEFAULT_FONT_WEIGHT, DEFAULT_LINE_HEIGHT_FACTOR, DEFAULT_TEXT_COLOR, DefaultImageResolver, DefaultVideoResolver, DirtyFlags, DirtyNodeSet, EDITOR_PROP, ELLIPSIS, EMPTY_RECORDING, EXTERNAL_FILES, EditableLayout, EditableText, EditableTextModel, EnvironmentNotifier, FlexDirection, FocusNotifier, GLYPH_SUBPIXEL_PHASES, GlyphShaper, Grid, INSPECTOR_HEAT_MS, INSTANCE_STRIDE_BYTES, INSTANCE_STRIDE_FLOATS, IconRasterizer, LABEL_PADDING_X, LayoutEngine, LayoutInspector, LayoutNotifier, LayoutRecord, LazyColumn, LazyGrid, LazyRow, MARK_PREFIX, MAX_GRADIENT_STOPS, MAX_MEASURES_PER_CHILD, MOTION_CHANNELS, MOTION_REST, MOUSE_POINTER, MainAxisAlignment, Mp3Frames, NO_CLIP_INDEX, NodeTransitions, Paint, PaintPictureCache, PaintRecorder, PaintTarget, ParagraphTextMeasurer, PrimitiveKind, Responsive, Row, SCROLLBAR_FADE_MS, SCROLLBAR_HOVER_ZONE, SCROLLBAR_INSET, SCROLLBAR_LINGER_MS, SCROLLBAR_MIN_THUMB, SCROLLBAR_THICKNESS, ScrollView, Stack, TEXTURED_STRIDE_BYTES, TEXTURED_STRIDE_FLOATS, TEXT_MATCHES_PROP, TEXT_RUN_ID_SEPARATOR, TEXT_SELECTION_PROP, Text, UI_ROLES, UI_SEMANTIC_STATES, UI_SUBGRID_AXES, UiAnimation, UiAnimationFrameClock, UiBasicColors, UiBeforeInputEvent, UiBinding, UiBorderRadiuses, UiChildrenBinding, UiContainerSizeSource, UiDragSession, UiEditingController, UiEnvironment, UiEnvironmentKeys, UiEventBinding, UiEventType, UiFindController, UiFocusEvent, UiFocusManager, UiFrame, UiGestureEvent, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiHostFrameClock, UiInputDispatcher, UiInputEvent, UiInsetRegistry, UiKeyboardController, UiKeyboardEvent, UiManualFrameClock, UiModifierSet, UiNode, UiNodeType, UiPinchEvent, UiPinchRecognizer, UiPlatformAdapter, UiPointerController, UiPointerEvent, UiProperties, UiScheduler, UiSelectionController, UiSharedElements, UiShortcutRegistry, UiSpring, UiTextChangeEvent, UiTimerFrameClock, UiTouchScroller, UiTransforms, UiTween, UiUnknownModifierPropertyError, UiVirtualWindow, UiVisualState, UiWheelController, UiWheelDeltaMode, UiWheelEvent, VIRTUAL_INDEX_PROP, VIRTUAL_LEAD_PROP, VIRTUAL_WINDOW_PROP, WebGPUError, WebGPUGlyphAtlas, WebGPUGlyphPages, WebGPURenderer, WebGPUSurface, WebGPUTextureCache, accumulatedOffsetTo, animateLayout, applyCanvasTextStyle, assertModifierList, assertTransitionMap, auto, autoFocus, bandOf, borderRadius, borderRadiusCorners, borderRadiusEqual, borderRadiusIsZero, borderRadiusValuesEqual, boxShadow, boxShadowArraysEqual, boxShadowsEqual, breakpoint, buildFontString, buildRenderList, buildSemanticsTree, bumpFontStack, bundle, canDecodeVideo, capturePointer, caretRectFor, caretVisibleAt, clampSize, clearFontStacks, clearLinkHover, clearMatchRanges, clearOverrideProperty, clearSelectionRange, clickOutside, closestPropertyName, colorToCss, colorToHex, colorToRgba, colorValuesEqual, colorsEqual, colorsEqualPalette, commandForKey, computeObjectFitRect, constraintsEqual, containerBands, contentOffset, contextMenu, contrastRatio, createCanvasSurface, createCanvasTextStyle, createElement, createEnvironmentKey, createPaintState, createTextCache, createTween, createWebGPUSurface, cubicBezier, darkColors, darkTheme, decorated, decorationColor, decorationRect, defaultMotion, defaultShadows, defaultShapes, defaultSpacing, defaultTextStyle, defaultTypography, defaultVisualState, defineModifier, defineProperty, defineThemeExtension, demuxMp4Video, densityFactors, describeLength, describeOverrides, detectEditingPlatform, diffSemantics, dragSessionFor, dragSource, draggable, drawOverlayShapes, drawText, dropTarget, easings, editorFor, editorOf, fade, fillTextRects, findEnvironmentKey, findMatchesIn, findPropertyDefinition, flattenTextSpans, focusRing, fontStackFor, formatConstraints, formatExplanation, formatShortcut, fr, getPropertyNames, glyphCount, gradientPaint, gradientsEqual, graphemeBoundaries, hasDecorationPhase, hasDrawnText, hasTextLinks, hasThemeExtension, hasVisualState, highContrastColors, hoverable, iconKey, iconSource, imageSource, inheritedPropertyFlags, inheritedPropertyNames, initializeWebGPU, inputsEqual, insetPadding, insetsEqual, interactive, interpolateColor, interpolateNumber, interpolateTransform, interpolatorFor, isAutoLength, isComponentLikeElement, isEditableNode, isFrLength, isLayoutProtocol, isMinMaxTrack, isMotionRest, isMultiline, isNodeFocusable, isNodeHitTestable, isNodeInert, isNodeSelectable, isObservable, isPaintVisible, isPercentLength, isPrintable, isReadOnly, isScrollContainer, isShapeName, isTypographyRole, isUiChild, isUiElement, isUiModifier, isUiRole, isUiSemanticState, isVideoSurface, isWebGPUAvailable, labelNode, labelOrigin, layoutParagraph, layoutTextLines, lazySource, lightColors, lightTheme, lineEndAt, lineIndexAtY, lineIndexForOffset, lineLimit, lineStartAt, linear, linearGradient, linkHoverOf, linkOf, linkRunAtPointIn, markInstant, markNow, matchRangesOf, measure, measureSpan, minmax, motion, nextCaretToggle, nextGraphemeEnd, nextWordEnd, noInsets, noKeyModifiers, noModifiers, noThemeExtensions, normalizeBorderRadius, normalizeColor, normalizeStates, normalizeTextAlign, normalizeTransition, normalizeVerticalAlign, observeViewportInsets, offsetAtPoint, offsetAtPointIn, offsetAtX, offsetForVerticalMove, overrideSources, paintPictures, paintValuesEqual, paintsAfterChildren, paragraphGeometry, paragraphGeometryFrom, parseAlignContent, parseColor, parseCrossAxisAlignment, parseFlexDirection, parseMainAxisAlignment, parseShortcut, parseTransform, pathValuesEqual, percent, performanceMarksEnabled, pinchable, placeGridItems, pointerDeviceOf, pop, prepareInputSurface, pressable, previousGraphemeStart, previousWordStart, propertyEffects, propertyIsInherited, propertyValuesEqual, propertyValuesEqualByName, proportionalFontMetrics, provideEnvironment, publishInset, radialGradient, raiseContrast, readMp3Header, recordsEqual, registerFontStack, relativeLuminance, reorderable, repeat, replayPaint, resetOverrideWarnings, resetTransitionWarnings, resolveBoolean, resolveCursor, resolveLength, resolveMotionState, resolveNumber, resolvePaintState, resolveProperty, resolvePropertyByName, resolveString, resolvedSpansOf, rgb8, rgba, rotateFrom, runCanvasStyleInto, runFontString, sameArgs, scaleFrom, scaleSpacing, scrollPosition, scrollbarThumb, scrollbarThumbs, scrollbarZoneAt, selectableTextNodes, selectableTextOf, selectionRangeOf, selectionRects, selectionRectsIn, setLinkHover, setMatchRanges, setPerformanceMarks, setSelectionRange, shadowsEqual, shapesEqual, sharedElement, shortcut, shortcuts, sizeContainer, sizeGridTracks, slideDown, slideFrom, slideUp, spacingEqual, spacingSteps, spanAtOffset, spannedRunsFor, spannedTextOf, splitMp3Frames, spring, statesEqual, steps, syncEditorValue, textContentOf, textRunBackgrounds, textRunDecorations, textRunOfRecordId, textRuns, textSpansEqual, textStylesEqual, themeExtension, themeExtensionsEqual, themeTokens, themesEqual, tightenConstraints, touchActionFor, tracePathData, traceRoundedRect, transform, transformIsIdentity, transformsEqual, tween, typographyEqual, uniformBorderRadius, unknownContainerSize, validateGradient, validateRole, validateStates, validateSubgrid, videoFrameSize, videoSource, visualState, visualStatesEqual, wheelDeltaYOf, withContrast, withDensity, withOverrideSource, withThemeExtension, wordRangeAt, wordRangeIn, writeDeclaredProperty, writeOverrideProperty };
14670
+ export { AlignContent, AnimationDriver, BUTTON_INTERACTION, Box, Button, CARET_BLINK_MS, CARET_WIDTH, CLIP_STRIDE_BYTES, CLIP_STRIDE_FLOATS, Canvas2DRenderer, CanvasPlatformSurface, CanvasSurface, CanvasTextMeasurer, CharacterCountTextMeasurer, Column, CommandKind, Constraints, CrossAxisAlignment, DEFAULT_BLOCK_SIZE, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DEFAULT_FONT_WEIGHT, DEFAULT_LINE_HEIGHT_FACTOR, DEFAULT_TEXT_COLOR, DefaultImageResolver, DefaultVideoResolver, DirtyFlags, DirtyNodeSet, EDITOR_PROP, ELLIPSIS, EMPTY_RECORDING, EXTERNAL_FILES, EditableLayout, EditableText, EditableTextModel, EnvironmentNotifier, FlexDirection, FocusNotifier, GLYPH_SUBPIXEL_PHASES, GlyphShaper, Grid, INSPECTOR_HEAT_MS, INSTANCE_STRIDE_BYTES, INSTANCE_STRIDE_FLOATS, IconRasterizer, LABEL_PADDING_X, LayoutEngine, LayoutInspector, LayoutNotifier, LayoutRecord, LazyColumn, LazyGrid, LazyRow, MARK_PREFIX, MAX_GRADIENT_STOPS, MAX_MEASURES_PER_CHILD, MOTION_CHANNELS, MOTION_REST, MOUSE_POINTER, MainAxisAlignment, Mp3Frames, NO_CLIP_INDEX, NodeTransitions, Paint, PaintPictureCache, PaintRecorder, PaintTarget, ParagraphTextMeasurer, PrimitiveKind, Responsive, Row, SCROLLBAR_FADE_MS, SCROLLBAR_HOVER_ZONE, SCROLLBAR_INSET, SCROLLBAR_LINGER_MS, SCROLLBAR_MIN_THUMB, SCROLLBAR_THICKNESS, ScrollView, Stack, TEXTURED_STRIDE_BYTES, TEXTURED_STRIDE_FLOATS, TEXT_MATCHES_PROP, TEXT_RUN_ID_SEPARATOR, TEXT_SELECTION_PROP, Text, UI_ROLES, UI_SEMANTIC_STATES, UI_SUBGRID_AXES, UiAnimation, UiAnimationFrameClock, UiBasicColors, UiBeforeInputEvent, UiBinding, UiBorderRadiuses, UiChildrenBinding, UiContainerSizeSource, UiDragSession, UiEditingController, UiEnvironment, UiEnvironmentKeys, UiEventBinding, UiEventType, UiFindController, UiFocusEvent, UiFocusManager, UiFrame, UiGestureEvent, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiHostFrameClock, UiInputDispatcher, UiInputEvent, UiInsetRegistry, UiKeyboardController, UiKeyboardEvent, UiManualFrameClock, UiModifierSet, UiNode, UiNodeType, UiPinchEvent, UiPinchRecognizer, UiPlatformAdapter, UiPointerController, UiPointerEvent, UiProperties, UiScheduler, UiSelectionController, UiSharedElements, UiShortcutRegistry, UiSpring, UiTextChangeEvent, UiTimerFrameClock, UiTouchScroller, UiTransforms, UiTween, UiUnknownModifierPropertyError, UiVirtualWindow, UiVisualState, UiWheelController, UiWheelDeltaMode, UiWheelEvent, VIRTUAL_INDEX_PROP, VIRTUAL_LEAD_PROP, VIRTUAL_WINDOW_PROP, WebGPUError, WebGPUGlyphAtlas, WebGPUGlyphPages, WebGPURenderer, WebGPUSurface, WebGPUTextureCache, accumulatedOffsetTo, animateLayout, applyCanvasTextStyle, assertModifierList, assertTransitionMap, auto, autoFocus, bandOf, borderRadius, borderRadiusCorners, borderRadiusEqual, borderRadiusIsZero, borderRadiusValuesEqual, boxShadow, boxShadowArraysEqual, boxShadowsEqual, breakpoint, bufferSource, buildFontString, buildRenderList, buildSemanticsSubtree, buildSemanticsTree, bumpFontStack, bundle, canDecodeVideo, capturePointer, caretRectFor, caretVisibleAt, clampSize, clearFontStacks, clearLinkHover, clearMatchRanges, clearOverrideProperty, clearSelectionRange, clickOutside, closestPropertyName, colorToCss, colorToHex, colorToRgba, colorValuesEqual, colorsEqual, colorsEqualPalette, commandForKey, computeObjectFitRect, constraintsEqual, containerBands, contentOffset, contextMenu, contrastRatio, createCanvasSurface, createCanvasTextStyle, createElement, createEnvironmentKey, createPaintState, createTextCache, createTween, createWebGPUSurface, cubicBezier, cueAt, darkColors, darkTheme, decorated, decorationColor, decorationRect, defaultMotion, defaultShadows, defaultShapes, defaultSpacing, defaultTextStyle, defaultTypography, defaultVisualState, defineModifier, defineProperty, defineThemeExtension, demuxMp4Audio, demuxMp4Video, densityFactors, describeLength, describeOverrides, detectEditingPlatform, diffSemantics, dragSessionFor, dragSource, draggable, drawOverlayShapes, drawText, dropTarget, easings, editorFor, editorOf, fade, fillTextRects, findEnvironmentKey, findMatchesIn, findPropertyDefinition, flattenTextSpans, focusRing, fontStackFor, formatConstraints, formatExplanation, formatShortcut, fr, getPropertyNames, glyphCount, gradientPaint, gradientsEqual, graphemeBoundaries, hasDecorationPhase, hasDrawnText, hasTextLinks, hasThemeExtension, hasVisualState, highContrastColors, hoverable, iconKey, iconSource, imageSource, inheritedPropertyFlags, inheritedPropertyNames, initializeWebGPU, inputsEqual, insetPadding, insetsEqual, interactive, interpolateColor, interpolateNumber, interpolateTransform, interpolatorFor, isAutoLength, isComponentLikeElement, isEditableNode, isFrLength, isLayoutProtocol, isMinMaxTrack, isMotionRest, isMultiline, isNodeFocusable, isNodeHitTestable, isNodeInert, isNodeSelectable, isObservable, isPaintVisible, isPercentLength, isPrintable, isReadOnly, isScrollContainer, isShapeName, isTypographyRole, isUiChild, isUiElement, isUiModifier, isUiRole, isUiSemanticState, isVideoSurface, isWebGPUAvailable, labelNode, labelOrigin, layoutParagraph, layoutTextLines, lazySource, lightColors, lightTheme, lineEndAt, lineIndexAtY, lineIndexForOffset, lineLimit, lineStartAt, linear, linearGradient, linkHoverOf, linkOf, linkRunAtPointIn, markInstant, markNow, matchRangesOf, measure, measureSpan, minmax, motion, nextCaretToggle, nextGraphemeEnd, nextWordEnd, noInsets, noKeyModifiers, noModifiers, noThemeExtensions, normalizeBorderRadius, normalizeColor, normalizeStates, normalizeTextAlign, normalizeTransition, normalizeVerticalAlign, observeViewportInsets, offsetAtPoint, offsetAtPointIn, offsetAtX, offsetForVerticalMove, overrideSources, paintPictures, paintValuesEqual, paintsAfterChildren, paragraphGeometry, paragraphGeometryFrom, parseAlignContent, parseColor, parseCrossAxisAlignment, parseFlexDirection, parseMainAxisAlignment, parseShortcut, parseTransform, parseWebVtt, pathValuesEqual, percent, performanceMarksEnabled, pinchable, placeGridItems, pointerDeviceOf, pop, prepareInputSurface, pressable, previousGraphemeStart, previousWordStart, propertyEffects, propertyIsInherited, propertyValuesEqual, propertyValuesEqualByName, proportionalFontMetrics, provideEnvironment, publishInset, radialGradient, raiseContrast, rangeSource, readMp3Header, recordsEqual, registerFontStack, relativeLuminance, reorderable, repeat, replayPaint, resetOverrideWarnings, resetTransitionWarnings, resolveBoolean, resolveCursor, resolveLength, resolveMotionState, resolveNumber, resolvePaintState, resolveProperty, resolvePropertyByName, resolveString, resolvedSpansOf, rgb8, rgba, rotateFrom, runCanvasStyleInto, runFontString, sameArgs, scaleFrom, scaleSpacing, scrollPosition, scrollbarThumb, scrollbarThumbs, scrollbarZoneAt, selectableTextNodes, selectableTextOf, selectionRangeOf, selectionRects, selectionRectsIn, semanticsInertAbove, setLinkHover, setMatchRanges, setPerformanceMarks, setSelectionRange, shadowsEqual, shapesEqual, sharedElement, shortcut, shortcuts, sizeContainer, sizeGridTracks, slideDown, slideFrom, slideUp, spacingEqual, spacingSteps, spanAtOffset, spannedRunsFor, spannedTextOf, splitMp3Frames, spring, statesEqual, steps, syncEditorValue, textContentOf, textRunBackgrounds, textRunDecorations, textRunOfRecordId, textRuns, textSpansEqual, textStylesEqual, themeExtension, themeExtensionsEqual, themeTokens, themesEqual, tightenConstraints, touchActionFor, tracePathData, traceRoundedRect, transform, transformIsIdentity, transformsEqual, tween, typographyEqual, uniformBorderRadius, unknownContainerSize, validateGradient, validateRole, validateStates, validateSubgrid, videoFrameSize, videoSource, visualState, visualStatesEqual, wheelDeltaYOf, withContrast, withDensity, withOverrideSource, withThemeExtension, wordRangeAt, wordRangeIn, writeDeclaredProperty, writeOverrideProperty };
13349
14671
 
13350
14672
  //# sourceMappingURL=index.js.map