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/CHANGELOG.md +147 -0
- package/README.md +1 -1
- package/dist/{CanvasSurface-CrG-uJ5o.js → CanvasSurface-5dU-U3qQ.js} +34 -5
- package/dist/{CanvasSurface-CrG-uJ5o.js.map → CanvasSurface-5dU-U3qQ.js.map} +1 -1
- package/dist/{index-DuZEAhyZ.d.ts → index-DAdauEn9.d.ts} +634 -53
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1439 -117
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +163 -2
- package/dist/testing.js +325 -2
- package/dist/testing.js.map +1 -1
- package/package.json +2 -2
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-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
6907
|
-
*
|
|
6908
|
-
*
|
|
6909
|
-
*
|
|
6910
|
-
*
|
|
6911
|
-
*
|
|
6912
|
-
*
|
|
6913
|
-
* **
|
|
6914
|
-
*
|
|
6915
|
-
*
|
|
6916
|
-
*
|
|
6917
|
-
*
|
|
6918
|
-
*
|
|
6919
|
-
*
|
|
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
|
|
6942
|
-
* the
|
|
6943
|
-
*
|
|
6944
|
-
*
|
|
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
|
-
|
|
6979
|
-
if (playback.present(at)) host.requestFrame();
|
|
7070
|
+
if (playback.present(next)) host.requestFrame();
|
|
6980
7071
|
}
|
|
6981
7072
|
};
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
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:
|
|
7093
|
+
duration: remaining / rate,
|
|
6997
7094
|
easing: linear,
|
|
6998
|
-
repeat: args.loop !== false,
|
|
6999
7095
|
reducedMotion: "keep",
|
|
7000
|
-
stepMs:
|
|
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)
|
|
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
|
|
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 =
|
|
11349
|
-
if (table === null)
|
|
11350
|
-
|
|
11351
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
11762
|
+
matches = reader.ascii(4) === kind;
|
|
11405
11763
|
return;
|
|
11406
11764
|
}
|
|
11407
11765
|
if (box.type === "minf") minf = box;
|
|
11408
11766
|
});
|
|
11409
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
11552
|
-
|
|
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
|
|
11720
|
-
const playback = new Mp4Playback(
|
|
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 (
|
|
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
|
-
/**
|
|
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,
|
|
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
|
-
*
|
|
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
|
|
11854
|
-
* going.
|
|
11855
|
-
*
|
|
11856
|
-
*
|
|
11857
|
-
*
|
|
11858
|
-
|
|
11859
|
-
|
|
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 =
|
|
11863
|
-
this.
|
|
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
|
-
|
|
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:
|
|
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 ` ` is
|
|
13158
|
+
* written by tools that think they are writing HTML.
|
|
13159
|
+
*/
|
|
13160
|
+
function stripMarkup(text) {
|
|
13161
|
+
return text.replace(/<[^>]*>/g, "").replace(/</g, "<").replace(/>/g, ">").replace(/‎|‏/g, "").replace(/ /g, "\xA0").replace(/&/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
|
-
*
|
|
12397
|
-
*
|
|
12398
|
-
*
|
|
12399
|
-
*
|
|
12400
|
-
*
|
|
12401
|
-
*
|
|
12402
|
-
*
|
|
12403
|
-
*
|
|
12404
|
-
*
|
|
12405
|
-
*
|
|
12406
|
-
* the
|
|
12407
|
-
*
|
|
12408
|
-
*
|
|
12409
|
-
*
|
|
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,
|
|
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 && !
|
|
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
|