supervision 0.2.0-next.0 → 0.2.0-next.1

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.
Files changed (172) hide show
  1. package/dist/constants/media-renderer.d.ts +3 -0
  2. package/dist/constants/media-renderer.d.ts.map +1 -1
  3. package/dist/index.d.ts +2 -5
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1246 -1300
  6. package/dist/index.js.map +1 -1
  7. package/dist/mask-preparation.worker.js +109 -31
  8. package/dist/mask-preparation.worker.js.map +1 -1
  9. package/dist/media/media-source-state.d.ts.map +1 -1
  10. package/dist/media/video-engine-media-source.d.ts +17 -7
  11. package/dist/media/video-engine-media-source.d.ts.map +1 -1
  12. package/dist/media/video-engine-media-source.js +3 -0
  13. package/dist/media/video-engine-media-source.js.map +1 -0
  14. package/dist/playback/media-playback-controller.d.ts +7 -1
  15. package/dist/playback/media-playback-controller.d.ts.map +1 -1
  16. package/dist/render-preparation/mask-frame-artifact.d.ts +2 -0
  17. package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
  18. package/dist/render-preparation/mask-frame-compositor.d.ts +7 -0
  19. package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
  20. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +1 -0
  21. package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
  22. package/dist/render-preparation/prepared-render-window.d.ts +21 -2
  23. package/dist/render-preparation/prepared-render-window.d.ts.map +1 -1
  24. package/dist/renderers/injected-pixi.d.ts +38 -0
  25. package/dist/renderers/injected-pixi.d.ts.map +1 -0
  26. package/dist/renderers/media-renderer-core.d.ts +9 -0
  27. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  28. package/dist/renderers/media-renderer-scene.d.ts +16 -5
  29. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  30. package/dist/renderers/media-renderer-state.d.ts +9 -0
  31. package/dist/renderers/media-renderer-state.d.ts.map +1 -1
  32. package/dist/renderers/media-renderer-transport.d.ts +16 -3
  33. package/dist/renderers/media-renderer-transport.d.ts.map +1 -1
  34. package/dist/renderers/pixi-focus-layer.d.ts +1 -0
  35. package/dist/renderers/pixi-focus-layer.d.ts.map +1 -1
  36. package/dist/renderers/pixi-frame-present.d.ts +1 -1
  37. package/dist/renderers/pixi-frame-present.d.ts.map +1 -1
  38. package/dist/renderers/pixi-id-mask-shader.d.ts +4 -33
  39. package/dist/renderers/pixi-id-mask-shader.d.ts.map +1 -1
  40. package/dist/renderers/pixi-interaction-presentation-layer.d.ts +0 -1
  41. package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
  42. package/dist/renderers/pixi-mask-halo.d.ts +4 -33
  43. package/dist/renderers/pixi-mask-halo.d.ts.map +1 -1
  44. package/dist/renderers/pixi-mask-layer.d.ts +28 -29
  45. package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
  46. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  47. package/dist/renderers/pixi-polygon-layer.d.ts +2 -0
  48. package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -1
  49. package/dist/renderers/pixi-region-coverage-mask.d.ts +4 -33
  50. package/dist/renderers/pixi-region-coverage-mask.d.ts.map +1 -1
  51. package/dist/renderers/pixi-region-layer.d.ts +6 -31
  52. package/dist/renderers/pixi-region-layer.d.ts.map +1 -1
  53. package/dist/renderers/pixi-shader-lifecycle.d.ts +14 -0
  54. package/dist/renderers/pixi-shader-lifecycle.d.ts.map +1 -0
  55. package/dist/renderers/presented-frame-channel.d.ts +32 -1
  56. package/dist/renderers/presented-frame-channel.d.ts.map +1 -1
  57. package/dist/sessions/media-session-defaults.d.ts.map +1 -1
  58. package/dist/sessions/media-session-state.d.ts.map +1 -1
  59. package/dist/tracking.worker.js +19 -5
  60. package/dist/tracking.worker.js.map +1 -1
  61. package/dist/types/media-session.d.ts +20 -27
  62. package/dist/types/media-session.d.ts.map +1 -1
  63. package/dist/types/render-preparation.d.ts +70 -23
  64. package/dist/types/render-preparation.d.ts.map +1 -1
  65. package/dist/video-engine-media-source-CUXnOJOV.js +450 -0
  66. package/dist/video-engine-media-source-CUXnOJOV.js.map +1 -0
  67. package/dist/web-video-engine/analysis-session.d.ts +0 -1
  68. package/dist/web-video-engine/analysis.d.ts +0 -1
  69. package/dist/web-video-engine/analysis.js +195 -23
  70. package/dist/web-video-engine/cache-budget.d.ts +0 -1
  71. package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts +0 -1
  72. package/dist/web-video-engine/clock.d.ts +0 -1
  73. package/dist/web-video-engine/constants.d.ts +9 -1
  74. package/dist/web-video-engine/create-scrub-cursor.d.ts +6 -3
  75. package/dist/web-video-engine/decode-resolution.d.ts +0 -1
  76. package/dist/web-video-engine/decode-scheduler.d.ts +18 -3
  77. package/dist/web-video-engine/decode-session.d.ts +8 -1
  78. package/dist/web-video-engine/decode-source.d.ts +9 -1
  79. package/dist/web-video-engine/diagnostics-store.d.ts +0 -1
  80. package/dist/web-video-engine/diagnostics.d.ts +19 -19
  81. package/dist/web-video-engine/embedded-engine-worker.d.ts +0 -1
  82. package/dist/web-video-engine/engine-core.d.ts +22 -11
  83. package/dist/web-video-engine/engine.d.ts +3 -4
  84. package/dist/web-video-engine/engine.js +76 -37
  85. package/dist/web-video-engine/engine.worker.d.ts +0 -1
  86. package/dist/web-video-engine/engine.worker.js +5501 -3197
  87. package/dist/web-video-engine/frame-cache.d.ts +119 -12
  88. package/dist/web-video-engine/frame-extractor.d.ts +0 -1
  89. package/dist/web-video-engine/{frame-timeline-GINI8Gum.js → frame-timeline-DreEwsRY.js} +83 -27
  90. package/dist/web-video-engine/frame-timeline.d.ts +10 -1
  91. package/dist/web-video-engine/frame-walker.d.ts +0 -1
  92. package/dist/web-video-engine/index.d.ts +12 -2
  93. package/dist/web-video-engine/index.d.ts.map +1 -1
  94. package/dist/web-video-engine/index.js +2 -2
  95. package/dist/web-video-engine/key-packet.d.ts +0 -1
  96. package/dist/web-video-engine/keyframe-index.d.ts +0 -1
  97. package/dist/web-video-engine/mirror-store.d.ts +0 -1
  98. package/dist/web-video-engine/renderer.d.ts +0 -1
  99. package/dist/web-video-engine/rotation.d.ts +0 -1
  100. package/dist/web-video-engine/scrub-controller.d.ts +0 -1
  101. package/dist/web-video-engine/scrub-cursor.d.ts +12 -7
  102. package/dist/web-video-engine/scrub-trajectory.d.ts +0 -1
  103. package/dist/web-video-engine/source-residency.d.ts +35 -1
  104. package/dist/web-video-engine/trace-recorder.d.ts +0 -1
  105. package/dist/web-video-engine/types.d.ts +18 -22
  106. package/dist/web-video-engine/video-engine.d.ts +27 -16
  107. package/dist/web-video-engine/webgpu-renderer.d.ts +0 -1
  108. package/dist/web-video-engine/worker-bridge.d.ts +0 -1
  109. package/dist/web-video-engine/worker-dispatch.d.ts +0 -1
  110. package/dist/web-video-engine/worker-protocol.d.ts +29 -14
  111. package/node_modules/supervision-js-core/dist/detections/buffered-detection-timeline.d.ts.map +1 -1
  112. package/node_modules/supervision-js-core/dist/index.d.ts +2 -2
  113. package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
  114. package/node_modules/supervision-js-core/dist/index.js +107 -29
  115. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  116. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +12 -0
  117. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
  118. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +28 -26
  119. package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
  120. package/node_modules/supervision-js-core/dist/types/session-lifecycle.d.ts +16 -0
  121. package/node_modules/supervision-js-core/dist/types/session-lifecycle.d.ts.map +1 -1
  122. package/node_modules/supervision-js-core/dist/utils/detection-masks.d.ts.map +1 -1
  123. package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts +19 -0
  124. package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts.map +1 -1
  125. package/package.json +3 -3
  126. package/dist/media/media-condition-probe.d.ts +0 -51
  127. package/dist/media/media-condition-probe.d.ts.map +0 -1
  128. package/dist/media/media-conditions.d.ts +0 -28
  129. package/dist/media/media-conditions.d.ts.map +0 -1
  130. package/dist/types/media-conditions.d.ts +0 -149
  131. package/dist/types/media-conditions.d.ts.map +0 -1
  132. package/dist/web-video-engine/analysis-session.d.ts.map +0 -1
  133. package/dist/web-video-engine/analysis.d.ts.map +0 -1
  134. package/dist/web-video-engine/analysis.js.map +0 -1
  135. package/dist/web-video-engine/cache-budget.d.ts.map +0 -1
  136. package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts.map +0 -1
  137. package/dist/web-video-engine/clock.d.ts.map +0 -1
  138. package/dist/web-video-engine/constants.d.ts.map +0 -1
  139. package/dist/web-video-engine/create-scrub-cursor.d.ts.map +0 -1
  140. package/dist/web-video-engine/decode-resolution.d.ts.map +0 -1
  141. package/dist/web-video-engine/decode-scheduler.d.ts.map +0 -1
  142. package/dist/web-video-engine/decode-session.d.ts.map +0 -1
  143. package/dist/web-video-engine/decode-source.d.ts.map +0 -1
  144. package/dist/web-video-engine/diagnostics-store.d.ts.map +0 -1
  145. package/dist/web-video-engine/diagnostics.d.ts.map +0 -1
  146. package/dist/web-video-engine/embedded-engine-worker.d.ts.map +0 -1
  147. package/dist/web-video-engine/engine-core.d.ts.map +0 -1
  148. package/dist/web-video-engine/engine.d.ts.map +0 -1
  149. package/dist/web-video-engine/engine.js.map +0 -1
  150. package/dist/web-video-engine/engine.worker.d.ts.map +0 -1
  151. package/dist/web-video-engine/engine.worker.js.map +0 -1
  152. package/dist/web-video-engine/frame-cache.d.ts.map +0 -1
  153. package/dist/web-video-engine/frame-extractor.d.ts.map +0 -1
  154. package/dist/web-video-engine/frame-timeline-GINI8Gum.js.map +0 -1
  155. package/dist/web-video-engine/frame-timeline.d.ts.map +0 -1
  156. package/dist/web-video-engine/frame-walker.d.ts.map +0 -1
  157. package/dist/web-video-engine/key-packet.d.ts.map +0 -1
  158. package/dist/web-video-engine/keyframe-index.d.ts.map +0 -1
  159. package/dist/web-video-engine/mirror-store.d.ts.map +0 -1
  160. package/dist/web-video-engine/renderer.d.ts.map +0 -1
  161. package/dist/web-video-engine/rotation.d.ts.map +0 -1
  162. package/dist/web-video-engine/scrub-controller.d.ts.map +0 -1
  163. package/dist/web-video-engine/scrub-cursor.d.ts.map +0 -1
  164. package/dist/web-video-engine/scrub-trajectory.d.ts.map +0 -1
  165. package/dist/web-video-engine/source-residency.d.ts.map +0 -1
  166. package/dist/web-video-engine/trace-recorder.d.ts.map +0 -1
  167. package/dist/web-video-engine/types.d.ts.map +0 -1
  168. package/dist/web-video-engine/video-engine.d.ts.map +0 -1
  169. package/dist/web-video-engine/webgpu-renderer.d.ts.map +0 -1
  170. package/dist/web-video-engine/worker-bridge.d.ts.map +0 -1
  171. package/dist/web-video-engine/worker-dispatch.d.ts.map +0 -1
  172. package/dist/web-video-engine/worker-protocol.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
- import { copySortedDetectionFrames, detectionFrameOverlapsRange, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, decodeCompressedRleMask, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MAX_ID_MASK_PALETTE_ENTRIES, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, createDetectionPickKey, rebaseDetectionPickToFrame, pickDetectionAtPoint, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, DetectionBufferStatus, MAX_ID_MASK_STROKE_WIDTH, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, RegionRendererMediaEffectKind, RegionRendererSourceKind, RegionRendererCoverageKind, RegionRendererSizeSpace, resolveStyleValue, resolveAnnotationStyleState, lightenColor, resolveAnnotationRendererStyleFields, annotationRendererKinds, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation, projectDetectionFrames, DetectionFrameSelectionMode } from 'supervision-js-core';
1
+ import { copySortedDetectionFrames, detectionFrameOverlapsRange, projectDetectionFrameForTracking, createSortTracker, createOCSortTracker, createCBIoUTracker, createByteTrackTracker, MediaErrorKind, includeDefined, MediaSourceStatus, MediaRendererPlaybackState, MediaRendererFit, createDefaultAnnotationPresentation, createProjectedDetectionFrameSource, createArrayDetectionFrameSource, createBufferedDetectionTimeline, resolveAnnotationRendererPresentation, PlaybackGateReach, createIdleDetectionBufferState, MediaInteractionMode, decodeCompressedRleMask, createIdMaskFrame, encodeBinaryMask, rasterizePolygonToMask, resolveIdMaskPaletteId, decodeCompressedRleCounts, canReuseMaskStyleArtifacts, getBufferedDetectionTimelineFrameSnapshot, MAX_ID_MASK_PALETTE_ENTRIES, StrokeAlignment, BaseBoxStyle, centerRectToTopLeftRect, BoxShape, BaseFocusStyle, extractDetectionMaskRectRuns, AnnotationGestureStateKind, AnnotationHandleKind, getDetectionRect, DetectionPickTarget, createDetectionPickKey, rebaseDetectionPickToFrame, pickDetectionAtPoint, followDetectionPickAcrossFrames, haveSameDetectionPickIdentities, pickAnnotationHandle, getAnnotationHandles, LabelPlacement, BasePolygonStyle, BasePolylineStyle, BaseKeypointStyle, ShapeInstructionKind, sampleEllipseArc, resolveEllipseSegmentCount, KeypointMarkerShape, MarkerSizeSpace, MarkerShape, resolveMarkerGeometry, BaseInteractionStyle, DetectionInteractionState, writeIdMaskPaletteEntry, resolveIdMaskStrokeTexels, MAX_ID_MASK_STROKE_WIDTH, DetectionBufferStatus, resolveMaskStyleOpacity, pickDetectionByMaskId, BoxStrokeAlignment, RegionRendererMediaEffectKind, RegionRendererSourceKind, RegionRendererCoverageKind, RegionRendererSizeSpace, resolveStyleValue, resolveAnnotationStyleState, lightenColor, resolveAnnotationRendererStyleFields, annotationRendererKinds, createViewportController, MediaSessionMode, DetectionFrameRetentionMode, createMemoryColdDetectionFrameStore, createWritableDetectionFrameSource, createCompositeDetectionFrameSource, MediaSessionActivityStatus, MediaSessionActivityKind, MediaSessionStatus, createSourceAwarePresentation, projectDetectionFrames, DetectionFrameSelectionMode } from 'supervision-js-core';
2
2
  export { BaseBoxCornerStyle, BaseBoxStyle, BaseFocusStyle, BaseInteractionStyle, BaseKeypointStyle, BaseLabelStyle, BaseMarkerStyle, BaseMaskStyle, BasePolygonStyle, BasePolylineStyle, BoxShape, BoxStrokeAlignment, DEFAULT_DETECTION_CLASS_STYLES, DEFAULT_DETECTION_COLOR_SEQUENCE, DetectionBufferStatus, DetectionFrameRetentionMode, DetectionFrameSelectionMode, DetectionInteractionState, DetectionMaskEncoding, DetectionPickTarget, FocusTargetMode, KeypointMarkerShape, KeypointVisibility, LabelPlacement, LabelVisibilityMode, MarkerShape, MarkerSizeSpace, MaskRenderMode, MediaErrorKind, MediaInteractionMode, MediaRendererFit, MediaRendererPlaybackState, MediaSessionActivityKind, MediaSessionActivityStatus, MediaSessionMode, MediaSessionStatus, MediaSourceStatus, PlaybackGateReach, RegionRendererComposeMode, RegionRendererCoverageKind, RegionRendererMediaEffectKind, RegionRendererRegionKind, RegionRendererSizeSpace, RegionRendererSourceKind, SUPERVISION_ROBOFLOW_COLOR, TrackingGeometry, annotationRendererKinds, annotationRenderers, createArrayDetectionFrameSource, createBufferedDetectionTimeline, createByteTrackTracker, createCBIoUTracker, createColdDetectionFrameSource, createCompositeDetectionFrameSource, createDefaultAnnotationPresentation, createMemoryColdDetectionFrameStore, createOCSortTracker, createProjectedDetectionFrameSource, createSortTracker, createWritableDetectionFrameSource, detectionPostProcessors, normalizeDetectionClassName, pickDetectionAtPoint, projectDetectionFrame, projectDetectionFrameForTracking, projectDetectionFrames, resolveDetectionClassColorStyle } from 'supervision-js-core';
3
+ import { M as MediaSourceError, t as toMediaSourceError, r as resolveDisplayPixelRatio } from './video-engine-media-source-CUXnOJOV.js';
4
+ export { c as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-CUXnOJOV.js';
3
5
 
4
6
  const DEFAULT_DATABASE_NAME = "supervision-js-detection-frames";
5
7
  const DEFAULT_CHUNK_DURATION_SECONDS = 1;
@@ -495,7 +497,7 @@ function getDetectionFrameDedupeKey(frame) {
495
497
  : `index:${frame.frameIndex}`;
496
498
  }
497
499
 
498
- const EMBEDDED_TRACKING_WORKER_SOURCE = "(function () {\n 'use strict';\n\n /** COCO-compatible keypoint visibility values. */\n var KeypointVisibility;\n (function (KeypointVisibility) {\n KeypointVisibility[KeypointVisibility[\"NotLabeled\"] = 0] = \"NotLabeled\";\n KeypointVisibility[KeypointVisibility[\"Occluded\"] = 1] = \"Occluded\";\n KeypointVisibility[KeypointVisibility[\"Visible\"] = 2] = \"Visible\";\n })(KeypointVisibility || (KeypointVisibility = {}));\n var DetectionMaskEncoding;\n (function (DetectionMaskEncoding) {\n DetectionMaskEncoding[\"CompressedRle\"] = \"compressedRle\";\n })(DetectionMaskEncoding || (DetectionMaskEncoding = {}));\n\n var DetectionBufferStatus;\n (function (DetectionBufferStatus) {\n DetectionBufferStatus[\"Idle\"] = \"idle\";\n DetectionBufferStatus[\"Loading\"] = \"loading\";\n /**\n * The playback gate is holding playback until the source produces detections\n * for the requested range. {@link DetectionBufferStatus.Loading} fetches\n * detections the source already has; this one waits on a producer.\n */\n DetectionBufferStatus[\"AwaitingCoverage\"] = \"awaitingCoverage\";\n DetectionBufferStatus[\"Ready\"] = \"ready\";\n DetectionBufferStatus[\"Error\"] = \"error\";\n DetectionBufferStatus[\"Destroyed\"] = \"destroyed\";\n })(DetectionBufferStatus || (DetectionBufferStatus = {}));\n /**\n * How the renderer selects the active detection frame for a media timestamp.\n */\n var DetectionFrameSelectionMode;\n (function (DetectionFrameSelectionMode) {\n /**\n * Select the frame whose `[mediaTime, endTime)` interval contains the media\n * time. This is the default for interval annotations and timestamped sources.\n *\n * Frame times must come from the media itself. Times reconstructed from a\n * nominal frame rate drift against a clip whose real rate differs, and once\n * that drift passes the half-millisecond selection tolerance a playhead\n * landing on a frame boundary selects the previous frame's detections.\n *\n * A frame that carries no `endTime` stays active until the next frame starts,\n * which bridges an index the source never produced.\n * {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to\n * one grid step instead, so a missing index reads as no detections.\n */\n DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex` and\n * `frameRate`. This is useful when inference was run on normalized frames and\n * playback should snap detections to that grid.\n *\n * A frame speaks for one grid step starting at its own media time, and an\n * index the source never produced selects nothing rather than a neighbour.\n *\n * A frame carrying no `frameIndex` speaks for its step on the same terms,\n * reached by the time it starts at rather than by an index that names it. Its\n * `endTime` does not widen it past that step, so a source that labels only\n * part of what it writes cannot bridge the indexes it never produced.\n */\n DetectionFrameSelectionMode[\"NearestFrameIndex\"] = \"nearestFrameIndex\";\n })(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));\n var DetectionFrameRetentionMode;\n (function (DetectionFrameRetentionMode) {\n /**\n * Keep writable detections only in an in-memory store. Useful for ephemeral\n * live streams where old predictions should disappear when evicted.\n */\n DetectionFrameRetentionMode[\"MemoryOnly\"] = \"memoryOnly\";\n /**\n * Persist every written detection frame. Useful for finite media where seek\n * and replay should not require recomputing detections.\n */\n DetectionFrameRetentionMode[\"PersistAll\"] = \"persistAll\";\n /**\n * Persist only the most recent retention window. Useful for long-running\n * streams where replay is bounded to a recent time horizon.\n */\n DetectionFrameRetentionMode[\"PersistWindow\"] = \"persistWindow\";\n })(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));\n\n var AnnotationFrameMutationKind;\n (function (AnnotationFrameMutationKind) {\n AnnotationFrameMutationKind[\"Add\"] = \"add\";\n AnnotationFrameMutationKind[\"Remove\"] = \"remove\";\n AnnotationFrameMutationKind[\"Replace\"] = \"replace\";\n AnnotationFrameMutationKind[\"Transact\"] = \"transact\";\n AnnotationFrameMutationKind[\"Update\"] = \"update\";\n })(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));\n\n /**\n * Creates one stateful SORT tracker for a single ordered media sequence.\n *\n * Defaults, lifecycle semantics, and observation-only output mirror\n * roboflow/trackers SORT. Motion predictions remain internal to association.\n */\n function createSortTracker$1(options = {}) {\n const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, \"lostTrackBuffer\");\n const frameRate = options.frameRate ?? 30;\n const trackActivationThreshold = options.trackActivationThreshold ?? 0.25;\n const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 3, \"minimumConsecutiveFrames\");\n const minimumIouThreshold = options.minimumIouThreshold ?? 0.3;\n if (!Number.isFinite(frameRate) || frameRate <= 0) {\n throw new Error(\"frameRate must be a finite positive value.\");\n }\n normalizeUnitInterval$1(trackActivationThreshold, \"trackActivationThreshold\");\n normalizeUnitInterval$1(minimumIouThreshold, \"minimumIouThreshold\");\n const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;\n if (!Number.isFinite(scaledLostTrackBuffer)) {\n throw new Error(\"Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.\");\n }\n const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));\n let tracks = [];\n let nextTrackerId = 0;\n let previousFrameIndex;\n return {\n reset() {\n tracks = [];\n nextTrackerId = 0;\n previousFrameIndex = undefined;\n },\n update(detections, frameIndex) {\n const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);\n previousFrameIndex = frameIndex ?? previousFrameIndex;\n const predicted = tracks.map((track) => track.predict(frameStep, frameRate));\n const { matches, unmatchedDetections } = associateDetectionsToTracks(detections, predicted, minimumIouThreshold);\n const trackerIds = new Map();\n for (const match of matches) {\n const track = tracks[match.trackIndex];\n const detection = detections[match.detectionIndex];\n track.update(detection);\n if (track.trackerId === undefined &&\n track.successfulUpdates >= minimumConsecutiveFrames) {\n track.trackerId = nextTrackerId;\n nextTrackerId += 1;\n }\n if (track.trackerId !== undefined) {\n trackerIds.set(detection.detectionIndex, track.trackerId);\n }\n }\n for (const detectionIndex of unmatchedDetections) {\n const detection = detections[detectionIndex];\n if ((detection.confidence ?? 1) >= trackActivationThreshold) {\n tracks.push(new KalmanBoxTrack(detection));\n }\n }\n tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate &&\n (track.successfulUpdates >= minimumConsecutiveFrames ||\n track.timeSinceUpdate === 0));\n return {\n activeTrackCount: tracks.length,\n assignments: detections.flatMap((detection) => {\n const trackerId = trackerIds.get(detection.detectionIndex);\n return trackerId === undefined\n ? []\n : [{ detectionIndex: detection.detectionIndex, trackerId }];\n }),\n confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined).length,\n };\n },\n };\n }\n class KalmanBoxTrack {\n consecutiveUpdates;\n trackerId;\n successfulUpdates = 1;\n timeSinceUpdate = 0;\n state;\n covariance = identity(8);\n constructor(detection, consecutiveUpdates = false) {\n this.consecutiveUpdates = consecutiveUpdates;\n this.state = [...rectToXyxy(detection.rect), 0, 0, 0, 0].map((value) => [\n value,\n ]);\n }\n predict(frameStep, frameRate) {\n if (this.consecutiveUpdates && this.timeSinceUpdate > 0) {\n this.successfulUpdates = 0;\n }\n const transition = identity(8);\n for (let index = 0; index < 4; index += 1) {\n transition[index][index + 4] = frameStep;\n }\n this.state = multiply(transition, this.state);\n this.covariance = add(multiply(multiply(transition, this.covariance), transpose(transition)), createProcessNoise(frameStep, frameRate));\n // Python SORT counts update calls for the fixed-rate lost-track budget.\n this.timeSinceUpdate += 1;\n return stateToRect(this.state);\n }\n update(detection) {\n const measurement = rectToXyxy(detection.rect).map((value) => [value]);\n const observation = [\n [1, 0, 0, 0, 0, 0, 0, 0],\n [0, 1, 0, 0, 0, 0, 0, 0],\n [0, 0, 1, 0, 0, 0, 0, 0],\n [0, 0, 0, 1, 0, 0, 0, 0],\n ];\n const measurementNoise = scale(identity(4), 0.1);\n const innovation = subtract(measurement, multiply(observation, this.state));\n const covarianceObservationTranspose = multiply(this.covariance, transpose(observation));\n const innovationCovariance = add(multiply(observation, covarianceObservationTranspose), measurementNoise);\n const gain = multiply(covarianceObservationTranspose, inverse(innovationCovariance));\n this.state = add(this.state, multiply(gain, innovation));\n const identityMinusGainObservation = subtract(identity(8), multiply(gain, observation));\n // Joseph form matches the Python implementation and is more stable.\n this.covariance = add(multiply(multiply(identityMinusGainObservation, this.covariance), transpose(identityMinusGainObservation)), multiply(multiply(gain, measurementNoise), transpose(gain)));\n this.timeSinceUpdate = 0;\n this.successfulUpdates += 1;\n }\n getStateRect() {\n return stateToRect(this.state);\n }\n }\n function associateDetectionsToTracks(detections, predicted, minimumIouThreshold) {\n if (predicted.length === 0 || detections.length === 0) {\n return {\n matches: [],\n unmatchedDetections: detections.map((_, index) => index),\n };\n }\n // Python SORT associates all detections class-agnostically using standard IoU.\n const scores = predicted.map((rect) => detections.map((detection) => intersectionOverUnion(rect, detection.rect)));\n const candidateMatches = maximizeAssignment(scores);\n const matches = [];\n const matchedDetections = new Set();\n for (const [trackIndex, detectionIndex] of candidateMatches) {\n if (scores[trackIndex][detectionIndex] < minimumIouThreshold)\n continue;\n matches.push({ detectionIndex, trackIndex });\n matchedDetections.add(detectionIndex);\n }\n matches.sort((left, right) => left.trackIndex - right.trackIndex);\n return {\n matches,\n unmatchedDetections: detections.flatMap((_, index) => matchedDetections.has(index) ? [] : [index]),\n };\n }\n /** Hungarian assignment for a rectangular score matrix. */\n function maximizeAssignment(scores) {\n if (scores.length === 0 || scores[0]?.length === 0) {\n return [];\n }\n const rowCount = scores.length;\n const columnCount = scores[0].length;\n const transposed = rowCount > columnCount;\n const costs = transposed\n ? Array.from({ length: columnCount }, (_, row) => Array.from({ length: rowCount }, (_, column) => 1 - scores[column][row]))\n : scores.map((row) => row.map((score) => 1 - score));\n const rows = costs.length;\n const columns = costs[0].length;\n const u = new Array(rows + 1).fill(0);\n const v = new Array(columns + 1).fill(0);\n const p = new Array(columns + 1).fill(0);\n const way = new Array(columns + 1).fill(0);\n for (let row = 1; row <= rows; row += 1) {\n p[0] = row;\n let column0 = 0;\n const minValue = new Array(columns + 1).fill(Number.POSITIVE_INFINITY);\n const used = new Array(columns + 1).fill(false);\n do {\n used[column0] = true;\n const row0 = p[column0];\n let delta = Number.POSITIVE_INFINITY;\n let column1 = 0;\n for (let column = 1; column <= columns; column += 1) {\n if (used[column])\n continue;\n const current = costs[row0 - 1][column - 1] - u[row0] - v[column];\n if (current < minValue[column]) {\n minValue[column] = current;\n way[column] = column0;\n }\n if (minValue[column] < delta) {\n delta = minValue[column];\n column1 = column;\n }\n }\n for (let column = 0; column <= columns; column += 1) {\n if (used[column]) {\n u[p[column]] += delta;\n v[column] -= delta;\n }\n else {\n minValue[column] -= delta;\n }\n }\n column0 = column1;\n } while (p[column0] !== 0);\n do {\n const column1 = way[column0];\n p[column0] = p[column1];\n column0 = column1;\n } while (column0 !== 0);\n }\n const assignments = [];\n for (let column = 1; column <= columns; column += 1) {\n if (p[column] === 0)\n continue;\n const row = p[column] - 1;\n assignments.push(transposed ? [column - 1, row] : [row, column - 1]);\n }\n return assignments;\n }\n function rectToXyxy(rect) {\n return [\n rect.x - rect.width / 2,\n rect.y - rect.height / 2,\n rect.x + rect.width / 2,\n rect.y + rect.height / 2,\n ];\n }\n function stateToRect(state) {\n // The Python XYXY estimator intentionally leaves corner velocities\n // unconstrained. Normalize only at the browser Rect boundary so a crossing\n // prediction cannot violate the positive-width/height storage contract.\n const x1 = Math.min(state[0][0], state[2][0]);\n const y1 = Math.min(state[1][0], state[3][0]);\n const x2 = Math.max(state[0][0], state[2][0]);\n const y2 = Math.max(state[1][0], state[3][0]);\n return {\n height: Math.max(Number.EPSILON, y2 - y1),\n width: Math.max(Number.EPSILON, x2 - x1),\n x: (x1 + x2) / 2,\n y: (y1 + y2) / 2,\n };\n }\n function createProcessNoise(frameStep, frameRate) {\n if (Math.abs(frameStep - 1) <= 0.004 * frameRate) {\n return scale(identity(8), 0.01);\n }\n const result = Array.from({ length: 8 }, () => new Array(8).fill(0));\n const dt2 = frameStep * frameStep;\n const dt3 = dt2 * frameStep;\n const dt4 = dt2 * dt2;\n for (let index = 0; index < 4; index += 1) {\n const velocityIndex = index + 4;\n result[index][index] = (0.01 * dt4) / 4;\n result[index][velocityIndex] = (0.01 * dt3) / 2;\n result[velocityIndex][index] = (0.01 * dt3) / 2;\n result[velocityIndex][velocityIndex] = 0.01 * dt2;\n }\n return result;\n }\n function intersectionOverUnion(left, right) {\n const leftX = left.x - left.width / 2;\n const leftY = left.y - left.height / 2;\n const rightX = right.x - right.width / 2;\n const rightY = right.y - right.height / 2;\n const intersectionWidth = Math.max(0, Math.min(leftX + left.width, rightX + right.width) -\n Math.max(leftX, rightX));\n const intersectionHeight = Math.max(0, Math.min(leftY + left.height, rightY + right.height) -\n Math.max(leftY, rightY));\n const intersection = intersectionWidth * intersectionHeight;\n const union = left.width * left.height + right.width * right.height - intersection;\n return union <= 0 ? 0 : intersection / union;\n }\n function resolveFrameStep(current, previous) {\n if (current === undefined || previous === undefined)\n return 1;\n return Math.max(1, current - previous);\n }\n function normalizePositiveInteger$1(value, label) {\n if (!Number.isInteger(value) || value < 1) {\n throw new Error(`${label} must be a positive integer.`);\n }\n return value;\n }\n function normalizeNonNegativeInteger$1(value, label) {\n if (!Number.isInteger(value) || value < 0) {\n throw new Error(`${label} must be a non-negative integer.`);\n }\n return value;\n }\n function normalizeUnitInterval$1(value, label) {\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n throw new Error(`${label} must be between 0 and 1.`);\n }\n }\n function identity(size) {\n return Array.from({ length: size }, (_, row) => Array.from({ length: size }, (_, column) => (row === column ? 1 : 0)));\n }\n function scale(matrix, factor) {\n return matrix.map((row) => row.map((value) => value * factor));\n }\n function transpose(matrix) {\n return matrix[0].map((_, column) => matrix.map((row) => row[column]));\n }\n function add(left, right) {\n return left.map((row, rowIndex) => row.map((value, columnIndex) => value + right[rowIndex][columnIndex]));\n }\n function subtract(left, right) {\n return left.map((row, rowIndex) => row.map((value, columnIndex) => value - right[rowIndex][columnIndex]));\n }\n function multiply(left, right) {\n return left.map((row) => right[0].map((_, column) => row.reduce((sum, value, index) => sum + value * right[index][column], 0)));\n }\n function inverse(matrix) {\n const size = matrix.length;\n const augmented = matrix.map((row, index) => [\n ...row,\n ...identity(size)[index],\n ]);\n for (let column = 0; column < size; column += 1) {\n let pivot = column;\n for (let row = column + 1; row < size; row += 1) {\n if (Math.abs(augmented[row][column]) >\n Math.abs(augmented[pivot][column])) {\n pivot = row;\n }\n }\n if (Math.abs(augmented[pivot][column]) < 1e-12) {\n throw new Error(\"SORT Kalman covariance is singular.\");\n }\n [augmented[column], augmented[pivot]] = [\n augmented[pivot],\n augmented[column],\n ];\n const divisor = augmented[column][column];\n augmented[column] = augmented[column].map((value) => value / divisor);\n for (let row = 0; row < size; row += 1) {\n if (row === column)\n continue;\n const factor = augmented[row][column];\n augmented[row] = augmented[row].map((value, index) => value - factor * augmented[column][index]);\n }\n }\n return augmented.map((row) => row.slice(size));\n }\n\n /**\n * Creates one stateful ByteTrack tracker for a single ordered media sequence.\n *\n * Defaults and two-stage association mirror roboflow/trackers ByteTrack at\n * source commit 60b21c8a48676784085fbee455559f16b75a7c9a.\n * Motion predictions remain internal; only observed detections receive IDs.\n */\n function createByteTrackTracker$1(options = {}) {\n const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, \"lostTrackBuffer\");\n const frameRate = options.frameRate ?? 30;\n const trackActivationThreshold = options.trackActivationThreshold ?? 0.7;\n const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 2, \"minimumConsecutiveFrames\");\n const minimumIouThreshold = options.minimumIouThreshold ?? 0.1;\n const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ?? 0.6;\n if (!Number.isFinite(frameRate) || frameRate <= 0) {\n throw new Error(\"frameRate must be a finite positive value.\");\n }\n normalizeUnitInterval$1(trackActivationThreshold, \"trackActivationThreshold\");\n normalizeUnitInterval$1(minimumIouThreshold, \"minimumIouThreshold\");\n normalizeUnitInterval$1(highConfidenceDetectionThreshold, \"highConfidenceDetectionThreshold\");\n const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;\n if (!Number.isFinite(scaledLostTrackBuffer)) {\n throw new Error(\"Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.\");\n }\n const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));\n let tracks = [];\n let nextTrackerId = 0;\n let previousFrameIndex;\n return {\n reset() {\n tracks = [];\n nextTrackerId = 0;\n previousFrameIndex = undefined;\n },\n update(detections, frameIndex) {\n const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);\n previousFrameIndex = frameIndex ?? previousFrameIndex;\n tracks.forEach((track) => track.predict(frameStep, frameRate));\n const highDetectionIndexes = [];\n const lowDetectionIndexes = [];\n detections.forEach((detection, index) => {\n if ((detection.confidence ?? 1) >= highConfidenceDetectionThreshold) {\n highDetectionIndexes.push(index);\n }\n else {\n lowDetectionIndexes.push(index);\n }\n });\n const assignments = new Map();\n const firstStage = associate(tracks, detections, highDetectionIndexes, minimumIouThreshold);\n for (const match of firstStage.matches) {\n updateMatchedTrack(tracks[match.trackIndex], detections[match.detectionIndex], assignments);\n }\n const remainingTracks = firstStage.unmatchedTrackIndexes.map((index) => tracks[index]);\n const secondStage = associate(remainingTracks, detections, lowDetectionIndexes, minimumIouThreshold);\n for (const match of secondStage.matches) {\n updateMatchedTrack(remainingTracks[match.trackIndex], detections[match.detectionIndex], assignments);\n }\n // Only unmatched high-confidence observations can start a track.\n for (const detectionIndex of firstStage.unmatchedDetectionIndexes) {\n const detection = detections[detectionIndex];\n if ((detection.confidence ?? 1) >= trackActivationThreshold) {\n tracks.push(new KalmanBoxTrack(detection, true));\n }\n }\n // Confirmation is sticky once an ID has been allocated. An unmatched\n // unconfirmed track is discarded immediately, as in Python ByteTrack.\n tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate &&\n (track.trackerId !== undefined ||\n track.successfulUpdates >= minimumConsecutiveFrames ||\n track.timeSinceUpdate === 0));\n return {\n activeTrackCount: tracks.length,\n assignments: detections.flatMap((detection) => {\n const trackerId = assignments.get(detection.detectionIndex);\n return trackerId === undefined\n ? []\n : [{ detectionIndex: detection.detectionIndex, trackerId }];\n }),\n confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined).length,\n };\n },\n };\n function updateMatchedTrack(track, detection, assignments) {\n track.update(detection);\n if (track.trackerId === undefined &&\n track.successfulUpdates >= minimumConsecutiveFrames) {\n track.trackerId = nextTrackerId;\n nextTrackerId += 1;\n }\n if (track.trackerId !== undefined) {\n assignments.set(detection.detectionIndex, track.trackerId);\n }\n }\n }\n function associate(tracks, detections, detectionIndexes, minimumIouThreshold) {\n if (tracks.length === 0 || detectionIndexes.length === 0) {\n return {\n matches: [],\n unmatchedDetectionIndexes: [...detectionIndexes],\n unmatchedTrackIndexes: tracks.map((_, index) => index),\n };\n }\n const scores = tracks.map((track) => {\n const predictedRect = track.getStateRect();\n return detectionIndexes.map((detectionIndex) => intersectionOverUnion(predictedRect, detections[detectionIndex].rect));\n });\n const matches = [];\n const matchedTracks = new Set();\n const matchedDetections = new Set();\n for (const [trackIndex, localDetectionIndex] of maximizeAssignment(scores)) {\n if (scores[trackIndex][localDetectionIndex] < minimumIouThreshold) {\n continue;\n }\n const detectionIndex = detectionIndexes[localDetectionIndex];\n matches.push({ detectionIndex, trackIndex });\n matchedTracks.add(trackIndex);\n matchedDetections.add(detectionIndex);\n }\n matches.sort((left, right) => left.trackIndex - right.trackIndex);\n return {\n matches,\n unmatchedDetectionIndexes: detectionIndexes.filter((index) => !matchedDetections.has(index)),\n unmatchedTrackIndexes: tracks.flatMap((_, index) => matchedTracks.has(index) ? [] : [index]),\n };\n }\n\n function associateTrackingScores(scores, trackCount, detectionCount, minimumScore, acceptanceScores = scores) {\n const matches = [];\n const matchedTracks = new Set();\n const matchedDetections = new Set();\n for (const [trackIndex, detectionIndex] of maximizeAssignment(scores)) {\n if (acceptanceScores[trackIndex][detectionIndex] < minimumScore)\n continue;\n matches.push({ detectionIndex, trackIndex });\n matchedTracks.add(trackIndex);\n matchedDetections.add(detectionIndex);\n }\n matches.sort((left, right) => left.trackIndex - right.trackIndex);\n return {\n matches,\n unmatchedDetectionIndexes: Array.from({ length: detectionCount }, (_, index) => index).filter((index) => !matchedDetections.has(index)),\n unmatchedTrackIndexes: Array.from({ length: trackCount }, (_, index) => index).filter((index) => !matchedTracks.has(index)),\n };\n }\n function pairwiseIou(tracks, detections, bufferRatio = 0) {\n return tracks.map((track) => detections.map((detection) => bufferedIntersectionOverUnion(track, detection, bufferRatio)));\n }\n function bufferedIntersectionOverUnion(left, right, bufferRatio) {\n const leftWidth = left.width * (1 + 2 * bufferRatio);\n const leftHeight = left.height * (1 + 2 * bufferRatio);\n const rightWidth = right.width * (1 + 2 * bufferRatio);\n const rightHeight = right.height * (1 + 2 * bufferRatio);\n const leftX = left.x - leftWidth / 2;\n const leftY = left.y - leftHeight / 2;\n const rightX = right.x - rightWidth / 2;\n const rightY = right.y - rightHeight / 2;\n const intersectionWidth = Math.max(0, Math.min(leftX + leftWidth, rightX + rightWidth) - Math.max(leftX, rightX));\n const intersectionHeight = Math.max(0, Math.min(leftY + leftHeight, rightY + rightHeight) -\n Math.max(leftY, rightY));\n const intersection = intersectionWidth * intersectionHeight;\n const union = leftWidth * leftHeight + rightWidth * rightHeight - intersection;\n return union <= 0 ? 0 : intersection / union;\n }\n\n /**\n * Kalman state estimator shared by the browser C-BIoU and OC-SORT ports.\n * Its layouts, covariance update, and gap-scaled constant-velocity model\n * mirror roboflow/trackers at 60b21c8.\n */\n class TrackingKalmanEstimator {\n representation;\n dimension;\n measurementDimension = 4;\n covariance;\n measurementNoise = identity(4);\n processNoise;\n state;\n baselineProcessNoise;\n positionIndexes;\n velocityIndexes;\n constructor(initialRect, representation) {\n this.representation = representation;\n const measurement = rectToMeasurement(initialRect, representation);\n this.dimension = representation === \"xcycsr\" ? 7 : 8;\n this.positionIndexes =\n representation === \"xcycsr\" ? [0, 1, 2] : [0, 1, 2, 3];\n this.velocityIndexes =\n representation === \"xcycsr\" ? [4, 5, 6] : [4, 5, 6, 7];\n this.state = Array.from({ length: this.dimension }, (_, index) => [\n measurement[index] ?? 0,\n ]);\n this.covariance = identity(this.dimension);\n this.processNoise = identity(this.dimension);\n this.baselineProcessNoise = identity(this.dimension);\n }\n predict(frameStep, frameRate) {\n if (this.representation === \"xcycsr\" &&\n this.state[2][0] + frameStep * this.state[6][0] <= 0) {\n this.state[6][0] = 0;\n }\n const transition = identity(this.dimension);\n this.positionIndexes.forEach((positionIndex, index) => {\n transition[positionIndex][this.velocityIndexes[index]] = frameStep;\n });\n this.processNoise = this.createProcessNoise(frameStep, frameRate);\n this.state = multiply(transition, this.state);\n this.covariance = add(multiply(multiply(transition, this.covariance), transpose(transition)), this.processNoise);\n }\n update(rect) {\n this.updateMeasurement(rectToMeasurement(rect, this.representation));\n }\n updateMeasurement(measurement) {\n const observation = Array.from({ length: 4 }, (_, row) => Array.from({ length: this.dimension }, (_, column) => row === column ? 1 : 0));\n const measurementColumn = measurement.map((value) => [value]);\n const innovation = subtract(measurementColumn, multiply(observation, this.state));\n const covarianceObservationTranspose = multiply(this.covariance, transpose(observation));\n const innovationCovariance = add(multiply(observation, covarianceObservationTranspose), this.measurementNoise);\n const gain = multiply(covarianceObservationTranspose, inverse(innovationCovariance));\n this.state = add(this.state, multiply(gain, innovation));\n const identityMinusGainObservation = subtract(identity(this.dimension), multiply(gain, observation));\n this.covariance = add(multiply(multiply(identityMinusGainObservation, this.covariance), transpose(identityMinusGainObservation)), multiply(multiply(gain, this.measurementNoise), transpose(gain)));\n }\n getRect() {\n return measurementToRect(this.state.slice(0, 4).map((row) => row[0]), this.representation);\n }\n setCovariances(options) {\n if (options.covariance)\n this.covariance = clone(options.covariance);\n if (options.measurementNoise) {\n this.measurementNoise = clone(options.measurementNoise);\n }\n if (options.processNoise) {\n this.processNoise = clone(options.processNoise);\n this.baselineProcessNoise = clone(options.processNoise);\n }\n }\n snapshot() {\n return {\n covariance: clone(this.covariance),\n measurementNoise: clone(this.measurementNoise),\n processNoise: clone(this.processNoise),\n state: clone(this.state),\n };\n }\n restore(snapshot) {\n this.covariance = clone(snapshot.covariance);\n this.measurementNoise = clone(snapshot.measurementNoise);\n this.processNoise = clone(snapshot.processNoise);\n this.state = clone(snapshot.state);\n }\n createProcessNoise(frameStep, frameRate) {\n if (Math.abs(frameStep - 1) <= 0.004 * frameRate) {\n return clone(this.baselineProcessNoise);\n }\n const result = Array.from({ length: this.dimension }, () => new Array(this.dimension).fill(0));\n const dt2 = frameStep * frameStep;\n const dt3 = dt2 * frameStep;\n const dt4 = dt2 * dt2;\n const kinematicIndexes = new Set([\n ...this.positionIndexes,\n ...this.velocityIndexes,\n ]);\n this.positionIndexes.forEach((positionIndex, index) => {\n const velocityIndex = this.velocityIndexes[index];\n const accelerationVariance = this.baselineProcessNoise[velocityIndex][velocityIndex];\n result[positionIndex][positionIndex] = (accelerationVariance * dt4) / 4;\n result[positionIndex][velocityIndex] = (accelerationVariance * dt3) / 2;\n result[velocityIndex][positionIndex] = (accelerationVariance * dt3) / 2;\n result[velocityIndex][velocityIndex] = accelerationVariance * dt2;\n });\n for (let index = 0; index < this.dimension; index += 1) {\n if (!kinematicIndexes.has(index)) {\n result[index][index] = this.baselineProcessNoise[index][index];\n }\n }\n return result;\n }\n }\n function rectToMeasurement(rect, representation) {\n if (representation === \"xcycwh\") {\n return [rect.x, rect.y, rect.width, rect.height];\n }\n return [\n rect.x,\n rect.y,\n rect.width * rect.height,\n rect.width / (rect.height + 1e-6),\n ];\n }\n function measurementToRect(measurement, representation) {\n const [x, y, third, fourth] = measurement;\n if (representation === \"xcycwh\") {\n return {\n height: Math.max(1e-3, fourth),\n width: Math.max(1e-3, third),\n x,\n y,\n };\n }\n const width = Math.sqrt(third * fourth);\n const height = width === 0 ? 0 : third / width;\n return {\n height: Math.max(Number.EPSILON, height),\n width: Math.max(Number.EPSILON, width),\n x,\n y,\n };\n }\n function diagonal(values) {\n return values.map((value, row) => values.map((_, column) => (row === column ? value : 0)));\n }\n function scaleMatrix(matrix, factor) {\n return scale(matrix, factor);\n }\n function clone(matrix) {\n return matrix.map((row) => [...row]);\n }\n\n const MINIMUM_DETECTION_CONFIDENCE = 0.1;\n class CBIoUTrack {\n trackerId;\n successfulUpdates = 1;\n timeSinceUpdate = 0;\n estimator;\n constructor(initial) {\n this.estimator = new TrackingKalmanEstimator(initial.rect, \"xcycwh\");\n this.setInitialNoise(initial.rect.width, initial.rect.height);\n }\n predict(frameStep, frameRate) {\n const current = this.estimator.getRect();\n this.estimator.setCovariances({\n processNoise: this.buildProcessNoise(Math.max(current.width, 1e-3), Math.max(current.height, 1e-3)),\n });\n this.estimator.predict(frameStep, frameRate);\n this.clampState();\n this.timeSinceUpdate += 1;\n }\n update(detection) {\n const current = this.estimator.getRect();\n this.estimator.setCovariances({\n measurementNoise: this.buildMeasurementNoise(Math.max(current.width, 1e-3), Math.max(current.height, 1e-3)),\n });\n this.estimator.update(detection.rect);\n this.clampState();\n this.timeSinceUpdate = 0;\n this.successfulUpdates += 1;\n }\n getRect() {\n return this.estimator.getRect();\n }\n setInitialNoise(width, height) {\n const sigmaPosition = 0.05;\n const sigmaVelocity = 0.00625;\n this.estimator.setCovariances({\n covariance: diagonal([\n (2 * sigmaPosition * width) ** 2,\n (2 * sigmaPosition * height) ** 2,\n (2 * sigmaPosition * width) ** 2,\n (2 * sigmaPosition * height) ** 2,\n (10 * sigmaVelocity * width) ** 2,\n (10 * sigmaVelocity * height) ** 2,\n (10 * sigmaVelocity * width) ** 2,\n (10 * sigmaVelocity * height) ** 2,\n ]),\n measurementNoise: this.buildMeasurementNoise(width, height),\n processNoise: this.buildProcessNoise(width, height),\n });\n }\n buildProcessNoise(width, height) {\n const sigmaPosition = 0.05;\n const sigmaVelocity = 0.00625;\n return diagonal([\n (sigmaPosition * width) ** 2,\n (sigmaPosition * height) ** 2,\n (sigmaPosition * width) ** 2,\n (sigmaPosition * height) ** 2,\n (sigmaVelocity * width) ** 2,\n (sigmaVelocity * height) ** 2,\n (sigmaVelocity * width) ** 2,\n (sigmaVelocity * height) ** 2,\n ]);\n }\n buildMeasurementNoise(width, height) {\n const sigmaMeasurement = 0.05;\n return diagonal([\n (sigmaMeasurement * width) ** 2,\n (sigmaMeasurement * height) ** 2,\n (sigmaMeasurement * width) ** 2,\n (sigmaMeasurement * height) ** 2,\n ]);\n }\n clampState() {\n this.estimator.state[2][0] = Math.max(this.estimator.state[2][0], 1e-3);\n this.estimator.state[3][0] = Math.max(this.estimator.state[3][0], 1e-3);\n }\n }\n /** Creates the detection-only C-BIoU implementation from roboflow/trackers. */\n function createCBIoUTracker$1(options = {}) {\n const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, \"lostTrackBuffer\");\n const frameRate = options.frameRate ?? 30;\n const trackActivationThreshold = options.trackActivationThreshold ?? 0.7;\n const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 2, \"minimumConsecutiveFrames\");\n const minimumIouThresholdFirstAssociation = options.minimumIouThresholdFirstAssociation ?? 0.2;\n const minimumIouThresholdSecondAssociation = options.minimumIouThresholdSecondAssociation ?? 0.5;\n const minimumIouThresholdUnconfirmedAssociation = options.minimumIouThresholdUnconfirmedAssociation ?? 0.3;\n const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ?? 0.6;\n const instantFirstFrameActivation = options.instantFirstFrameActivation ?? true;\n const bufferRatioFirst = options.bufferRatioFirst ?? 0.3;\n const bufferRatioSecond = options.bufferRatioSecond ?? 0.5;\n if (!Number.isFinite(frameRate) || frameRate <= 0) {\n throw new Error(\"frameRate must be a finite positive value.\");\n }\n normalizeUnitInterval$1(trackActivationThreshold, \"trackActivationThreshold\");\n normalizeUnitInterval$1(minimumIouThresholdFirstAssociation, \"minimumIouThresholdFirstAssociation\");\n normalizeUnitInterval$1(minimumIouThresholdSecondAssociation, \"minimumIouThresholdSecondAssociation\");\n normalizeUnitInterval$1(minimumIouThresholdUnconfirmedAssociation, \"minimumIouThresholdUnconfirmedAssociation\");\n normalizeUnitInterval$1(highConfidenceDetectionThreshold, \"highConfidenceDetectionThreshold\");\n if (!Number.isFinite(bufferRatioFirst) || bufferRatioFirst < 0) {\n throw new Error(\"bufferRatioFirst must be a finite non-negative value.\");\n }\n if (!Number.isFinite(bufferRatioSecond) || bufferRatioSecond < 0) {\n throw new Error(\"bufferRatioSecond must be a finite non-negative value.\");\n }\n const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;\n if (!Number.isFinite(scaledLostTrackBuffer)) {\n throw new Error(\"Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.\");\n }\n const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));\n let tracks = [];\n let nextTrackerId = 0;\n let frameId = 0;\n let previousFrameIndex;\n return {\n reset() {\n tracks = [];\n nextTrackerId = 0;\n frameId = 0;\n previousFrameIndex = undefined;\n },\n update(detections, frameIndex) {\n const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);\n previousFrameIndex = frameIndex ?? previousFrameIndex;\n frameId += 1;\n tracks.forEach((track) => track.predict(frameStep, frameRate));\n const highDetectionIndexes = [];\n const lowDetectionIndexes = [];\n detections.forEach((detection, index) => {\n const confidence = detection.confidence ?? 1;\n if (confidence >= highConfidenceDetectionThreshold) {\n highDetectionIndexes.push(index);\n }\n else if (confidence > MINIMUM_DETECTION_CONFIDENCE) {\n lowDetectionIndexes.push(index);\n }\n });\n const confirmed = [];\n const unconfirmed = [];\n const lost = [];\n for (const track of tracks) {\n if (track.timeSinceUpdate > 1)\n lost.push(track);\n else if (track.trackerId !== undefined ||\n track.successfulUpdates >= minimumConsecutiveFrames) {\n confirmed.push(track);\n }\n else\n unconfirmed.push(track);\n }\n const assignments = new Map();\n const pool = [...confirmed, ...lost];\n const highDetections = highDetectionIndexes.map((index) => detections[index]);\n const firstScores = pairwiseIou(pool.map((track) => track.getRect()), highDetections.map((detection) => detection.rect), bufferRatioFirst).map((row) => row.map((score, index) => score * (highDetections[index].confidence ?? 1)));\n const firstAssociation = associateTrackingScores(firstScores, pool.length, highDetections.length, minimumIouThresholdFirstAssociation);\n for (const match of firstAssociation.matches) {\n updateMatchedTrack(pool[match.trackIndex], highDetections[match.detectionIndex], assignments);\n }\n const remainingTracked = firstAssociation.unmatchedTrackIndexes\n .map((index) => pool[index])\n .filter((track) => track.timeSinceUpdate === 1);\n const lowDetections = lowDetectionIndexes.map((index) => detections[index]);\n const secondScores = pairwiseIou(remainingTracked.map((track) => track.getRect()), lowDetections.map((detection) => detection.rect), bufferRatioSecond);\n const secondAssociation = associateTrackingScores(secondScores, remainingTracked.length, lowDetections.length, minimumIouThresholdSecondAssociation);\n for (const match of secondAssociation.matches) {\n updateMatchedTrack(remainingTracked[match.trackIndex], lowDetections[match.detectionIndex], assignments);\n }\n let unmatchedHighLocal = [...firstAssociation.unmatchedDetectionIndexes];\n let unmatchedUnconfirmed = unconfirmed.map((_, index) => index);\n if (unconfirmed.length > 0 && unmatchedHighLocal.length > 0) {\n const remainingHigh = unmatchedHighLocal.map((index) => highDetections[index]);\n const unconfirmedScores = pairwiseIou(unconfirmed.map((track) => track.getRect()), remainingHigh.map((detection) => detection.rect), bufferRatioFirst).map((row) => row.map((score, index) => score * (remainingHigh[index].confidence ?? 1)));\n const unconfirmedAssociation = associateTrackingScores(unconfirmedScores, unconfirmed.length, remainingHigh.length, minimumIouThresholdUnconfirmedAssociation);\n unmatchedUnconfirmed = unconfirmedAssociation.unmatchedTrackIndexes;\n for (const match of unconfirmedAssociation.matches) {\n updateMatchedTrack(unconfirmed[match.trackIndex], remainingHigh[match.detectionIndex], assignments);\n }\n unmatchedHighLocal =\n unconfirmedAssociation.unmatchedDetectionIndexes.map((index) => unmatchedHighLocal[index]);\n }\n const unmatchedUnconfirmedTracks = new Set(unmatchedUnconfirmed.map((index) => unconfirmed[index]));\n tracks = tracks.filter((track) => !unmatchedUnconfirmedTracks.has(track));\n for (const localIndex of unmatchedHighLocal) {\n const detection = highDetections[localIndex];\n if ((detection.confidence ?? 1) < trackActivationThreshold)\n continue;\n const track = new CBIoUTrack(detection);\n if (frameId === 1 && instantFirstFrameActivation) {\n track.trackerId = nextTrackerId++;\n assignments.set(detection.detectionIndex, track.trackerId);\n }\n tracks.push(track);\n }\n tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate &&\n (track.timeSinceUpdate === 0 ||\n track.trackerId !== undefined ||\n track.successfulUpdates >= minimumConsecutiveFrames));\n return createUpdate(detections, assignments, tracks);\n },\n };\n function updateMatchedTrack(track, detection, assignments) {\n track.update(detection);\n if (track.trackerId === undefined &&\n track.successfulUpdates >= minimumConsecutiveFrames) {\n track.trackerId = nextTrackerId++;\n }\n if (track.trackerId !== undefined) {\n assignments.set(detection.detectionIndex, track.trackerId);\n }\n }\n }\n function createUpdate(detections, assignments, tracks) {\n const resolvedAssignments = detections.flatMap((detection) => {\n const trackerId = assignments.get(detection.detectionIndex);\n return trackerId === undefined\n ? []\n : [{ detectionIndex: detection.detectionIndex, trackerId }];\n });\n return {\n activeTrackCount: tracks.length,\n assignments: resolvedAssignments,\n confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined)\n .length,\n };\n }\n\n class OCSortTrack {\n deltaT;\n age = 0;\n lastObservation;\n trackerId;\n successfulConsecutiveUpdates = 0;\n timeSinceUpdate = 0;\n velocity;\n frozenState;\n observed = true;\n observations = new Map();\n estimator;\n constructor(initial, deltaT) {\n this.deltaT = deltaT;\n this.lastObservation = initial;\n this.estimator = new TrackingKalmanEstimator(initial.rect, \"xcycsr\");\n this.configureNoise();\n }\n predict(frameStep, frameRate) {\n if (this.observed && this.timeSinceUpdate > 0) {\n this.frozenState = this.estimator.snapshot();\n this.observed = false;\n }\n this.estimator.predict(frameStep, frameRate);\n if (this.timeSinceUpdate > 0) {\n this.successfulConsecutiveUpdates = 0;\n }\n this.timeSinceUpdate += 1;\n this.age += 1;\n }\n update(detection, frameStep, frameRate) {\n const previous = this.getPreviousObservation();\n if (previous) {\n this.velocity = computeVelocity(previous, detection);\n }\n if (!this.observed && this.frozenState) {\n this.unfreeze(detection, frameStep, frameRate);\n }\n this.estimator.update(detection.rect);\n this.observed = true;\n this.timeSinceUpdate = 0;\n this.successfulConsecutiveUpdates += 1;\n this.lastObservation = detection;\n this.observations.set(this.age, detection);\n const cutoff = this.age - this.deltaT;\n for (const age of this.observations.keys()) {\n if (age < cutoff)\n this.observations.delete(age);\n }\n }\n getRect() {\n return this.estimator.getRect();\n }\n getPreviousObservation() {\n if (this.observations.size === 0)\n return undefined;\n for (let index = 0; index < this.deltaT; index += 1) {\n const delta = this.deltaT - index;\n const observation = this.observations.get(this.age - delta);\n if (observation)\n return observation;\n }\n const latestAge = Math.max(...this.observations.keys());\n return this.observations.get(latestAge);\n }\n configureNoise() {\n const measurementNoise = identity(4);\n for (let index = 2; index < 4; index += 1) {\n measurementNoise[index][index] *= 10;\n }\n const covariance = scaleMatrix(identity(7), 10);\n for (let index = 4; index < 7; index += 1) {\n covariance[index][index] *= 1000;\n }\n const processNoise = identity(7);\n processNoise[6][6] *= 0.01;\n for (let index = 4; index < 7; index += 1) {\n processNoise[index][index] *= 0.01;\n }\n this.estimator.setCovariances({\n covariance,\n measurementNoise,\n processNoise,\n });\n }\n unfreeze(detection, frameStep, frameRate) {\n if (!this.frozenState || this.timeSinceUpdate === 0)\n return;\n this.estimator.restore(this.frozenState);\n const timeGap = this.timeSinceUpdate;\n const start = rectToMeasurement(this.lastObservation.rect, \"xcycsr\");\n const end = rectToMeasurement(detection.rect, \"xcycsr\");\n const startWidth = Math.sqrt(start[2] * start[3]);\n const startHeight = start[3] === 0 ? 0 : Math.sqrt(start[2] / start[3]);\n const endWidth = Math.sqrt(end[2] * end[3]);\n const endHeight = end[3] === 0 ? 0 : Math.sqrt(end[2] / end[3]);\n for (let index = 0; index < timeGap; index += 1) {\n const progress = (index + 1) / timeGap;\n const x = start[0] + progress * (end[0] - start[0]);\n const y = start[1] + progress * (end[1] - start[1]);\n const width = startWidth + progress * (endWidth - startWidth);\n const height = startHeight + progress * (endHeight - startHeight);\n this.estimator.updateMeasurement([x, y, width * height, width / height]);\n if (index < timeGap - 1) {\n this.estimator.predict(frameStep, frameRate);\n }\n }\n this.frozenState = undefined;\n }\n }\n /** Creates the observation-centric SORT implementation from roboflow/trackers. */\n function createOCSortTracker$1(options = {}) {\n const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, \"lostTrackBuffer\");\n const frameRate = options.frameRate ?? 30;\n const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 3, \"minimumConsecutiveFrames\");\n const minimumIouThreshold = options.minimumIouThreshold ?? 0.3;\n const directionConsistencyWeight = options.directionConsistencyWeight ?? 0.2;\n const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ?? 0.6;\n const deltaT = normalizePositiveInteger$1(options.deltaT ?? 3, \"deltaT\");\n if (!Number.isFinite(frameRate) || frameRate <= 0) {\n throw new Error(\"frameRate must be a finite positive value.\");\n }\n normalizeUnitInterval$1(minimumIouThreshold, \"minimumIouThreshold\");\n normalizeUnitInterval$1(directionConsistencyWeight, \"directionConsistencyWeight\");\n normalizeUnitInterval$1(highConfidenceDetectionThreshold, \"highConfidenceDetectionThreshold\");\n const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;\n if (!Number.isFinite(scaledLostTrackBuffer)) {\n throw new Error(\"Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.\");\n }\n const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));\n let tracks = [];\n let frameCount = 0;\n let nextTrackerId = 0;\n let previousFrameIndex;\n return {\n reset() {\n tracks = [];\n frameCount = 0;\n nextTrackerId = 0;\n previousFrameIndex = undefined;\n },\n update(detections, frameIndex) {\n const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);\n previousFrameIndex = frameIndex ?? previousFrameIndex;\n // Match roboflow/trackers: an empty stream before any track exists is\n // not part of OC-SORT's early-sequence activation window.\n if (tracks.length === 0 && detections.length === 0) {\n return {\n activeTrackCount: 0,\n assignments: [],\n confirmedTrackCount: 0,\n };\n }\n tracks.forEach((track) => track.predict(frameStep, frameRate));\n const highDetectionIndexes = detections.flatMap((detection, index) => detection.confidence === undefined ||\n detection.confidence >= highConfidenceDetectionThreshold\n ? [index]\n : []);\n const highDetections = highDetectionIndexes.map((index) => detections[index]);\n const iouScores = pairwiseIou(tracks.map((track) => track.getRect()), highDetections.map((detection) => detection.rect));\n const combinedScores = iouScores.map((row, trackIndex) => row.map((iou, detectionIndex) => {\n if (directionConsistencyWeight === 0)\n return iou;\n return (iou +\n directionConsistencyWeight *\n directionConsistency(tracks[trackIndex], highDetections[detectionIndex]) *\n (highDetections[detectionIndex].confidence ?? 1));\n }));\n const primary = associateTrackingScores(combinedScores, tracks.length, highDetections.length, minimumIouThreshold, iouScores);\n const assignments = new Map();\n for (const match of primary.matches) {\n updateMatchedTrack(tracks[match.trackIndex], highDetections[match.detectionIndex], assignments, frameStep);\n }\n let remainingHigh = [...primary.unmatchedDetectionIndexes];\n if (primary.unmatchedTrackIndexes.length > 0 &&\n remainingHigh.length > 0) {\n const unmatchedTracks = primary.unmatchedTrackIndexes.map((index) => tracks[index]);\n const unmatchedDetections = remainingHigh.map((index) => highDetections[index]);\n const recoveryScores = pairwiseIou(unmatchedTracks.map((track) => track.lastObservation.rect), unmatchedDetections.map((detection) => detection.rect));\n const recovery = associateTrackingScores(recoveryScores, unmatchedTracks.length, unmatchedDetections.length, minimumIouThreshold);\n for (const match of recovery.matches) {\n updateMatchedTrack(unmatchedTracks[match.trackIndex], unmatchedDetections[match.detectionIndex], assignments, frameStep);\n }\n remainingHigh = recovery.unmatchedDetectionIndexes.map((index) => remainingHigh[index]);\n }\n for (const localIndex of remainingHigh) {\n tracks.push(new OCSortTrack(highDetections[localIndex], deltaT));\n }\n tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate);\n frameCount += 1;\n const resolvedAssignments = detections.flatMap((detection) => {\n const trackerId = assignments.get(detection.detectionIndex);\n return trackerId === undefined\n ? []\n : [{ detectionIndex: detection.detectionIndex, trackerId }];\n });\n return {\n activeTrackCount: tracks.length,\n assignments: resolvedAssignments,\n confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined).length,\n };\n },\n };\n function updateMatchedTrack(track, detection, assignments, frameStep) {\n track.update(detection, frameStep, frameRate);\n const earlySequence = frameCount <= minimumConsecutiveFrames;\n const shouldEmit = (earlySequence && track.timeSinceUpdate === 0) ||\n track.successfulConsecutiveUpdates >= minimumConsecutiveFrames;\n if (shouldEmit) {\n if (track.trackerId === undefined) {\n track.trackerId = nextTrackerId++;\n }\n assignments.set(detection.detectionIndex, track.trackerId);\n }\n }\n }\n function computeVelocity(previous, current) {\n const deltaX = current.rect.x - previous.rect.x;\n const deltaY = current.rect.y - previous.rect.y;\n const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY) + 1e-6;\n return { x: deltaX / norm, y: deltaY / norm };\n }\n function directionConsistency(track, detection) {\n if (!track.velocity)\n return 0;\n const reference = track.getPreviousObservation() ?? track.lastObservation;\n const deltaX = detection.rect.x - reference.rect.x;\n const deltaY = detection.rect.y - reference.rect.y;\n const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY) + 1e-6;\n const cosine = Math.max(-1, Math.min(1, track.velocity.x * (deltaX / norm) + track.velocity.y * (deltaY / norm)));\n const angle = Math.acos(cosine);\n return (Math.PI / 2 - Math.abs(angle)) / Math.PI;\n }\n\n /** Public core facade for the internal tracker engine workspace. */\n function createSortTracker(options = {}) {\n return createSortTracker$1(options);\n }\n\n /** Public core facade for the internal tracker engine workspace. */\n function createByteTrackTracker(options = {}) {\n return createByteTrackTracker$1(options);\n }\n\n /** Public core facade for the internal tracker engine workspace. */\n function createCBIoUTracker(options = {}) {\n return createCBIoUTracker$1(options);\n }\n\n /** Public core facade for the internal tracker engine workspace. */\n function createOCSortTracker(options = {}) {\n return createOCSortTracker$1(options);\n }\n\n /** Geometry projected into a tracker. The original detection payload is retained. */\n var TrackingGeometry;\n (function (TrackingGeometry) {\n TrackingGeometry[\"Box\"] = \"box\";\n TrackingGeometry[\"Mask\"] = \"mask\";\n TrackingGeometry[\"Keypoints\"] = \"keypoints\";\n })(TrackingGeometry || (TrackingGeometry = {}));\n\n var DetectionMaskPayloadFormat;\n (function (DetectionMaskPayloadFormat) {\n DetectionMaskPayloadFormat[\"RawCocoRle\"] = \"rawCocoRle\";\n DetectionMaskPayloadFormat[\"DeflatedBase64\"] = \"deflatedBase64\";\n })(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));\n\n var DetectionPickTarget;\n (function (DetectionPickTarget) {\n DetectionPickTarget[\"Box\"] = \"box\";\n DetectionPickTarget[\"Edge\"] = \"edge\";\n DetectionPickTarget[\"Keypoint\"] = \"keypoint\";\n DetectionPickTarget[\"Label\"] = \"label\";\n DetectionPickTarget[\"Mask\"] = \"mask\";\n DetectionPickTarget[\"Polygon\"] = \"polygon\";\n DetectionPickTarget[\"Polyline\"] = \"polyline\";\n })(DetectionPickTarget || (DetectionPickTarget = {}));\n var MediaInteractionMode;\n (function (MediaInteractionMode) {\n MediaInteractionMode[\"Always\"] = \"always\";\n MediaInteractionMode[\"Disabled\"] = \"disabled\";\n MediaInteractionMode[\"PausedOnly\"] = \"pausedOnly\";\n })(MediaInteractionMode || (MediaInteractionMode = {}));\n\n var AnnotationGeometryKind;\n (function (AnnotationGeometryKind) {\n AnnotationGeometryKind[\"Box\"] = \"box\";\n AnnotationGeometryKind[\"Polygon\"] = \"polygon\";\n AnnotationGeometryKind[\"Polyline\"] = \"polyline\";\n AnnotationGeometryKind[\"Keypoints\"] = \"keypoints\";\n AnnotationGeometryKind[\"Mask\"] = \"mask\";\n })(AnnotationGeometryKind || (AnnotationGeometryKind = {}));\n var AnnotationGestureStateKind;\n (function (AnnotationGestureStateKind) {\n AnnotationGestureStateKind[\"Idle\"] = \"idle\";\n AnnotationGestureStateKind[\"Creating\"] = \"creating\";\n AnnotationGestureStateKind[\"Moving\"] = \"moving\";\n AnnotationGestureStateKind[\"Resizing\"] = \"resizing\";\n AnnotationGestureStateKind[\"DragSelecting\"] = \"dragSelecting\";\n })(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));\n var AnnotationHandleKind;\n (function (AnnotationHandleKind) {\n AnnotationHandleKind[\"Resize\"] = \"resize\";\n AnnotationHandleKind[\"Vertex\"] = \"vertex\";\n AnnotationHandleKind[\"AddVertex\"] = \"addVertex\";\n AnnotationHandleKind[\"Keypoint\"] = \"keypoint\";\n })(AnnotationHandleKind || (AnnotationHandleKind = {}));\n\n /** Alignment for strokes applied to closed rendered geometry. */\n var StrokeAlignment;\n (function (StrokeAlignment) {\n StrokeAlignment[\"Inside\"] = \"inside\";\n StrokeAlignment[\"Center\"] = \"center\";\n StrokeAlignment[\"Outside\"] = \"outside\";\n })(StrokeAlignment || (StrokeAlignment = {}));\n\n var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n\n var FocusTargetMode;\n (function (FocusTargetMode) {\n FocusTargetMode[\"Hovered\"] = \"hovered\";\n FocusTargetMode[\"Selected\"] = \"selected\";\n FocusTargetMode[\"HoveredAndSelected\"] = \"hoveredAndSelected\";\n FocusTargetMode[\"Ambient\"] = \"ambient\";\n })(FocusTargetMode || (FocusTargetMode = {}));\n\n var DetectionInteractionState;\n (function (DetectionInteractionState) {\n DetectionInteractionState[\"Hovered\"] = \"hovered\";\n DetectionInteractionState[\"Selected\"] = \"selected\";\n })(DetectionInteractionState || (DetectionInteractionState = {}));\n\n var LabelPlacement;\n (function (LabelPlacement) {\n LabelPlacement[\"Top\"] = \"top\";\n LabelPlacement[\"Bottom\"] = \"bottom\";\n LabelPlacement[\"InsideTop\"] = \"insideTop\";\n LabelPlacement[\"InsideBottom\"] = \"insideBottom\";\n LabelPlacement[\"Center\"] = \"center\";\n })(LabelPlacement || (LabelPlacement = {}));\n var LabelVisibilityMode;\n (function (LabelVisibilityMode) {\n LabelVisibilityMode[\"Always\"] = \"always\";\n LabelVisibilityMode[\"HoveredOnly\"] = \"hoveredOnly\";\n })(LabelVisibilityMode || (LabelVisibilityMode = {}));\n\n var MaskRenderMode;\n (function (MaskRenderMode) {\n MaskRenderMode[\"FillAndStroke\"] = \"fillAndStroke\";\n MaskRenderMode[\"FillOnly\"] = \"fillOnly\";\n MaskRenderMode[\"StrokeOnly\"] = \"strokeOnly\";\n })(MaskRenderMode || (MaskRenderMode = {}));\n\n /**\n * Discriminator for renderer-neutral shape draw instructions.\n */\n var ShapeInstructionKind;\n (function (ShapeInstructionKind) {\n ShapeInstructionKind[\"Ellipse\"] = \"ellipse\";\n ShapeInstructionKind[\"Marker\"] = \"marker\";\n ShapeInstructionKind[\"Path\"] = \"path\";\n })(ShapeInstructionKind || (ShapeInstructionKind = {}));\n var MarkerShape;\n (function (MarkerShape) {\n MarkerShape[\"Circle\"] = \"circle\";\n MarkerShape[\"Cross\"] = \"cross\";\n MarkerShape[\"Square\"] = \"square\";\n MarkerShape[\"Triangle\"] = \"triangle\";\n })(MarkerShape || (MarkerShape = {}));\n /**\n * Coordinate space for marker sizing. Media sizes scale with the viewport;\n * screen sizes stay constant on screen, matching stroke-width semantics.\n */\n var MarkerSizeSpace;\n (function (MarkerSizeSpace) {\n MarkerSizeSpace[\"Media\"] = \"media\";\n MarkerSizeSpace[\"Screen\"] = \"screen\";\n })(MarkerSizeSpace || (MarkerSizeSpace = {}));\n\n var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\n\n var MediaRendererFit;\n (function (MediaRendererFit) {\n /**\n * Preserve media aspect ratio and fit the full frame inside the canvas.\n */\n MediaRendererFit[\"Contain\"] = \"contain\";\n /**\n * Preserve media aspect ratio and fill the canvas, cropping if necessary.\n */\n MediaRendererFit[\"Cover\"] = \"cover\";\n })(MediaRendererFit || (MediaRendererFit = {}));\n /**\n * Playback lifecycle state reported by a platform renderer.\n */\n var MediaRendererPlaybackState;\n (function (MediaRendererPlaybackState) {\n MediaRendererPlaybackState[\"Loading\"] = \"loading\";\n MediaRendererPlaybackState[\"Ready\"] = \"ready\";\n MediaRendererPlaybackState[\"Playing\"] = \"playing\";\n MediaRendererPlaybackState[\"Buffering\"] = \"buffering\";\n MediaRendererPlaybackState[\"Paused\"] = \"paused\";\n MediaRendererPlaybackState[\"Error\"] = \"error\";\n MediaRendererPlaybackState[\"Destroyed\"] = \"destroyed\";\n })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));\n /**\n * Stable classification of a media failure.\n *\n * Applications branch on these kinds instead of parsing decoder, demuxer, or\n * container error text, and still own their localized user-facing copy. New\n * kinds may be added over time, so treat unrecognized values like `Unknown`.\n */\n var MediaErrorKind;\n (function (MediaErrorKind) {\n /** The media could not be opened or read at all. */\n MediaErrorKind[\"Unreadable\"] = \"unreadable\";\n /** The container or codec is not supported by this platform. */\n MediaErrorKind[\"UnsupportedFormat\"] = \"unsupportedFormat\";\n /** The media opened but carries no usable video track. */\n MediaErrorKind[\"NoVideoTrack\"] = \"noVideoTrack\";\n /** Decoding a sample failed after the source opened. */\n MediaErrorKind[\"Decode\"] = \"decode\";\n /** The media could not be fetched. */\n MediaErrorKind[\"Network\"] = \"network\";\n /** The host environment lacks the APIs this media source requires. */\n MediaErrorKind[\"EnvironmentUnsupported\"] = \"environmentUnsupported\";\n /** The failure could not be classified. */\n MediaErrorKind[\"Unknown\"] = \"unknown\";\n })(MediaErrorKind || (MediaErrorKind = {}));\n /**\n * Lower-level media source readiness.\n */\n var MediaSourceStatus;\n (function (MediaSourceStatus) {\n MediaSourceStatus[\"Loading\"] = \"loading\";\n MediaSourceStatus[\"Ready\"] = \"ready\";\n MediaSourceStatus[\"Error\"] = \"error\";\n MediaSourceStatus[\"Destroyed\"] = \"destroyed\";\n })(MediaSourceStatus || (MediaSourceStatus = {}));\n /**\n * Current renderer state.\n */\n /**\n * What a playback gate holds back.\n *\n * A source the renderer pulls samples from can be held between any two frames,\n * because the renderer decides when each one is drawn. A source that presents\n * its own frames owns the playhead, so holding it means stopping the producer\n * and starting it again, which the detection gate does and the\n * render-preparation gate does not.\n */\n var PlaybackGateReach;\n (function (PlaybackGateReach) {\n /** No gate: the picture moves and unprepared layers are absent from it. */\n PlaybackGateReach[\"Off\"] = \"off\";\n /**\n * Playback waits to begin, and stops again at any frame whose artifacts are\n * missing, until they arrive or the gate's own wait bound gives up on them.\n */\n PlaybackGateReach[\"EveryFrame\"] = \"everyFrame\";\n /** Playback waits to begin; frames after that are not held. */\n PlaybackGateReach[\"StartOfPlayback\"] = \"startOfPlayback\";\n })(PlaybackGateReach || (PlaybackGateReach = {}));\n\n /**\n * Media-session operating mode.\n *\n * Core owns the names because file-like and stream-like lifecycle choices are\n * platform-neutral. Platform packages decide how these modes tune media,\n * storage, and renderer defaults.\n */\n var MediaSessionMode;\n (function (MediaSessionMode) {\n /**\n * Finite media. Defaults usually favor seek/replay and persistent detection\n * storage.\n */\n MediaSessionMode[\"File\"] = \"file\";\n /**\n * Live or append-only media. Defaults usually favor rolling windows and\n * bounded retention.\n */\n MediaSessionMode[\"Stream\"] = \"stream\";\n })(MediaSessionMode || (MediaSessionMode = {}));\n /**\n * Aggregate lifecycle state for a media session.\n */\n var MediaSessionStatus;\n (function (MediaSessionStatus) {\n MediaSessionStatus[\"Buffering\"] = \"buffering\";\n MediaSessionStatus[\"Destroyed\"] = \"destroyed\";\n MediaSessionStatus[\"Error\"] = \"error\";\n MediaSessionStatus[\"Loading\"] = \"loading\";\n MediaSessionStatus[\"Paused\"] = \"paused\";\n MediaSessionStatus[\"Playing\"] = \"playing\";\n MediaSessionStatus[\"Processing\"] = \"processing\";\n MediaSessionStatus[\"Ready\"] = \"ready\";\n })(MediaSessionStatus || (MediaSessionStatus = {}));\n /**\n * Subsystem currently affecting session readiness or presentation.\n */\n var MediaSessionActivityKind;\n (function (MediaSessionActivityKind) {\n /**\n * Playback is held waiting for a producer to emit detections for the frame\n * about to be shown, which is a wait on inference rather than on transfer or\n * decode. `DetectionsBuffering` is the transfer of detections that already\n * exist, and `PlaybackBuffering` is the wait for media bytes.\n */\n MediaSessionActivityKind[\"DetectionsAwaitingCoverage\"] = \"detectionsAwaitingCoverage\";\n MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\n MediaSessionActivityKind[\"RenderPreparing\"] = \"renderPreparing\";\n })(MediaSessionActivityKind || (MediaSessionActivityKind = {}));\n /**\n * State of one session activity.\n */\n var MediaSessionActivityStatus;\n (function (MediaSessionActivityStatus) {\n MediaSessionActivityStatus[\"Error\"] = \"error\";\n MediaSessionActivityStatus[\"Running\"] = \"running\";\n MediaSessionActivityStatus[\"Waiting\"] = \"waiting\";\n })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));\n\n let tracker;\n self.addEventListener(\"message\", (event) => {\n const request = event.data;\n try {\n let response;\n if (request.type === \"configure\") {\n switch (request.processor.algorithm) {\n case \"bytetrack\":\n tracker = createByteTrackTracker(request.processor.options);\n break;\n case \"cbiou\":\n tracker = createCBIoUTracker(request.processor.options);\n break;\n case \"ocsort\":\n tracker = createOCSortTracker(request.processor.options);\n break;\n default:\n tracker = createSortTracker(request.processor.options);\n }\n response = { requestId: request.requestId, type: \"success\" };\n }\n else if (request.type === \"reset\") {\n tracker?.reset();\n response = { requestId: request.requestId, type: \"success\" };\n }\n else {\n if (!tracker) {\n throw new Error(\"Tracking worker is not configured.\");\n }\n const startedAt = performance.now();\n const update = tracker.update(request.detections, request.frameIndex);\n response = {\n ...update,\n durationMs: performance.now() - startedAt,\n requestId: request.requestId,\n type: \"success\",\n };\n }\n self.postMessage(response);\n }\n catch (error) {\n self.postMessage({\n message: error instanceof Error ? error.message : \"Tracking worker failed.\",\n requestId: request.requestId,\n type: \"error\",\n });\n }\n });\n\n})();";
500
+ const EMBEDDED_TRACKING_WORKER_SOURCE = "(function () {\n 'use strict';\n\n /** COCO-compatible keypoint visibility values. */\n var KeypointVisibility;\n (function (KeypointVisibility) {\n KeypointVisibility[KeypointVisibility[\"NotLabeled\"] = 0] = \"NotLabeled\";\n KeypointVisibility[KeypointVisibility[\"Occluded\"] = 1] = \"Occluded\";\n KeypointVisibility[KeypointVisibility[\"Visible\"] = 2] = \"Visible\";\n })(KeypointVisibility || (KeypointVisibility = {}));\n var DetectionMaskEncoding;\n (function (DetectionMaskEncoding) {\n DetectionMaskEncoding[\"CompressedRle\"] = \"compressedRle\";\n })(DetectionMaskEncoding || (DetectionMaskEncoding = {}));\n\n var DetectionBufferStatus;\n (function (DetectionBufferStatus) {\n DetectionBufferStatus[\"Idle\"] = \"idle\";\n DetectionBufferStatus[\"Loading\"] = \"loading\";\n /**\n * The playback gate is holding playback until the source produces detections\n * for the requested range. {@link DetectionBufferStatus.Loading} fetches\n * detections the source already has; this one waits on a producer.\n */\n DetectionBufferStatus[\"AwaitingCoverage\"] = \"awaitingCoverage\";\n DetectionBufferStatus[\"Ready\"] = \"ready\";\n DetectionBufferStatus[\"Error\"] = \"error\";\n DetectionBufferStatus[\"Destroyed\"] = \"destroyed\";\n })(DetectionBufferStatus || (DetectionBufferStatus = {}));\n /**\n * How the renderer selects the active detection frame for a media timestamp.\n */\n var DetectionFrameSelectionMode;\n (function (DetectionFrameSelectionMode) {\n /**\n * Select the frame whose `[mediaTime, endTime)` interval contains the media\n * time. This is the default for interval annotations and timestamped sources.\n *\n * Frame times must come from the media itself. Times reconstructed from a\n * nominal frame rate drift against a clip whose real rate differs, and once\n * that drift passes the half-millisecond selection tolerance a playhead\n * landing on a frame boundary selects the previous frame's detections.\n *\n * A frame that carries no `endTime` stays active until the next frame starts,\n * which bridges an index the source never produced.\n * {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to\n * one grid step instead, so a missing index reads as no detections.\n */\n DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex` and\n * `frameRate`. This is useful when inference was run on normalized frames and\n * playback should snap detections to that grid.\n *\n * A frame speaks for one grid step starting at its own media time, and an\n * index the source never produced selects nothing rather than a neighbour.\n *\n * A frame carrying no `frameIndex` speaks for its step on the same terms,\n * reached by the time it starts at rather than by an index that names it. Its\n * `endTime` does not widen it past that step, so a source that labels only\n * part of what it writes cannot bridge the indexes it never produced.\n */\n DetectionFrameSelectionMode[\"NearestFrameIndex\"] = \"nearestFrameIndex\";\n })(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));\n var DetectionFrameRetentionMode;\n (function (DetectionFrameRetentionMode) {\n /**\n * Keep writable detections only in an in-memory store. Useful for ephemeral\n * live streams where old predictions should disappear when evicted.\n */\n DetectionFrameRetentionMode[\"MemoryOnly\"] = \"memoryOnly\";\n /**\n * Persist every written detection frame. Useful for finite media where seek\n * and replay should not require recomputing detections.\n */\n DetectionFrameRetentionMode[\"PersistAll\"] = \"persistAll\";\n /**\n * Persist only the most recent retention window. Useful for long-running\n * streams where replay is bounded to a recent time horizon.\n */\n DetectionFrameRetentionMode[\"PersistWindow\"] = \"persistWindow\";\n })(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));\n\n var AnnotationFrameMutationKind;\n (function (AnnotationFrameMutationKind) {\n AnnotationFrameMutationKind[\"Add\"] = \"add\";\n AnnotationFrameMutationKind[\"Remove\"] = \"remove\";\n AnnotationFrameMutationKind[\"Replace\"] = \"replace\";\n AnnotationFrameMutationKind[\"Transact\"] = \"transact\";\n AnnotationFrameMutationKind[\"Update\"] = \"update\";\n })(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));\n\n /**\n * Creates one stateful SORT tracker for a single ordered media sequence.\n *\n * Defaults, lifecycle semantics, and observation-only output mirror\n * roboflow/trackers SORT. Motion predictions remain internal to association.\n */\n function createSortTracker$1(options = {}) {\n const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, \"lostTrackBuffer\");\n const frameRate = options.frameRate ?? 30;\n const trackActivationThreshold = options.trackActivationThreshold ?? 0.25;\n const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 3, \"minimumConsecutiveFrames\");\n const minimumIouThreshold = options.minimumIouThreshold ?? 0.3;\n if (!Number.isFinite(frameRate) || frameRate <= 0) {\n throw new Error(\"frameRate must be a finite positive value.\");\n }\n normalizeUnitInterval$1(trackActivationThreshold, \"trackActivationThreshold\");\n normalizeUnitInterval$1(minimumIouThreshold, \"minimumIouThreshold\");\n const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;\n if (!Number.isFinite(scaledLostTrackBuffer)) {\n throw new Error(\"Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.\");\n }\n const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));\n let tracks = [];\n let nextTrackerId = 0;\n let previousFrameIndex;\n return {\n reset() {\n tracks = [];\n nextTrackerId = 0;\n previousFrameIndex = undefined;\n },\n update(detections, frameIndex) {\n const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);\n previousFrameIndex = frameIndex ?? previousFrameIndex;\n const predicted = tracks.map((track) => track.predict(frameStep, frameRate));\n const { matches, unmatchedDetections } = associateDetectionsToTracks(detections, predicted, minimumIouThreshold);\n const trackerIds = new Map();\n for (const match of matches) {\n const track = tracks[match.trackIndex];\n const detection = detections[match.detectionIndex];\n track.update(detection);\n if (track.trackerId === undefined &&\n track.successfulUpdates >= minimumConsecutiveFrames) {\n track.trackerId = nextTrackerId;\n nextTrackerId += 1;\n }\n if (track.trackerId !== undefined) {\n trackerIds.set(detection.detectionIndex, track.trackerId);\n }\n }\n for (const detectionIndex of unmatchedDetections) {\n const detection = detections[detectionIndex];\n if ((detection.confidence ?? 1) >= trackActivationThreshold) {\n tracks.push(new KalmanBoxTrack(detection));\n }\n }\n tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate &&\n (track.successfulUpdates >= minimumConsecutiveFrames ||\n track.timeSinceUpdate === 0));\n return {\n activeTrackCount: tracks.length,\n assignments: detections.flatMap((detection) => {\n const trackerId = trackerIds.get(detection.detectionIndex);\n return trackerId === undefined\n ? []\n : [{ detectionIndex: detection.detectionIndex, trackerId }];\n }),\n confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined).length,\n };\n },\n };\n }\n class KalmanBoxTrack {\n consecutiveUpdates;\n trackerId;\n successfulUpdates = 1;\n timeSinceUpdate = 0;\n state;\n covariance = identity(8);\n constructor(detection, consecutiveUpdates = false) {\n this.consecutiveUpdates = consecutiveUpdates;\n this.state = [...rectToXyxy(detection.rect), 0, 0, 0, 0].map((value) => [\n value,\n ]);\n }\n predict(frameStep, frameRate) {\n if (this.consecutiveUpdates && this.timeSinceUpdate > 0) {\n this.successfulUpdates = 0;\n }\n const transition = identity(8);\n for (let index = 0; index < 4; index += 1) {\n transition[index][index + 4] = frameStep;\n }\n this.state = multiply(transition, this.state);\n this.covariance = add(multiply(multiply(transition, this.covariance), transpose(transition)), createProcessNoise(frameStep, frameRate));\n // Python SORT counts update calls for the fixed-rate lost-track budget.\n this.timeSinceUpdate += 1;\n return stateToRect(this.state);\n }\n update(detection) {\n const measurement = rectToXyxy(detection.rect).map((value) => [value]);\n const observation = [\n [1, 0, 0, 0, 0, 0, 0, 0],\n [0, 1, 0, 0, 0, 0, 0, 0],\n [0, 0, 1, 0, 0, 0, 0, 0],\n [0, 0, 0, 1, 0, 0, 0, 0],\n ];\n const measurementNoise = scale(identity(4), 0.1);\n const innovation = subtract(measurement, multiply(observation, this.state));\n const covarianceObservationTranspose = multiply(this.covariance, transpose(observation));\n const innovationCovariance = add(multiply(observation, covarianceObservationTranspose), measurementNoise);\n const gain = multiply(covarianceObservationTranspose, inverse(innovationCovariance));\n this.state = add(this.state, multiply(gain, innovation));\n const identityMinusGainObservation = subtract(identity(8), multiply(gain, observation));\n // Joseph form matches the Python implementation and is more stable.\n this.covariance = add(multiply(multiply(identityMinusGainObservation, this.covariance), transpose(identityMinusGainObservation)), multiply(multiply(gain, measurementNoise), transpose(gain)));\n this.timeSinceUpdate = 0;\n this.successfulUpdates += 1;\n }\n getStateRect() {\n return stateToRect(this.state);\n }\n }\n function associateDetectionsToTracks(detections, predicted, minimumIouThreshold) {\n if (predicted.length === 0 || detections.length === 0) {\n return {\n matches: [],\n unmatchedDetections: detections.map((_, index) => index),\n };\n }\n // Python SORT associates all detections class-agnostically using standard IoU.\n const scores = predicted.map((rect) => detections.map((detection) => intersectionOverUnion(rect, detection.rect)));\n const candidateMatches = maximizeAssignment(scores);\n const matches = [];\n const matchedDetections = new Set();\n for (const [trackIndex, detectionIndex] of candidateMatches) {\n if (scores[trackIndex][detectionIndex] < minimumIouThreshold)\n continue;\n matches.push({ detectionIndex, trackIndex });\n matchedDetections.add(detectionIndex);\n }\n matches.sort((left, right) => left.trackIndex - right.trackIndex);\n return {\n matches,\n unmatchedDetections: detections.flatMap((_, index) => matchedDetections.has(index) ? [] : [index]),\n };\n }\n /** Hungarian assignment for a rectangular score matrix. */\n function maximizeAssignment(scores) {\n if (scores.length === 0 || scores[0]?.length === 0) {\n return [];\n }\n const rowCount = scores.length;\n const columnCount = scores[0].length;\n const transposed = rowCount > columnCount;\n const costs = transposed\n ? Array.from({ length: columnCount }, (_, row) => Array.from({ length: rowCount }, (_, column) => 1 - scores[column][row]))\n : scores.map((row) => row.map((score) => 1 - score));\n const rows = costs.length;\n const columns = costs[0].length;\n const u = new Array(rows + 1).fill(0);\n const v = new Array(columns + 1).fill(0);\n const p = new Array(columns + 1).fill(0);\n const way = new Array(columns + 1).fill(0);\n for (let row = 1; row <= rows; row += 1) {\n p[0] = row;\n let column0 = 0;\n const minValue = new Array(columns + 1).fill(Number.POSITIVE_INFINITY);\n const used = new Array(columns + 1).fill(false);\n do {\n used[column0] = true;\n const row0 = p[column0];\n let delta = Number.POSITIVE_INFINITY;\n let column1 = 0;\n for (let column = 1; column <= columns; column += 1) {\n if (used[column])\n continue;\n const current = costs[row0 - 1][column - 1] - u[row0] - v[column];\n if (current < minValue[column]) {\n minValue[column] = current;\n way[column] = column0;\n }\n if (minValue[column] < delta) {\n delta = minValue[column];\n column1 = column;\n }\n }\n for (let column = 0; column <= columns; column += 1) {\n if (used[column]) {\n u[p[column]] += delta;\n v[column] -= delta;\n }\n else {\n minValue[column] -= delta;\n }\n }\n column0 = column1;\n } while (p[column0] !== 0);\n do {\n const column1 = way[column0];\n p[column0] = p[column1];\n column0 = column1;\n } while (column0 !== 0);\n }\n const assignments = [];\n for (let column = 1; column <= columns; column += 1) {\n if (p[column] === 0)\n continue;\n const row = p[column] - 1;\n assignments.push(transposed ? [column - 1, row] : [row, column - 1]);\n }\n return assignments;\n }\n function rectToXyxy(rect) {\n return [\n rect.x - rect.width / 2,\n rect.y - rect.height / 2,\n rect.x + rect.width / 2,\n rect.y + rect.height / 2,\n ];\n }\n function stateToRect(state) {\n // The Python XYXY estimator intentionally leaves corner velocities\n // unconstrained. Normalize only at the browser Rect boundary so a crossing\n // prediction cannot violate the positive-width/height storage contract.\n const x1 = Math.min(state[0][0], state[2][0]);\n const y1 = Math.min(state[1][0], state[3][0]);\n const x2 = Math.max(state[0][0], state[2][0]);\n const y2 = Math.max(state[1][0], state[3][0]);\n return {\n height: Math.max(Number.EPSILON, y2 - y1),\n width: Math.max(Number.EPSILON, x2 - x1),\n x: (x1 + x2) / 2,\n y: (y1 + y2) / 2,\n };\n }\n function createProcessNoise(frameStep, frameRate) {\n if (Math.abs(frameStep - 1) <= 0.004 * frameRate) {\n return scale(identity(8), 0.01);\n }\n const result = Array.from({ length: 8 }, () => new Array(8).fill(0));\n const dt2 = frameStep * frameStep;\n const dt3 = dt2 * frameStep;\n const dt4 = dt2 * dt2;\n for (let index = 0; index < 4; index += 1) {\n const velocityIndex = index + 4;\n result[index][index] = (0.01 * dt4) / 4;\n result[index][velocityIndex] = (0.01 * dt3) / 2;\n result[velocityIndex][index] = (0.01 * dt3) / 2;\n result[velocityIndex][velocityIndex] = 0.01 * dt2;\n }\n return result;\n }\n function intersectionOverUnion(left, right) {\n const leftX = left.x - left.width / 2;\n const leftY = left.y - left.height / 2;\n const rightX = right.x - right.width / 2;\n const rightY = right.y - right.height / 2;\n const intersectionWidth = Math.max(0, Math.min(leftX + left.width, rightX + right.width) -\n Math.max(leftX, rightX));\n const intersectionHeight = Math.max(0, Math.min(leftY + left.height, rightY + right.height) -\n Math.max(leftY, rightY));\n const intersection = intersectionWidth * intersectionHeight;\n const union = left.width * left.height + right.width * right.height - intersection;\n return union <= 0 ? 0 : intersection / union;\n }\n function resolveFrameStep(current, previous) {\n if (current === undefined || previous === undefined)\n return 1;\n return Math.max(1, current - previous);\n }\n function normalizePositiveInteger$1(value, label) {\n if (!Number.isInteger(value) || value < 1) {\n throw new Error(`${label} must be a positive integer.`);\n }\n return value;\n }\n function normalizeNonNegativeInteger$1(value, label) {\n if (!Number.isInteger(value) || value < 0) {\n throw new Error(`${label} must be a non-negative integer.`);\n }\n return value;\n }\n function normalizeUnitInterval$1(value, label) {\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n throw new Error(`${label} must be between 0 and 1.`);\n }\n }\n function identity(size) {\n return Array.from({ length: size }, (_, row) => Array.from({ length: size }, (_, column) => (row === column ? 1 : 0)));\n }\n function scale(matrix, factor) {\n return matrix.map((row) => row.map((value) => value * factor));\n }\n function transpose(matrix) {\n return matrix[0].map((_, column) => matrix.map((row) => row[column]));\n }\n function add(left, right) {\n return left.map((row, rowIndex) => row.map((value, columnIndex) => value + right[rowIndex][columnIndex]));\n }\n function subtract(left, right) {\n return left.map((row, rowIndex) => row.map((value, columnIndex) => value - right[rowIndex][columnIndex]));\n }\n function multiply(left, right) {\n return left.map((row) => right[0].map((_, column) => row.reduce((sum, value, index) => sum + value * right[index][column], 0)));\n }\n function inverse(matrix) {\n const size = matrix.length;\n const augmented = matrix.map((row, index) => [\n ...row,\n ...identity(size)[index],\n ]);\n for (let column = 0; column < size; column += 1) {\n let pivot = column;\n for (let row = column + 1; row < size; row += 1) {\n if (Math.abs(augmented[row][column]) >\n Math.abs(augmented[pivot][column])) {\n pivot = row;\n }\n }\n if (Math.abs(augmented[pivot][column]) < 1e-12) {\n throw new Error(\"SORT Kalman covariance is singular.\");\n }\n [augmented[column], augmented[pivot]] = [\n augmented[pivot],\n augmented[column],\n ];\n const divisor = augmented[column][column];\n augmented[column] = augmented[column].map((value) => value / divisor);\n for (let row = 0; row < size; row += 1) {\n if (row === column)\n continue;\n const factor = augmented[row][column];\n augmented[row] = augmented[row].map((value, index) => value - factor * augmented[column][index]);\n }\n }\n return augmented.map((row) => row.slice(size));\n }\n\n /**\n * Creates one stateful ByteTrack tracker for a single ordered media sequence.\n *\n * Defaults and two-stage association mirror roboflow/trackers ByteTrack at\n * source commit 60b21c8a48676784085fbee455559f16b75a7c9a.\n * Motion predictions remain internal; only observed detections receive IDs.\n */\n function createByteTrackTracker$1(options = {}) {\n const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, \"lostTrackBuffer\");\n const frameRate = options.frameRate ?? 30;\n const trackActivationThreshold = options.trackActivationThreshold ?? 0.7;\n const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 2, \"minimumConsecutiveFrames\");\n const minimumIouThreshold = options.minimumIouThreshold ?? 0.1;\n const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ?? 0.6;\n if (!Number.isFinite(frameRate) || frameRate <= 0) {\n throw new Error(\"frameRate must be a finite positive value.\");\n }\n normalizeUnitInterval$1(trackActivationThreshold, \"trackActivationThreshold\");\n normalizeUnitInterval$1(minimumIouThreshold, \"minimumIouThreshold\");\n normalizeUnitInterval$1(highConfidenceDetectionThreshold, \"highConfidenceDetectionThreshold\");\n const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;\n if (!Number.isFinite(scaledLostTrackBuffer)) {\n throw new Error(\"Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.\");\n }\n const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));\n let tracks = [];\n let nextTrackerId = 0;\n let previousFrameIndex;\n return {\n reset() {\n tracks = [];\n nextTrackerId = 0;\n previousFrameIndex = undefined;\n },\n update(detections, frameIndex) {\n const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);\n previousFrameIndex = frameIndex ?? previousFrameIndex;\n tracks.forEach((track) => track.predict(frameStep, frameRate));\n const highDetectionIndexes = [];\n const lowDetectionIndexes = [];\n detections.forEach((detection, index) => {\n if ((detection.confidence ?? 1) >= highConfidenceDetectionThreshold) {\n highDetectionIndexes.push(index);\n }\n else {\n lowDetectionIndexes.push(index);\n }\n });\n const assignments = new Map();\n const firstStage = associate(tracks, detections, highDetectionIndexes, minimumIouThreshold);\n for (const match of firstStage.matches) {\n updateMatchedTrack(tracks[match.trackIndex], detections[match.detectionIndex], assignments);\n }\n const remainingTracks = firstStage.unmatchedTrackIndexes.map((index) => tracks[index]);\n const secondStage = associate(remainingTracks, detections, lowDetectionIndexes, minimumIouThreshold);\n for (const match of secondStage.matches) {\n updateMatchedTrack(remainingTracks[match.trackIndex], detections[match.detectionIndex], assignments);\n }\n // Only unmatched high-confidence observations can start a track.\n for (const detectionIndex of firstStage.unmatchedDetectionIndexes) {\n const detection = detections[detectionIndex];\n if ((detection.confidence ?? 1) >= trackActivationThreshold) {\n tracks.push(new KalmanBoxTrack(detection, true));\n }\n }\n // Confirmation is sticky once an ID has been allocated. An unmatched\n // unconfirmed track is discarded immediately, as in Python ByteTrack.\n tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate &&\n (track.trackerId !== undefined ||\n track.successfulUpdates >= minimumConsecutiveFrames ||\n track.timeSinceUpdate === 0));\n return {\n activeTrackCount: tracks.length,\n assignments: detections.flatMap((detection) => {\n const trackerId = assignments.get(detection.detectionIndex);\n return trackerId === undefined\n ? []\n : [{ detectionIndex: detection.detectionIndex, trackerId }];\n }),\n confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined).length,\n };\n },\n };\n function updateMatchedTrack(track, detection, assignments) {\n track.update(detection);\n if (track.trackerId === undefined &&\n track.successfulUpdates >= minimumConsecutiveFrames) {\n track.trackerId = nextTrackerId;\n nextTrackerId += 1;\n }\n if (track.trackerId !== undefined) {\n assignments.set(detection.detectionIndex, track.trackerId);\n }\n }\n }\n function associate(tracks, detections, detectionIndexes, minimumIouThreshold) {\n if (tracks.length === 0 || detectionIndexes.length === 0) {\n return {\n matches: [],\n unmatchedDetectionIndexes: [...detectionIndexes],\n unmatchedTrackIndexes: tracks.map((_, index) => index),\n };\n }\n const scores = tracks.map((track) => {\n const predictedRect = track.getStateRect();\n return detectionIndexes.map((detectionIndex) => intersectionOverUnion(predictedRect, detections[detectionIndex].rect));\n });\n const matches = [];\n const matchedTracks = new Set();\n const matchedDetections = new Set();\n for (const [trackIndex, localDetectionIndex] of maximizeAssignment(scores)) {\n if (scores[trackIndex][localDetectionIndex] < minimumIouThreshold) {\n continue;\n }\n const detectionIndex = detectionIndexes[localDetectionIndex];\n matches.push({ detectionIndex, trackIndex });\n matchedTracks.add(trackIndex);\n matchedDetections.add(detectionIndex);\n }\n matches.sort((left, right) => left.trackIndex - right.trackIndex);\n return {\n matches,\n unmatchedDetectionIndexes: detectionIndexes.filter((index) => !matchedDetections.has(index)),\n unmatchedTrackIndexes: tracks.flatMap((_, index) => matchedTracks.has(index) ? [] : [index]),\n };\n }\n\n function associateTrackingScores(scores, trackCount, detectionCount, minimumScore, acceptanceScores = scores) {\n const matches = [];\n const matchedTracks = new Set();\n const matchedDetections = new Set();\n for (const [trackIndex, detectionIndex] of maximizeAssignment(scores)) {\n if (acceptanceScores[trackIndex][detectionIndex] < minimumScore)\n continue;\n matches.push({ detectionIndex, trackIndex });\n matchedTracks.add(trackIndex);\n matchedDetections.add(detectionIndex);\n }\n matches.sort((left, right) => left.trackIndex - right.trackIndex);\n return {\n matches,\n unmatchedDetectionIndexes: Array.from({ length: detectionCount }, (_, index) => index).filter((index) => !matchedDetections.has(index)),\n unmatchedTrackIndexes: Array.from({ length: trackCount }, (_, index) => index).filter((index) => !matchedTracks.has(index)),\n };\n }\n function pairwiseIou(tracks, detections, bufferRatio = 0) {\n return tracks.map((track) => detections.map((detection) => bufferedIntersectionOverUnion(track, detection, bufferRatio)));\n }\n function bufferedIntersectionOverUnion(left, right, bufferRatio) {\n const leftWidth = left.width * (1 + 2 * bufferRatio);\n const leftHeight = left.height * (1 + 2 * bufferRatio);\n const rightWidth = right.width * (1 + 2 * bufferRatio);\n const rightHeight = right.height * (1 + 2 * bufferRatio);\n const leftX = left.x - leftWidth / 2;\n const leftY = left.y - leftHeight / 2;\n const rightX = right.x - rightWidth / 2;\n const rightY = right.y - rightHeight / 2;\n const intersectionWidth = Math.max(0, Math.min(leftX + leftWidth, rightX + rightWidth) - Math.max(leftX, rightX));\n const intersectionHeight = Math.max(0, Math.min(leftY + leftHeight, rightY + rightHeight) -\n Math.max(leftY, rightY));\n const intersection = intersectionWidth * intersectionHeight;\n const union = leftWidth * leftHeight + rightWidth * rightHeight - intersection;\n return union <= 0 ? 0 : intersection / union;\n }\n\n /**\n * Kalman state estimator shared by the browser C-BIoU and OC-SORT ports.\n * Its layouts, covariance update, and gap-scaled constant-velocity model\n * mirror roboflow/trackers at 60b21c8.\n */\n class TrackingKalmanEstimator {\n representation;\n dimension;\n measurementDimension = 4;\n covariance;\n measurementNoise = identity(4);\n processNoise;\n state;\n baselineProcessNoise;\n positionIndexes;\n velocityIndexes;\n constructor(initialRect, representation) {\n this.representation = representation;\n const measurement = rectToMeasurement(initialRect, representation);\n this.dimension = representation === \"xcycsr\" ? 7 : 8;\n this.positionIndexes =\n representation === \"xcycsr\" ? [0, 1, 2] : [0, 1, 2, 3];\n this.velocityIndexes =\n representation === \"xcycsr\" ? [4, 5, 6] : [4, 5, 6, 7];\n this.state = Array.from({ length: this.dimension }, (_, index) => [\n measurement[index] ?? 0,\n ]);\n this.covariance = identity(this.dimension);\n this.processNoise = identity(this.dimension);\n this.baselineProcessNoise = identity(this.dimension);\n }\n predict(frameStep, frameRate) {\n if (this.representation === \"xcycsr\" &&\n this.state[2][0] + frameStep * this.state[6][0] <= 0) {\n this.state[6][0] = 0;\n }\n const transition = identity(this.dimension);\n this.positionIndexes.forEach((positionIndex, index) => {\n transition[positionIndex][this.velocityIndexes[index]] = frameStep;\n });\n this.processNoise = this.createProcessNoise(frameStep, frameRate);\n this.state = multiply(transition, this.state);\n this.covariance = add(multiply(multiply(transition, this.covariance), transpose(transition)), this.processNoise);\n }\n update(rect) {\n this.updateMeasurement(rectToMeasurement(rect, this.representation));\n }\n updateMeasurement(measurement) {\n const observation = Array.from({ length: 4 }, (_, row) => Array.from({ length: this.dimension }, (_, column) => row === column ? 1 : 0));\n const measurementColumn = measurement.map((value) => [value]);\n const innovation = subtract(measurementColumn, multiply(observation, this.state));\n const covarianceObservationTranspose = multiply(this.covariance, transpose(observation));\n const innovationCovariance = add(multiply(observation, covarianceObservationTranspose), this.measurementNoise);\n const gain = multiply(covarianceObservationTranspose, inverse(innovationCovariance));\n this.state = add(this.state, multiply(gain, innovation));\n const identityMinusGainObservation = subtract(identity(this.dimension), multiply(gain, observation));\n this.covariance = add(multiply(multiply(identityMinusGainObservation, this.covariance), transpose(identityMinusGainObservation)), multiply(multiply(gain, this.measurementNoise), transpose(gain)));\n }\n getRect() {\n return measurementToRect(this.state.slice(0, 4).map((row) => row[0]), this.representation);\n }\n setCovariances(options) {\n if (options.covariance)\n this.covariance = clone(options.covariance);\n if (options.measurementNoise) {\n this.measurementNoise = clone(options.measurementNoise);\n }\n if (options.processNoise) {\n this.processNoise = clone(options.processNoise);\n this.baselineProcessNoise = clone(options.processNoise);\n }\n }\n snapshot() {\n return {\n covariance: clone(this.covariance),\n measurementNoise: clone(this.measurementNoise),\n processNoise: clone(this.processNoise),\n state: clone(this.state),\n };\n }\n restore(snapshot) {\n this.covariance = clone(snapshot.covariance);\n this.measurementNoise = clone(snapshot.measurementNoise);\n this.processNoise = clone(snapshot.processNoise);\n this.state = clone(snapshot.state);\n }\n createProcessNoise(frameStep, frameRate) {\n if (Math.abs(frameStep - 1) <= 0.004 * frameRate) {\n return clone(this.baselineProcessNoise);\n }\n const result = Array.from({ length: this.dimension }, () => new Array(this.dimension).fill(0));\n const dt2 = frameStep * frameStep;\n const dt3 = dt2 * frameStep;\n const dt4 = dt2 * dt2;\n const kinematicIndexes = new Set([\n ...this.positionIndexes,\n ...this.velocityIndexes,\n ]);\n this.positionIndexes.forEach((positionIndex, index) => {\n const velocityIndex = this.velocityIndexes[index];\n const accelerationVariance = this.baselineProcessNoise[velocityIndex][velocityIndex];\n result[positionIndex][positionIndex] = (accelerationVariance * dt4) / 4;\n result[positionIndex][velocityIndex] = (accelerationVariance * dt3) / 2;\n result[velocityIndex][positionIndex] = (accelerationVariance * dt3) / 2;\n result[velocityIndex][velocityIndex] = accelerationVariance * dt2;\n });\n for (let index = 0; index < this.dimension; index += 1) {\n if (!kinematicIndexes.has(index)) {\n result[index][index] = this.baselineProcessNoise[index][index];\n }\n }\n return result;\n }\n }\n function rectToMeasurement(rect, representation) {\n if (representation === \"xcycwh\") {\n return [rect.x, rect.y, rect.width, rect.height];\n }\n return [\n rect.x,\n rect.y,\n rect.width * rect.height,\n rect.width / (rect.height + 1e-6),\n ];\n }\n function measurementToRect(measurement, representation) {\n const [x, y, third, fourth] = measurement;\n if (representation === \"xcycwh\") {\n return {\n height: Math.max(1e-3, fourth),\n width: Math.max(1e-3, third),\n x,\n y,\n };\n }\n const width = Math.sqrt(third * fourth);\n const height = width === 0 ? 0 : third / width;\n return {\n height: Math.max(Number.EPSILON, height),\n width: Math.max(Number.EPSILON, width),\n x,\n y,\n };\n }\n function diagonal(values) {\n return values.map((value, row) => values.map((_, column) => (row === column ? value : 0)));\n }\n function scaleMatrix(matrix, factor) {\n return scale(matrix, factor);\n }\n function clone(matrix) {\n return matrix.map((row) => [...row]);\n }\n\n const MINIMUM_DETECTION_CONFIDENCE = 0.1;\n class CBIoUTrack {\n trackerId;\n successfulUpdates = 1;\n timeSinceUpdate = 0;\n estimator;\n constructor(initial) {\n this.estimator = new TrackingKalmanEstimator(initial.rect, \"xcycwh\");\n this.setInitialNoise(initial.rect.width, initial.rect.height);\n }\n predict(frameStep, frameRate) {\n const current = this.estimator.getRect();\n this.estimator.setCovariances({\n processNoise: this.buildProcessNoise(Math.max(current.width, 1e-3), Math.max(current.height, 1e-3)),\n });\n this.estimator.predict(frameStep, frameRate);\n this.clampState();\n this.timeSinceUpdate += 1;\n }\n update(detection) {\n const current = this.estimator.getRect();\n this.estimator.setCovariances({\n measurementNoise: this.buildMeasurementNoise(Math.max(current.width, 1e-3), Math.max(current.height, 1e-3)),\n });\n this.estimator.update(detection.rect);\n this.clampState();\n this.timeSinceUpdate = 0;\n this.successfulUpdates += 1;\n }\n getRect() {\n return this.estimator.getRect();\n }\n setInitialNoise(width, height) {\n const sigmaPosition = 0.05;\n const sigmaVelocity = 0.00625;\n this.estimator.setCovariances({\n covariance: diagonal([\n (2 * sigmaPosition * width) ** 2,\n (2 * sigmaPosition * height) ** 2,\n (2 * sigmaPosition * width) ** 2,\n (2 * sigmaPosition * height) ** 2,\n (10 * sigmaVelocity * width) ** 2,\n (10 * sigmaVelocity * height) ** 2,\n (10 * sigmaVelocity * width) ** 2,\n (10 * sigmaVelocity * height) ** 2,\n ]),\n measurementNoise: this.buildMeasurementNoise(width, height),\n processNoise: this.buildProcessNoise(width, height),\n });\n }\n buildProcessNoise(width, height) {\n const sigmaPosition = 0.05;\n const sigmaVelocity = 0.00625;\n return diagonal([\n (sigmaPosition * width) ** 2,\n (sigmaPosition * height) ** 2,\n (sigmaPosition * width) ** 2,\n (sigmaPosition * height) ** 2,\n (sigmaVelocity * width) ** 2,\n (sigmaVelocity * height) ** 2,\n (sigmaVelocity * width) ** 2,\n (sigmaVelocity * height) ** 2,\n ]);\n }\n buildMeasurementNoise(width, height) {\n const sigmaMeasurement = 0.05;\n return diagonal([\n (sigmaMeasurement * width) ** 2,\n (sigmaMeasurement * height) ** 2,\n (sigmaMeasurement * width) ** 2,\n (sigmaMeasurement * height) ** 2,\n ]);\n }\n clampState() {\n this.estimator.state[2][0] = Math.max(this.estimator.state[2][0], 1e-3);\n this.estimator.state[3][0] = Math.max(this.estimator.state[3][0], 1e-3);\n }\n }\n /** Creates the detection-only C-BIoU implementation from roboflow/trackers. */\n function createCBIoUTracker$1(options = {}) {\n const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, \"lostTrackBuffer\");\n const frameRate = options.frameRate ?? 30;\n const trackActivationThreshold = options.trackActivationThreshold ?? 0.7;\n const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 2, \"minimumConsecutiveFrames\");\n const minimumIouThresholdFirstAssociation = options.minimumIouThresholdFirstAssociation ?? 0.2;\n const minimumIouThresholdSecondAssociation = options.minimumIouThresholdSecondAssociation ?? 0.5;\n const minimumIouThresholdUnconfirmedAssociation = options.minimumIouThresholdUnconfirmedAssociation ?? 0.3;\n const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ?? 0.6;\n const instantFirstFrameActivation = options.instantFirstFrameActivation ?? true;\n const bufferRatioFirst = options.bufferRatioFirst ?? 0.3;\n const bufferRatioSecond = options.bufferRatioSecond ?? 0.5;\n if (!Number.isFinite(frameRate) || frameRate <= 0) {\n throw new Error(\"frameRate must be a finite positive value.\");\n }\n normalizeUnitInterval$1(trackActivationThreshold, \"trackActivationThreshold\");\n normalizeUnitInterval$1(minimumIouThresholdFirstAssociation, \"minimumIouThresholdFirstAssociation\");\n normalizeUnitInterval$1(minimumIouThresholdSecondAssociation, \"minimumIouThresholdSecondAssociation\");\n normalizeUnitInterval$1(minimumIouThresholdUnconfirmedAssociation, \"minimumIouThresholdUnconfirmedAssociation\");\n normalizeUnitInterval$1(highConfidenceDetectionThreshold, \"highConfidenceDetectionThreshold\");\n if (!Number.isFinite(bufferRatioFirst) || bufferRatioFirst < 0) {\n throw new Error(\"bufferRatioFirst must be a finite non-negative value.\");\n }\n if (!Number.isFinite(bufferRatioSecond) || bufferRatioSecond < 0) {\n throw new Error(\"bufferRatioSecond must be a finite non-negative value.\");\n }\n const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;\n if (!Number.isFinite(scaledLostTrackBuffer)) {\n throw new Error(\"Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.\");\n }\n const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));\n let tracks = [];\n let nextTrackerId = 0;\n let frameId = 0;\n let previousFrameIndex;\n return {\n reset() {\n tracks = [];\n nextTrackerId = 0;\n frameId = 0;\n previousFrameIndex = undefined;\n },\n update(detections, frameIndex) {\n const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);\n previousFrameIndex = frameIndex ?? previousFrameIndex;\n frameId += 1;\n tracks.forEach((track) => track.predict(frameStep, frameRate));\n const highDetectionIndexes = [];\n const lowDetectionIndexes = [];\n detections.forEach((detection, index) => {\n const confidence = detection.confidence ?? 1;\n if (confidence >= highConfidenceDetectionThreshold) {\n highDetectionIndexes.push(index);\n }\n else if (confidence > MINIMUM_DETECTION_CONFIDENCE) {\n lowDetectionIndexes.push(index);\n }\n });\n const confirmed = [];\n const unconfirmed = [];\n const lost = [];\n for (const track of tracks) {\n if (track.timeSinceUpdate > 1)\n lost.push(track);\n else if (track.trackerId !== undefined ||\n track.successfulUpdates >= minimumConsecutiveFrames) {\n confirmed.push(track);\n }\n else\n unconfirmed.push(track);\n }\n const assignments = new Map();\n const pool = [...confirmed, ...lost];\n const highDetections = highDetectionIndexes.map((index) => detections[index]);\n const firstScores = pairwiseIou(pool.map((track) => track.getRect()), highDetections.map((detection) => detection.rect), bufferRatioFirst).map((row) => row.map((score, index) => score * (highDetections[index].confidence ?? 1)));\n const firstAssociation = associateTrackingScores(firstScores, pool.length, highDetections.length, minimumIouThresholdFirstAssociation);\n for (const match of firstAssociation.matches) {\n updateMatchedTrack(pool[match.trackIndex], highDetections[match.detectionIndex], assignments);\n }\n const remainingTracked = firstAssociation.unmatchedTrackIndexes\n .map((index) => pool[index])\n .filter((track) => track.timeSinceUpdate === 1);\n const lowDetections = lowDetectionIndexes.map((index) => detections[index]);\n const secondScores = pairwiseIou(remainingTracked.map((track) => track.getRect()), lowDetections.map((detection) => detection.rect), bufferRatioSecond);\n const secondAssociation = associateTrackingScores(secondScores, remainingTracked.length, lowDetections.length, minimumIouThresholdSecondAssociation);\n for (const match of secondAssociation.matches) {\n updateMatchedTrack(remainingTracked[match.trackIndex], lowDetections[match.detectionIndex], assignments);\n }\n let unmatchedHighLocal = [...firstAssociation.unmatchedDetectionIndexes];\n let unmatchedUnconfirmed = unconfirmed.map((_, index) => index);\n if (unconfirmed.length > 0 && unmatchedHighLocal.length > 0) {\n const remainingHigh = unmatchedHighLocal.map((index) => highDetections[index]);\n const unconfirmedScores = pairwiseIou(unconfirmed.map((track) => track.getRect()), remainingHigh.map((detection) => detection.rect), bufferRatioFirst).map((row) => row.map((score, index) => score * (remainingHigh[index].confidence ?? 1)));\n const unconfirmedAssociation = associateTrackingScores(unconfirmedScores, unconfirmed.length, remainingHigh.length, minimumIouThresholdUnconfirmedAssociation);\n unmatchedUnconfirmed = unconfirmedAssociation.unmatchedTrackIndexes;\n for (const match of unconfirmedAssociation.matches) {\n updateMatchedTrack(unconfirmed[match.trackIndex], remainingHigh[match.detectionIndex], assignments);\n }\n unmatchedHighLocal =\n unconfirmedAssociation.unmatchedDetectionIndexes.map((index) => unmatchedHighLocal[index]);\n }\n const unmatchedUnconfirmedTracks = new Set(unmatchedUnconfirmed.map((index) => unconfirmed[index]));\n tracks = tracks.filter((track) => !unmatchedUnconfirmedTracks.has(track));\n for (const localIndex of unmatchedHighLocal) {\n const detection = highDetections[localIndex];\n if ((detection.confidence ?? 1) < trackActivationThreshold)\n continue;\n const track = new CBIoUTrack(detection);\n if (frameId === 1 && instantFirstFrameActivation) {\n track.trackerId = nextTrackerId++;\n assignments.set(detection.detectionIndex, track.trackerId);\n }\n tracks.push(track);\n }\n tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate &&\n (track.timeSinceUpdate === 0 ||\n track.trackerId !== undefined ||\n track.successfulUpdates >= minimumConsecutiveFrames));\n return createUpdate(detections, assignments, tracks);\n },\n };\n function updateMatchedTrack(track, detection, assignments) {\n track.update(detection);\n if (track.trackerId === undefined &&\n track.successfulUpdates >= minimumConsecutiveFrames) {\n track.trackerId = nextTrackerId++;\n }\n if (track.trackerId !== undefined) {\n assignments.set(detection.detectionIndex, track.trackerId);\n }\n }\n }\n function createUpdate(detections, assignments, tracks) {\n const resolvedAssignments = detections.flatMap((detection) => {\n const trackerId = assignments.get(detection.detectionIndex);\n return trackerId === undefined\n ? []\n : [{ detectionIndex: detection.detectionIndex, trackerId }];\n });\n return {\n activeTrackCount: tracks.length,\n assignments: resolvedAssignments,\n confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined)\n .length,\n };\n }\n\n class OCSortTrack {\n deltaT;\n age = 0;\n lastObservation;\n trackerId;\n successfulConsecutiveUpdates = 0;\n timeSinceUpdate = 0;\n velocity;\n frozenState;\n observed = true;\n observations = new Map();\n estimator;\n constructor(initial, deltaT) {\n this.deltaT = deltaT;\n this.lastObservation = initial;\n this.estimator = new TrackingKalmanEstimator(initial.rect, \"xcycsr\");\n this.configureNoise();\n }\n predict(frameStep, frameRate) {\n if (this.observed && this.timeSinceUpdate > 0) {\n this.frozenState = this.estimator.snapshot();\n this.observed = false;\n }\n this.estimator.predict(frameStep, frameRate);\n if (this.timeSinceUpdate > 0) {\n this.successfulConsecutiveUpdates = 0;\n }\n this.timeSinceUpdate += 1;\n this.age += 1;\n }\n update(detection, frameStep, frameRate) {\n const previous = this.getPreviousObservation();\n if (previous) {\n this.velocity = computeVelocity(previous, detection);\n }\n if (!this.observed && this.frozenState) {\n this.unfreeze(detection, frameStep, frameRate);\n }\n this.estimator.update(detection.rect);\n this.observed = true;\n this.timeSinceUpdate = 0;\n this.successfulConsecutiveUpdates += 1;\n this.lastObservation = detection;\n this.observations.set(this.age, detection);\n const cutoff = this.age - this.deltaT;\n for (const age of this.observations.keys()) {\n if (age < cutoff)\n this.observations.delete(age);\n }\n }\n getRect() {\n return this.estimator.getRect();\n }\n getPreviousObservation() {\n if (this.observations.size === 0)\n return undefined;\n for (let index = 0; index < this.deltaT; index += 1) {\n const delta = this.deltaT - index;\n const observation = this.observations.get(this.age - delta);\n if (observation)\n return observation;\n }\n const latestAge = Math.max(...this.observations.keys());\n return this.observations.get(latestAge);\n }\n configureNoise() {\n const measurementNoise = identity(4);\n for (let index = 2; index < 4; index += 1) {\n measurementNoise[index][index] *= 10;\n }\n const covariance = scaleMatrix(identity(7), 10);\n for (let index = 4; index < 7; index += 1) {\n covariance[index][index] *= 1000;\n }\n const processNoise = identity(7);\n processNoise[6][6] *= 0.01;\n for (let index = 4; index < 7; index += 1) {\n processNoise[index][index] *= 0.01;\n }\n this.estimator.setCovariances({\n covariance,\n measurementNoise,\n processNoise,\n });\n }\n unfreeze(detection, frameStep, frameRate) {\n if (!this.frozenState || this.timeSinceUpdate === 0)\n return;\n this.estimator.restore(this.frozenState);\n const timeGap = this.timeSinceUpdate;\n const start = rectToMeasurement(this.lastObservation.rect, \"xcycsr\");\n const end = rectToMeasurement(detection.rect, \"xcycsr\");\n const startWidth = Math.sqrt(start[2] * start[3]);\n const startHeight = start[3] === 0 ? 0 : Math.sqrt(start[2] / start[3]);\n const endWidth = Math.sqrt(end[2] * end[3]);\n const endHeight = end[3] === 0 ? 0 : Math.sqrt(end[2] / end[3]);\n for (let index = 0; index < timeGap; index += 1) {\n const progress = (index + 1) / timeGap;\n const x = start[0] + progress * (end[0] - start[0]);\n const y = start[1] + progress * (end[1] - start[1]);\n const width = startWidth + progress * (endWidth - startWidth);\n const height = startHeight + progress * (endHeight - startHeight);\n this.estimator.updateMeasurement([x, y, width * height, width / height]);\n if (index < timeGap - 1) {\n this.estimator.predict(frameStep, frameRate);\n }\n }\n this.frozenState = undefined;\n }\n }\n /** Creates the observation-centric SORT implementation from roboflow/trackers. */\n function createOCSortTracker$1(options = {}) {\n const lostTrackBuffer = normalizeNonNegativeInteger$1(options.lostTrackBuffer ?? 30, \"lostTrackBuffer\");\n const frameRate = options.frameRate ?? 30;\n const minimumConsecutiveFrames = normalizePositiveInteger$1(options.minimumConsecutiveFrames ?? 3, \"minimumConsecutiveFrames\");\n const minimumIouThreshold = options.minimumIouThreshold ?? 0.3;\n const directionConsistencyWeight = options.directionConsistencyWeight ?? 0.2;\n const highConfidenceDetectionThreshold = options.highConfidenceDetectionThreshold ?? 0.6;\n const deltaT = normalizePositiveInteger$1(options.deltaT ?? 3, \"deltaT\");\n if (!Number.isFinite(frameRate) || frameRate <= 0) {\n throw new Error(\"frameRate must be a finite positive value.\");\n }\n normalizeUnitInterval$1(minimumIouThreshold, \"minimumIouThreshold\");\n normalizeUnitInterval$1(directionConsistencyWeight, \"directionConsistencyWeight\");\n normalizeUnitInterval$1(highConfidenceDetectionThreshold, \"highConfidenceDetectionThreshold\");\n const scaledLostTrackBuffer = (frameRate / 30) * lostTrackBuffer;\n if (!Number.isFinite(scaledLostTrackBuffer)) {\n throw new Error(\"Scaled lostTrackBuffer overflows: frameRate / 30 * lostTrackBuffer must be finite.\");\n }\n const maximumFramesWithoutUpdate = lostTrackBuffer === 0 ? 0 : Math.max(1, Math.ceil(scaledLostTrackBuffer));\n let tracks = [];\n let frameCount = 0;\n let nextTrackerId = 0;\n let previousFrameIndex;\n return {\n reset() {\n tracks = [];\n frameCount = 0;\n nextTrackerId = 0;\n previousFrameIndex = undefined;\n },\n update(detections, frameIndex) {\n const frameStep = resolveFrameStep(frameIndex, previousFrameIndex);\n previousFrameIndex = frameIndex ?? previousFrameIndex;\n // Match roboflow/trackers: an empty stream before any track exists is\n // not part of OC-SORT's early-sequence activation window.\n if (tracks.length === 0 && detections.length === 0) {\n return {\n activeTrackCount: 0,\n assignments: [],\n confirmedTrackCount: 0,\n };\n }\n tracks.forEach((track) => track.predict(frameStep, frameRate));\n const highDetectionIndexes = detections.flatMap((detection, index) => detection.confidence === undefined ||\n detection.confidence >= highConfidenceDetectionThreshold\n ? [index]\n : []);\n const highDetections = highDetectionIndexes.map((index) => detections[index]);\n const iouScores = pairwiseIou(tracks.map((track) => track.getRect()), highDetections.map((detection) => detection.rect));\n const combinedScores = iouScores.map((row, trackIndex) => row.map((iou, detectionIndex) => {\n if (directionConsistencyWeight === 0)\n return iou;\n return (iou +\n directionConsistencyWeight *\n directionConsistency(tracks[trackIndex], highDetections[detectionIndex]) *\n (highDetections[detectionIndex].confidence ?? 1));\n }));\n const primary = associateTrackingScores(combinedScores, tracks.length, highDetections.length, minimumIouThreshold, iouScores);\n const assignments = new Map();\n for (const match of primary.matches) {\n updateMatchedTrack(tracks[match.trackIndex], highDetections[match.detectionIndex], assignments, frameStep);\n }\n let remainingHigh = [...primary.unmatchedDetectionIndexes];\n if (primary.unmatchedTrackIndexes.length > 0 &&\n remainingHigh.length > 0) {\n const unmatchedTracks = primary.unmatchedTrackIndexes.map((index) => tracks[index]);\n const unmatchedDetections = remainingHigh.map((index) => highDetections[index]);\n const recoveryScores = pairwiseIou(unmatchedTracks.map((track) => track.lastObservation.rect), unmatchedDetections.map((detection) => detection.rect));\n const recovery = associateTrackingScores(recoveryScores, unmatchedTracks.length, unmatchedDetections.length, minimumIouThreshold);\n for (const match of recovery.matches) {\n updateMatchedTrack(unmatchedTracks[match.trackIndex], unmatchedDetections[match.detectionIndex], assignments, frameStep);\n }\n remainingHigh = recovery.unmatchedDetectionIndexes.map((index) => remainingHigh[index]);\n }\n for (const localIndex of remainingHigh) {\n tracks.push(new OCSortTrack(highDetections[localIndex], deltaT));\n }\n tracks = tracks.filter((track) => track.timeSinceUpdate <= maximumFramesWithoutUpdate);\n frameCount += 1;\n const resolvedAssignments = detections.flatMap((detection) => {\n const trackerId = assignments.get(detection.detectionIndex);\n return trackerId === undefined\n ? []\n : [{ detectionIndex: detection.detectionIndex, trackerId }];\n });\n return {\n activeTrackCount: tracks.length,\n assignments: resolvedAssignments,\n confirmedTrackCount: tracks.filter((track) => track.trackerId !== undefined).length,\n };\n },\n };\n function updateMatchedTrack(track, detection, assignments, frameStep) {\n track.update(detection, frameStep, frameRate);\n const earlySequence = frameCount <= minimumConsecutiveFrames;\n const shouldEmit = (earlySequence && track.timeSinceUpdate === 0) ||\n track.successfulConsecutiveUpdates >= minimumConsecutiveFrames;\n if (shouldEmit) {\n if (track.trackerId === undefined) {\n track.trackerId = nextTrackerId++;\n }\n assignments.set(detection.detectionIndex, track.trackerId);\n }\n }\n }\n function computeVelocity(previous, current) {\n const deltaX = current.rect.x - previous.rect.x;\n const deltaY = current.rect.y - previous.rect.y;\n const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY) + 1e-6;\n return { x: deltaX / norm, y: deltaY / norm };\n }\n function directionConsistency(track, detection) {\n if (!track.velocity)\n return 0;\n const reference = track.getPreviousObservation() ?? track.lastObservation;\n const deltaX = detection.rect.x - reference.rect.x;\n const deltaY = detection.rect.y - reference.rect.y;\n const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY) + 1e-6;\n const cosine = Math.max(-1, Math.min(1, track.velocity.x * (deltaX / norm) + track.velocity.y * (deltaY / norm)));\n const angle = Math.acos(cosine);\n return (Math.PI / 2 - Math.abs(angle)) / Math.PI;\n }\n\n /** Public core facade for the internal tracker engine workspace. */\n function createSortTracker(options = {}) {\n return createSortTracker$1(options);\n }\n\n /** Public core facade for the internal tracker engine workspace. */\n function createByteTrackTracker(options = {}) {\n return createByteTrackTracker$1(options);\n }\n\n /** Public core facade for the internal tracker engine workspace. */\n function createCBIoUTracker(options = {}) {\n return createCBIoUTracker$1(options);\n }\n\n /** Public core facade for the internal tracker engine workspace. */\n function createOCSortTracker(options = {}) {\n return createOCSortTracker$1(options);\n }\n\n /** Geometry projected into a tracker. The original detection payload is retained. */\n var TrackingGeometry;\n (function (TrackingGeometry) {\n TrackingGeometry[\"Box\"] = \"box\";\n TrackingGeometry[\"Mask\"] = \"mask\";\n TrackingGeometry[\"Keypoints\"] = \"keypoints\";\n })(TrackingGeometry || (TrackingGeometry = {}));\n\n var DetectionMaskPayloadFormat;\n (function (DetectionMaskPayloadFormat) {\n DetectionMaskPayloadFormat[\"RawCocoRle\"] = \"rawCocoRle\";\n DetectionMaskPayloadFormat[\"DeflatedBase64\"] = \"deflatedBase64\";\n })(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));\n\n var DetectionPickTarget;\n (function (DetectionPickTarget) {\n DetectionPickTarget[\"Box\"] = \"box\";\n DetectionPickTarget[\"Edge\"] = \"edge\";\n DetectionPickTarget[\"Keypoint\"] = \"keypoint\";\n DetectionPickTarget[\"Label\"] = \"label\";\n DetectionPickTarget[\"Mask\"] = \"mask\";\n DetectionPickTarget[\"Polygon\"] = \"polygon\";\n DetectionPickTarget[\"Polyline\"] = \"polyline\";\n })(DetectionPickTarget || (DetectionPickTarget = {}));\n var MediaInteractionMode;\n (function (MediaInteractionMode) {\n MediaInteractionMode[\"Always\"] = \"always\";\n MediaInteractionMode[\"Disabled\"] = \"disabled\";\n MediaInteractionMode[\"PausedOnly\"] = \"pausedOnly\";\n })(MediaInteractionMode || (MediaInteractionMode = {}));\n\n var AnnotationGeometryKind;\n (function (AnnotationGeometryKind) {\n AnnotationGeometryKind[\"Box\"] = \"box\";\n AnnotationGeometryKind[\"Polygon\"] = \"polygon\";\n AnnotationGeometryKind[\"Polyline\"] = \"polyline\";\n AnnotationGeometryKind[\"Keypoints\"] = \"keypoints\";\n AnnotationGeometryKind[\"Mask\"] = \"mask\";\n })(AnnotationGeometryKind || (AnnotationGeometryKind = {}));\n var AnnotationGestureStateKind;\n (function (AnnotationGestureStateKind) {\n AnnotationGestureStateKind[\"Idle\"] = \"idle\";\n AnnotationGestureStateKind[\"Creating\"] = \"creating\";\n AnnotationGestureStateKind[\"Moving\"] = \"moving\";\n AnnotationGestureStateKind[\"Resizing\"] = \"resizing\";\n AnnotationGestureStateKind[\"DragSelecting\"] = \"dragSelecting\";\n })(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));\n var AnnotationHandleKind;\n (function (AnnotationHandleKind) {\n AnnotationHandleKind[\"Resize\"] = \"resize\";\n AnnotationHandleKind[\"Vertex\"] = \"vertex\";\n AnnotationHandleKind[\"AddVertex\"] = \"addVertex\";\n AnnotationHandleKind[\"Keypoint\"] = \"keypoint\";\n })(AnnotationHandleKind || (AnnotationHandleKind = {}));\n\n /** Alignment for strokes applied to closed rendered geometry. */\n var StrokeAlignment;\n (function (StrokeAlignment) {\n StrokeAlignment[\"Inside\"] = \"inside\";\n StrokeAlignment[\"Center\"] = \"center\";\n StrokeAlignment[\"Outside\"] = \"outside\";\n })(StrokeAlignment || (StrokeAlignment = {}));\n\n var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n\n var FocusTargetMode;\n (function (FocusTargetMode) {\n FocusTargetMode[\"Hovered\"] = \"hovered\";\n FocusTargetMode[\"Selected\"] = \"selected\";\n FocusTargetMode[\"HoveredAndSelected\"] = \"hoveredAndSelected\";\n FocusTargetMode[\"Ambient\"] = \"ambient\";\n })(FocusTargetMode || (FocusTargetMode = {}));\n\n var DetectionInteractionState;\n (function (DetectionInteractionState) {\n DetectionInteractionState[\"Hovered\"] = \"hovered\";\n DetectionInteractionState[\"Selected\"] = \"selected\";\n })(DetectionInteractionState || (DetectionInteractionState = {}));\n\n var LabelPlacement;\n (function (LabelPlacement) {\n LabelPlacement[\"Top\"] = \"top\";\n LabelPlacement[\"Bottom\"] = \"bottom\";\n LabelPlacement[\"InsideTop\"] = \"insideTop\";\n LabelPlacement[\"InsideBottom\"] = \"insideBottom\";\n LabelPlacement[\"Center\"] = \"center\";\n })(LabelPlacement || (LabelPlacement = {}));\n var LabelVisibilityMode;\n (function (LabelVisibilityMode) {\n LabelVisibilityMode[\"Always\"] = \"always\";\n LabelVisibilityMode[\"HoveredOnly\"] = \"hoveredOnly\";\n })(LabelVisibilityMode || (LabelVisibilityMode = {}));\n\n var MaskRenderMode;\n (function (MaskRenderMode) {\n MaskRenderMode[\"FillAndStroke\"] = \"fillAndStroke\";\n MaskRenderMode[\"FillOnly\"] = \"fillOnly\";\n MaskRenderMode[\"StrokeOnly\"] = \"strokeOnly\";\n })(MaskRenderMode || (MaskRenderMode = {}));\n\n /**\n * Discriminator for renderer-neutral shape draw instructions.\n */\n var ShapeInstructionKind;\n (function (ShapeInstructionKind) {\n ShapeInstructionKind[\"Ellipse\"] = \"ellipse\";\n ShapeInstructionKind[\"Marker\"] = \"marker\";\n ShapeInstructionKind[\"Path\"] = \"path\";\n })(ShapeInstructionKind || (ShapeInstructionKind = {}));\n var MarkerShape;\n (function (MarkerShape) {\n MarkerShape[\"Circle\"] = \"circle\";\n MarkerShape[\"Cross\"] = \"cross\";\n MarkerShape[\"Square\"] = \"square\";\n MarkerShape[\"Triangle\"] = \"triangle\";\n })(MarkerShape || (MarkerShape = {}));\n /**\n * Coordinate space for marker sizing. Media sizes scale with the viewport;\n * screen sizes stay constant on screen, matching stroke-width semantics.\n */\n var MarkerSizeSpace;\n (function (MarkerSizeSpace) {\n MarkerSizeSpace[\"Media\"] = \"media\";\n MarkerSizeSpace[\"Screen\"] = \"screen\";\n })(MarkerSizeSpace || (MarkerSizeSpace = {}));\n\n var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\n\n var MediaRendererFit;\n (function (MediaRendererFit) {\n /**\n * Preserve media aspect ratio and fit the full frame inside the canvas.\n */\n MediaRendererFit[\"Contain\"] = \"contain\";\n /**\n * Preserve media aspect ratio and fill the canvas, cropping if necessary.\n */\n MediaRendererFit[\"Cover\"] = \"cover\";\n })(MediaRendererFit || (MediaRendererFit = {}));\n /**\n * Playback lifecycle state reported by a platform renderer.\n */\n var MediaRendererPlaybackState;\n (function (MediaRendererPlaybackState) {\n MediaRendererPlaybackState[\"Loading\"] = \"loading\";\n MediaRendererPlaybackState[\"Ready\"] = \"ready\";\n MediaRendererPlaybackState[\"Playing\"] = \"playing\";\n MediaRendererPlaybackState[\"Buffering\"] = \"buffering\";\n MediaRendererPlaybackState[\"Paused\"] = \"paused\";\n MediaRendererPlaybackState[\"Error\"] = \"error\";\n MediaRendererPlaybackState[\"Destroyed\"] = \"destroyed\";\n })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));\n /**\n * Stable classification of a media failure.\n *\n * Applications branch on these kinds instead of parsing decoder, demuxer, or\n * container error text, and still own their localized user-facing copy. New\n * kinds may be added over time, so treat unrecognized values like `Unknown`.\n */\n var MediaErrorKind;\n (function (MediaErrorKind) {\n /** The media could not be opened or read, or was refused before any read. */\n MediaErrorKind[\"Unreadable\"] = \"unreadable\";\n /** The container or codec is not supported by this platform. */\n MediaErrorKind[\"UnsupportedFormat\"] = \"unsupportedFormat\";\n /** The media opened but carries no usable video track. */\n MediaErrorKind[\"NoVideoTrack\"] = \"noVideoTrack\";\n /** Decoding a sample failed after the source opened. */\n MediaErrorKind[\"Decode\"] = \"decode\";\n /** The media could not be fetched. */\n MediaErrorKind[\"Network\"] = \"network\";\n /** The host environment lacks the APIs this media source requires. */\n MediaErrorKind[\"EnvironmentUnsupported\"] = \"environmentUnsupported\";\n /** The failure could not be classified. */\n MediaErrorKind[\"Unknown\"] = \"unknown\";\n })(MediaErrorKind || (MediaErrorKind = {}));\n /**\n * Lower-level media source readiness.\n */\n var MediaSourceStatus;\n (function (MediaSourceStatus) {\n MediaSourceStatus[\"Loading\"] = \"loading\";\n MediaSourceStatus[\"Ready\"] = \"ready\";\n MediaSourceStatus[\"Error\"] = \"error\";\n MediaSourceStatus[\"Destroyed\"] = \"destroyed\";\n })(MediaSourceStatus || (MediaSourceStatus = {}));\n /**\n * Current renderer state.\n */\n /**\n * What a playback gate holds back.\n *\n * A source the renderer pulls samples from can be held between any two frames,\n * because the renderer decides when each one is drawn. A source that presents\n * its own frames owns the playhead, so holding it means stopping the producer\n * and starting it again, and a gate that stops one has to bound its own wait\n * or a producer nothing answers for never runs again.\n */\n var PlaybackGateReach;\n (function (PlaybackGateReach) {\n /** No gate: the picture moves and unprepared layers are absent from it. */\n PlaybackGateReach[\"Off\"] = \"off\";\n /**\n * Playback waits to begin, and stops again at any frame whose artifacts are\n * missing, until they arrive or the gate's own wait bound gives up on them.\n */\n PlaybackGateReach[\"EveryFrame\"] = \"everyFrame\";\n })(PlaybackGateReach || (PlaybackGateReach = {}));\n\n /**\n * Media-session operating mode.\n *\n * Core owns the names because file-like and stream-like lifecycle choices are\n * platform-neutral. Platform packages decide how these modes tune media,\n * storage, and renderer defaults.\n */\n var MediaSessionMode;\n (function (MediaSessionMode) {\n /**\n * Finite media. Defaults usually favor seek/replay and persistent detection\n * storage.\n */\n MediaSessionMode[\"File\"] = \"file\";\n /**\n * Live or append-only media. Defaults usually favor rolling windows and\n * bounded retention.\n */\n MediaSessionMode[\"Stream\"] = \"stream\";\n })(MediaSessionMode || (MediaSessionMode = {}));\n /**\n * Aggregate lifecycle state for a media session.\n */\n var MediaSessionStatus;\n (function (MediaSessionStatus) {\n MediaSessionStatus[\"Buffering\"] = \"buffering\";\n MediaSessionStatus[\"Destroyed\"] = \"destroyed\";\n MediaSessionStatus[\"Error\"] = \"error\";\n MediaSessionStatus[\"Loading\"] = \"loading\";\n MediaSessionStatus[\"Paused\"] = \"paused\";\n MediaSessionStatus[\"Playing\"] = \"playing\";\n MediaSessionStatus[\"Processing\"] = \"processing\";\n MediaSessionStatus[\"Ready\"] = \"ready\";\n })(MediaSessionStatus || (MediaSessionStatus = {}));\n /**\n * Subsystem currently affecting session readiness or presentation.\n */\n var MediaSessionActivityKind;\n (function (MediaSessionActivityKind) {\n /**\n * Playback is held waiting for a producer to emit detections for the frame\n * about to be shown, which is a wait on inference rather than on transfer or\n * decode. `DetectionsBuffering` is the transfer of detections that already\n * exist, and `PlaybackBuffering` is the wait for media bytes.\n */\n MediaSessionActivityKind[\"DetectionsAwaitingCoverage\"] = \"detectionsAwaitingCoverage\";\n MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n /**\n * The picture is waiting on media the source has not handed over yet, which\n * is the bytes and their decode rather than anything downstream of them.\n * `PlaybackBuffering` is what a transport reports once it has already\n * stopped; this is reported from the read itself, including the reads a seek\n * makes while the transport still reads as paused.\n */\n MediaSessionActivityKind[\"MediaSourceReading\"] = \"mediaSourceReading\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\n /**\n * The render-preparation gate held the picture for artifacts that never\n * arrived and let it go. Nothing is blocked: the frames reaching the screen\n * are the ones whose artifacts were given up on. What is waited on is\n * preparation finishing another frame, which is what lets the gate hold the\n * picture again; `RenderPreparing` covers the holds that are running.\n */\n MediaSessionActivityKind[\"RenderPreparationAbandoned\"] = \"renderPreparationAbandoned\";\n MediaSessionActivityKind[\"RenderPreparing\"] = \"renderPreparing\";\n })(MediaSessionActivityKind || (MediaSessionActivityKind = {}));\n /**\n * State of one session activity.\n */\n var MediaSessionActivityStatus;\n (function (MediaSessionActivityStatus) {\n MediaSessionActivityStatus[\"Error\"] = \"error\";\n MediaSessionActivityStatus[\"Running\"] = \"running\";\n MediaSessionActivityStatus[\"Waiting\"] = \"waiting\";\n })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));\n\n let tracker;\n self.addEventListener(\"message\", (event) => {\n const request = event.data;\n try {\n let response;\n if (request.type === \"configure\") {\n switch (request.processor.algorithm) {\n case \"bytetrack\":\n tracker = createByteTrackTracker(request.processor.options);\n break;\n case \"cbiou\":\n tracker = createCBIoUTracker(request.processor.options);\n break;\n case \"ocsort\":\n tracker = createOCSortTracker(request.processor.options);\n break;\n default:\n tracker = createSortTracker(request.processor.options);\n }\n response = { requestId: request.requestId, type: \"success\" };\n }\n else if (request.type === \"reset\") {\n tracker?.reset();\n response = { requestId: request.requestId, type: \"success\" };\n }\n else {\n if (!tracker) {\n throw new Error(\"Tracking worker is not configured.\");\n }\n const startedAt = performance.now();\n const update = tracker.update(request.detections, request.frameIndex);\n response = {\n ...update,\n durationMs: performance.now() - startedAt,\n requestId: request.requestId,\n type: \"success\",\n };\n }\n self.postMessage(response);\n }\n catch (error) {\n self.postMessage({\n message: error instanceof Error ? error.message : \"Tracking worker failed.\",\n requestId: request.requestId,\n type: \"error\",\n });\n }\n });\n\n})();";
499
501
 
500
502
  const DEFAULT_WORKER_NAME$1 = "supervision-detection-post-processing";
501
503
  let defaultWorkerUrl$1;
@@ -990,90 +992,6 @@ var MediaProbeIssueCode;
990
992
  MediaProbeIssueCode["TargetVideoCannotEncode"] = "targetVideoCannotEncode";
991
993
  })(MediaProbeIssueCode || (MediaProbeIssueCode = {}));
992
994
 
993
- /**
994
- * The kind each coded refusal already stands for. Reading the code beats
995
- * re-deriving the kind from the message: the text of a container refusal says
996
- * "demuxer", the text of a wedged worker says "timed out", and the patterns
997
- * below would file both under the wrong kind. Written as a total record, so a
998
- * code added to the vocabulary above fails the build until it has a kind.
999
- */
1000
- const PRODUCER_CODE_KINDS = {
1001
- ABORTED: MediaErrorKind.Unknown,
1002
- BACKEND_CRASHED: MediaErrorKind.Decode,
1003
- CONTAINER_UNREADABLE: MediaErrorKind.Unreadable,
1004
- DECODER_STALLED: MediaErrorKind.Decode,
1005
- DECODE_UNSUPPORTED: MediaErrorKind.UnsupportedFormat,
1006
- NO_VIDEO_TRACK: MediaErrorKind.NoVideoTrack,
1007
- PRESENTATION_MISMATCH: MediaErrorKind.Unknown,
1008
- RATE_UNSUPPORTED: MediaErrorKind.Unknown,
1009
- SOURCE_UNREADABLE: MediaErrorKind.Unreadable,
1010
- VIDEO_TRACK_UNREADABLE: MediaErrorKind.UnsupportedFormat,
1011
- };
1012
- const UNSUPPORTED_FORMAT_PATTERN = /unsupported|not supported|no (?:matching )?(?:decoder|codec)|codec/i;
1013
- const DECODE_PATTERN = /decod|demux|corrupt|malformed|bitstream/i;
1014
- const NETWORK_PATTERN = /network|fetch|http|failed to load|load failed|timed? ?out|abort/i;
1015
- /**
1016
- * A media failure with a stable, documented kind.
1017
- *
1018
- * Branch on `kind` instead of matching decoder, demuxer, or container message
1019
- * text. `message` stays diagnostic and may name vendor internals; applications
1020
- * own their user-facing copy. The originating failure is preserved on `cause`.
1021
- */
1022
- class MediaSourceError extends Error {
1023
- kind;
1024
- constructor(kind, message, options) {
1025
- super(message, options);
1026
- this.name = "MediaSourceError";
1027
- this.kind = kind;
1028
- }
1029
- }
1030
- function isMediaSourceError(value) {
1031
- return value instanceof MediaSourceError;
1032
- }
1033
- /**
1034
- * Classifies an arbitrary media failure into a `MediaSourceError`.
1035
- *
1036
- * Already-classified errors pass through so a kind chosen at the point of
1037
- * failure is never downgraded by a broader message match. Anything the
1038
- * heuristics cannot place stays representable as `Unknown` rather than being
1039
- * forced into a wrong kind.
1040
- */
1041
- function toMediaSourceError(error, fallbackMessage = "Media source failed.") {
1042
- if (isMediaSourceError(error)) {
1043
- return error;
1044
- }
1045
- const message = error instanceof Error ? error.message : fallbackMessage;
1046
- const kind = producerCodeKind(error) ?? classifyMediaErrorMessage(message);
1047
- return new MediaSourceError(kind, message, { cause: error });
1048
- }
1049
- /**
1050
- * Failure kind of any caught media failure.
1051
- *
1052
- * A `MediaSourceError` reports the kind chosen where it failed. Anything else
1053
- * is classified from its message, and stays `Unknown` when no kind fits.
1054
- */
1055
- function getMediaErrorKind(error) {
1056
- return toMediaSourceError(error).kind;
1057
- }
1058
- function producerCodeKind(error) {
1059
- const code = error?.code;
1060
- return typeof code === "string" && code in PRODUCER_CODE_KINDS
1061
- ? PRODUCER_CODE_KINDS[code]
1062
- : null;
1063
- }
1064
- function classifyMediaErrorMessage(message) {
1065
- if (NETWORK_PATTERN.test(message)) {
1066
- return MediaErrorKind.Network;
1067
- }
1068
- if (UNSUPPORTED_FORMAT_PATTERN.test(message)) {
1069
- return MediaErrorKind.UnsupportedFormat;
1070
- }
1071
- if (DECODE_PATTERN.test(message)) {
1072
- return MediaErrorKind.Decode;
1073
- }
1074
- return MediaErrorKind.Unknown;
1075
- }
1076
-
1077
995
  /**
1078
996
  * Normalizes a decoded media source onto a zero-based presentation timeline.
1079
997
  *
@@ -1748,531 +1666,6 @@ function formatPreparationErrorMessage(probe) {
1748
1666
  .join(" ")}`;
1749
1667
  }
1750
1668
 
1751
- /**
1752
- * Where a container keeps the table that says which byte range holds which
1753
- * frame.
1754
- */
1755
- var MediaIndexPlacement;
1756
- (function (MediaIndexPlacement) {
1757
- /** Before the media data, so an open reads it in the first bytes. */
1758
- MediaIndexPlacement["Front"] = "front";
1759
- /** After the media data, so an open over a link seeks to the end first. */
1760
- MediaIndexPlacement["End"] = "end";
1761
- /** One index per fragment, spread through the file. */
1762
- MediaIndexPlacement["Fragmented"] = "fragmented";
1763
- /** Not an ISO base media file, which is the only layout this probe reads. */
1764
- MediaIndexPlacement["Unknown"] = "unknown";
1765
- })(MediaIndexPlacement || (MediaIndexPlacement = {}));
1766
- /** A fact about a source that conversion could be a response to. */
1767
- var MediaConditionCode;
1768
- (function (MediaConditionCode) {
1769
- /** The demuxer does not read this file's container. */
1770
- MediaConditionCode["ContainerUnreadable"] = "containerUnreadable";
1771
- /** The container opened and the demuxer parsed no track out of it. */
1772
- MediaConditionCode["VideoTrackUnreadable"] = "videoTrackUnreadable";
1773
- /** The container's tracks read and none of them carries video. */
1774
- MediaConditionCode["NoVideoTrack"] = "noVideoTrack";
1775
- /** A video track the demuxer cannot name a codec for, so no decoder can be
1776
- * configured for it whatever the browser supports. */
1777
- MediaConditionCode["CodecUnnamed"] = "codecUnnamed";
1778
- /** The codec is named and this browser has no decoder for it. */
1779
- MediaConditionCode["CodecUndecodable"] = "codecUndecodable";
1780
- /**
1781
- * Frames do not arrive on a steady grid: two share a presentation timestamp,
1782
- * or the gaps between them take more values than a timebase's own rounding
1783
- * explains. Frame indices reconstructed as `round(time * rate)` name the
1784
- * wrong frame on such a source.
1785
- */
1786
- MediaConditionCode["UnstableFrameTiming"] = "unstableFrameTiming";
1787
- /** The frame index sits after the media data, so opening over a link pays a
1788
- * seek to the end of the file before the first frame. */
1789
- MediaConditionCode["IndexAtEnd"] = "indexAtEnd";
1790
- /** The file carries one index per fragment rather than one for the whole. */
1791
- MediaConditionCode["IndexFragmented"] = "indexFragmented";
1792
- /** The first frame's presentation timestamp is not zero, so media time and
1793
- * elapsed time are different numbers on this source. */
1794
- MediaConditionCode["NonZeroStart"] = "nonZeroStart";
1795
- })(MediaConditionCode || (MediaConditionCode = {}));
1796
- /** What a condition stands in the way of. */
1797
- var MediaConditionScope;
1798
- (function (MediaConditionScope) {
1799
- /** Getting a picture on screen at all. */
1800
- MediaConditionScope["Playback"] = "playback";
1801
- /** Naming the frame on screen by index, which is how detections are paired. */
1802
- MediaConditionScope["FrameIndexing"] = "frameIndexing";
1803
- })(MediaConditionScope || (MediaConditionScope = {}));
1804
- /** What to do about a condition. */
1805
- var MediaConditionResponse;
1806
- (function (MediaConditionResponse) {
1807
- /** Open the source as it is. */
1808
- MediaConditionResponse["None"] = "none";
1809
- /** Copy the coded frames into another container without re-encoding them. */
1810
- MediaConditionResponse["RemuxFirst"] = "remuxFirst";
1811
- /** Re-encode, and open the result once it is finished. */
1812
- MediaConditionResponse["ConvertFirst"] = "convertFirst";
1813
- /** Re-encode, and open the prefix as it is written. */
1814
- MediaConditionResponse["ConvertProgressively"] = "convertProgressively";
1815
- /** Do not open it, and say why. */
1816
- MediaConditionResponse["Refuse"] = "refuse";
1817
- })(MediaConditionResponse || (MediaConditionResponse = {}));
1818
- /** Whether a response leaves the frame sequence detections index intact. */
1819
- var ConversionFrameEffect;
1820
- (function (ConversionFrameEffect) {
1821
- /** Every source frame comes out, at its own presentation time. */
1822
- ConversionFrameEffect["Preserved"] = "preserved";
1823
- /** The output carries a different frame sequence from the source, so a
1824
- * detection indexed against the source names a different picture. */
1825
- ConversionFrameEffect["Resampled"] = "resampled";
1826
- })(ConversionFrameEffect || (ConversionFrameEffect = {}));
1827
-
1828
- /**
1829
- * How many different neighbour gaps a steady source is allowed. One is a source
1830
- * whose rate its timebase states exactly. Two is one whose timebase cannot:
1831
- * 30 fps on a millisecond grain alternates 33 and 34 ticks forever, and 10007
1832
- * ticks a second alternates 333 and 334. Measured across the 89-clip media
1833
- * matrix, every clip that steps correctly sits at one or two and every clip
1834
- * that steps wrong sits at three or more.
1835
- */
1836
- const MAX_STABLE_DISTINCT_GAPS = 2;
1837
- const RESPONSE_STRENGTH = {
1838
- [MediaConditionResponse.None]: 0,
1839
- [MediaConditionResponse.RemuxFirst]: 1,
1840
- [MediaConditionResponse.ConvertProgressively]: 2,
1841
- [MediaConditionResponse.ConvertFirst]: 3,
1842
- [MediaConditionResponse.Refuse]: 4,
1843
- };
1844
- /**
1845
- * Which conditions a source's measured facts hold, and what to do about each.
1846
- *
1847
- * Pure: everything it reasons over was measured once by the probe. The two
1848
- * verdicts are separate because the conditions are: a source whose frames do
1849
- * not arrive on a steady grid plays perfectly and cannot be addressed by frame
1850
- * index, and answering that with one verdict would either refuse a clip that
1851
- * plays or hand out indices that name the wrong picture.
1852
- */
1853
- function evaluateMediaConditions(facts, policy = {}) {
1854
- const conditions = [];
1855
- if (!facts.container) {
1856
- conditions.push({
1857
- code: MediaConditionCode.ContainerUnreadable,
1858
- detail: "The demuxer does not read this file's container.",
1859
- frameEffect: null,
1860
- response: MediaConditionResponse.Refuse,
1861
- scope: MediaConditionScope.Playback,
1862
- });
1863
- return createReport(facts, conditions);
1864
- }
1865
- if (facts.videoTrackCount === 0) {
1866
- conditions.push(facts.trackCount === 0
1867
- ? {
1868
- code: MediaConditionCode.VideoTrackUnreadable,
1869
- detail: "The container opened and the demuxer parsed no track out of it, so whatever video it holds is in a form this build cannot reach.",
1870
- frameEffect: null,
1871
- response: MediaConditionResponse.Refuse,
1872
- scope: MediaConditionScope.Playback,
1873
- }
1874
- : {
1875
- code: MediaConditionCode.NoVideoTrack,
1876
- detail: `The container's ${facts.trackCount} track(s) read and none of them carries video this build can reach.`,
1877
- frameEffect: null,
1878
- response: MediaConditionResponse.Refuse,
1879
- scope: MediaConditionScope.Playback,
1880
- });
1881
- return createReport(facts, conditions);
1882
- }
1883
- if (facts.codec === null) {
1884
- conditions.push({
1885
- code: MediaConditionCode.CodecUnnamed,
1886
- detail: "The video track opened and the demuxer cannot name its codec, so no decoder can be configured for it whatever this browser supports.",
1887
- frameEffect: null,
1888
- response: MediaConditionResponse.Refuse,
1889
- scope: MediaConditionScope.Playback,
1890
- });
1891
- }
1892
- else if (facts.canDecode === false) {
1893
- const progressive = policy.progressiveConversion === true;
1894
- conditions.push({
1895
- code: MediaConditionCode.CodecUndecodable,
1896
- detail: `This browser has no decoder for ${facts.codec}, so the frames have to be re-encoded into one it does.`,
1897
- frameEffect: ConversionFrameEffect.Resampled,
1898
- response: progressive
1899
- ? MediaConditionResponse.ConvertProgressively
1900
- : MediaConditionResponse.ConvertFirst,
1901
- scope: MediaConditionScope.Playback,
1902
- });
1903
- }
1904
- if (facts.container.indexPlacement === MediaIndexPlacement.End) {
1905
- conditions.push({
1906
- code: MediaConditionCode.IndexAtEnd,
1907
- detail: facts.remote
1908
- ? "The frame index sits after the media data, so opening this over a link fetches the end of the file before the first frame."
1909
- : "The frame index sits after the media data, which costs nothing to read from bytes already on this machine.",
1910
- frameEffect: ConversionFrameEffect.Preserved,
1911
- response: facts.remote
1912
- ? MediaConditionResponse.RemuxFirst
1913
- : MediaConditionResponse.None,
1914
- scope: MediaConditionScope.Playback,
1915
- });
1916
- }
1917
- if (facts.container.indexPlacement === MediaIndexPlacement.Fragmented) {
1918
- conditions.push({
1919
- code: MediaConditionCode.IndexFragmented,
1920
- detail: "The file carries one index per fragment rather than one for the whole, so a seek resolves against the fragment it lands in.",
1921
- frameEffect: ConversionFrameEffect.Preserved,
1922
- response: MediaConditionResponse.None,
1923
- scope: MediaConditionScope.Playback,
1924
- });
1925
- }
1926
- if (facts.timing) {
1927
- conditions.push(...timingConditions(facts.timing));
1928
- }
1929
- return createReport(facts, conditions);
1930
- }
1931
- /**
1932
- * What a conversion would do to the frame sequence, judged against the options
1933
- * as {@link normalizeMedia} will actually run them rather than as they were
1934
- * written: leaving the rate unstated does not keep the source's timing here,
1935
- * it takes the 30 hertz default.
1936
- *
1937
- * A stated rate is only safe on a source that already runs at exactly that
1938
- * rate. On anything else the conversion writes a different number of pictures
1939
- * at different times, and a detection carrying a source frame index then names
1940
- * a picture that was never at that position.
1941
- */
1942
- function describeConversionFrameEffect(options) {
1943
- const resolvedFrameRate = options.video?.frameRate ?? DEFAULT_NORMALIZATION_FRAME_RATE;
1944
- const timing = options.timing ?? null;
1945
- if (!timing) {
1946
- return {
1947
- detail: `The conversion writes ${resolvedFrameRate} frames a second and nothing measured what the source runs at, so whether the frames survive is unknown and has to be assumed not to.`,
1948
- effect: ConversionFrameEffect.Resampled,
1949
- resolvedFrameRate,
1950
- };
1951
- }
1952
- if (distinctGapsExceedTimebaseRounding(timing)) {
1953
- return {
1954
- detail: `The source's frames do not arrive on a steady grid, so writing them out at ${resolvedFrameRate} a second drops and repeats pictures to fill it.`,
1955
- effect: ConversionFrameEffect.Resampled,
1956
- resolvedFrameRate,
1957
- };
1958
- }
1959
- if (timing.medianGapTicks === 0) {
1960
- return {
1961
- detail: `The source holds one frame, and a conversion at ${resolvedFrameRate} frames a second writes as many of it as the output's duration asks for.`,
1962
- effect: ConversionFrameEffect.Resampled,
1963
- resolvedFrameRate,
1964
- };
1965
- }
1966
- const sourceFrameRate = timing.tickRate / timing.medianGapTicks;
1967
- if (!ratesMatch(sourceFrameRate, resolvedFrameRate)) {
1968
- return {
1969
- detail: `The source runs at ${sourceFrameRate.toFixed(3)} frames a second and the conversion writes ${resolvedFrameRate}, so the output carries a different picture at every position.`,
1970
- effect: ConversionFrameEffect.Resampled,
1971
- resolvedFrameRate,
1972
- };
1973
- }
1974
- return {
1975
- detail: `The conversion writes the rate the source already runs at, so each source frame comes out once and a source frame index still names the picture it named.`,
1976
- effect: ConversionFrameEffect.Preserved,
1977
- resolvedFrameRate,
1978
- };
1979
- }
1980
- function timingConditions(timing) {
1981
- const conditions = [];
1982
- if (distinctGapsExceedTimebaseRounding(timing)) {
1983
- conditions.push({
1984
- code: MediaConditionCode.UnstableFrameTiming,
1985
- detail: unstableTimingDetail(timing),
1986
- frameEffect: null,
1987
- response: MediaConditionResponse.Refuse,
1988
- scope: MediaConditionScope.FrameIndexing,
1989
- });
1990
- }
1991
- if (timing.firstTimestampTicks !== 0) {
1992
- conditions.push({
1993
- code: MediaConditionCode.NonZeroStart,
1994
- detail: `The first frame is presented at ${(timing.firstTimestampTicks / timing.tickRate).toFixed(3)} seconds, so media time and elapsed time differ by that much on this source.`,
1995
- frameEffect: ConversionFrameEffect.Preserved,
1996
- response: MediaConditionResponse.None,
1997
- scope: MediaConditionScope.FrameIndexing,
1998
- });
1999
- }
2000
- return conditions;
2001
- }
2002
- function unstableTimingDetail(timing) {
2003
- const scope = timing.sampleComplete
2004
- ? `across all ${timing.sampledPacketCount} frames`
2005
- : `across the first ${timing.sampledPacketCount} frames`;
2006
- return timing.duplicateTimestampCount > 0
2007
- ? `${timing.duplicateTimestampCount} pair(s) of frames share one presentation timestamp ${scope}, so no arithmetic can tell those pictures apart by time.`
2008
- : `Neighbouring frames are separated by ${timing.distinctGapCount} different gaps ${scope}, from ${timing.minGapTicks} to ${timing.maxGapTicks} ticks of ${timing.tickRate}, which is more than a timebase's own rounding produces.`;
2009
- }
2010
- /**
2011
- * Whether the gaps between frames take more values than a timebase that cannot
2012
- * state the rate exactly would produce on its own. Duplicate timestamps are
2013
- * counted here too: they are a zero gap, and a zero gap is never rounding.
2014
- */
2015
- function distinctGapsExceedTimebaseRounding(timing) {
2016
- return (timing.duplicateTimestampCount > 0 ||
2017
- timing.distinctGapCount > MAX_STABLE_DISTINCT_GAPS);
2018
- }
2019
- /**
2020
- * A rate derived from a median gap of whole ticks lands within a tick of the
2021
- * rate it stands for, so `30000/1001` read off a 90000-tick grain is 29.97003
2022
- * and the 29.97 a caller writes has to match it.
2023
- */
2024
- function ratesMatch(sourceFrameRate, requestedFrameRate) {
2025
- return Math.abs(sourceFrameRate - requestedFrameRate) < 0.01;
2026
- }
2027
- function createReport(facts, conditions) {
2028
- return {
2029
- conditions,
2030
- facts,
2031
- frameIndexing: strongestResponse(conditions, MediaConditionScope.FrameIndexing),
2032
- playback: strongestResponse(conditions, MediaConditionScope.Playback),
2033
- };
2034
- }
2035
- function strongestResponse(conditions, scope) {
2036
- let strongest = MediaConditionResponse.None;
2037
- for (const condition of conditions) {
2038
- if (condition.scope === scope &&
2039
- RESPONSE_STRENGTH[condition.response] > RESPONSE_STRENGTH[strongest]) {
2040
- strongest = condition.response;
2041
- }
2042
- }
2043
- return strongest;
2044
- }
2045
-
2046
- const DEFAULT_SAMPLE_PACKETS = 240;
2047
- /** Enough for a 64-bit box header, which is the longest one this walk reads. */
2048
- const BOX_HEADER_BYTES = 16;
2049
- const BOX_HEADER_MINIMUM_BYTES = 8;
2050
- const LARGE_SIZE_MARKER = 1;
2051
- /** A file whose top-level boxes have not resolved the question by here is not
2052
- * laid out in a way this walk describes. */
2053
- const MAX_TOP_LEVEL_BOXES = 12;
2054
- /**
2055
- * Measures a source once and says which conversion conditions hold.
2056
- *
2057
- * Deliberately cheap, because "convert only when necessary" is worth nothing if
2058
- * finding out costs what converting would. Nothing here decodes a frame or
2059
- * reads a byte of media data on an indexed container: the container's own box
2060
- * headers answer where the index sits, and the packet walk runs metadata-only
2061
- * over a bounded prefix.
2062
- */
2063
- async function probeMediaConditions(source, options = {}) {
2064
- const { ALL_FORMATS, BlobSource, Input, UnsupportedInputFormatError } = await import('mediabunny');
2065
- const indexPlacement = await readIndexPlacement((start, end) => source.slice(start, end).arrayBuffer().then(toBytes), source.size);
2066
- const input = new Input({
2067
- formats: ALL_FORMATS,
2068
- source: new BlobSource(source),
2069
- });
2070
- try {
2071
- const facts = await collectFacts({
2072
- indexPlacement,
2073
- input,
2074
- remote: options.remote === true,
2075
- samplePackets: options.samplePackets ?? DEFAULT_SAMPLE_PACKETS,
2076
- UnsupportedInputFormatError,
2077
- });
2078
- return evaluateMediaConditions(facts, options);
2079
- }
2080
- finally {
2081
- input.dispose();
2082
- }
2083
- }
2084
- /**
2085
- * Where an ISO base media file keeps its frame index, read from the top-level
2086
- * box headers alone.
2087
- *
2088
- * Each header states the size of its own box, so the walk hops from one to the
2089
- * next and never reads a box's contents: eight to sixteen bytes per box, and no
2090
- * more boxes than it takes to see `moov` or `moof`. Over a link that is a
2091
- * handful of range requests whatever the file weighs.
2092
- */
2093
- async function readIndexPlacement(read, size) {
2094
- let offset = 0;
2095
- let sawIndex = false;
2096
- let sawMediaData = false;
2097
- for (let box = 0; box < MAX_TOP_LEVEL_BOXES; box += 1) {
2098
- const end = Math.min(offset + BOX_HEADER_BYTES, size);
2099
- if (end - offset < BOX_HEADER_MINIMUM_BYTES) {
2100
- return MediaIndexPlacement.Unknown;
2101
- }
2102
- const header = await read(offset, end);
2103
- if (header.byteLength < BOX_HEADER_MINIMUM_BYTES) {
2104
- return MediaIndexPlacement.Unknown;
2105
- }
2106
- const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
2107
- const type = boxType(header);
2108
- if (box === 0 && type !== "ftyp") {
2109
- return MediaIndexPlacement.Unknown;
2110
- }
2111
- if (type === "moof") {
2112
- return MediaIndexPlacement.Fragmented;
2113
- }
2114
- if (type === "moov") {
2115
- if (sawMediaData) {
2116
- return MediaIndexPlacement.End;
2117
- }
2118
- // A fragmented file opens on an index too, and only the fragment header
2119
- // that follows it tells the two layouts apart.
2120
- sawIndex = true;
2121
- }
2122
- if (type === "mdat") {
2123
- if (sawIndex) {
2124
- return MediaIndexPlacement.Front;
2125
- }
2126
- sawMediaData = true;
2127
- }
2128
- const boxSize = readBoxSize(view, header.byteLength);
2129
- if (boxSize === null) {
2130
- return MediaIndexPlacement.Unknown;
2131
- }
2132
- offset += boxSize;
2133
- if (offset >= size) {
2134
- return MediaIndexPlacement.Unknown;
2135
- }
2136
- }
2137
- return MediaIndexPlacement.Unknown;
2138
- }
2139
- async function collectFacts(options) {
2140
- let videoTrack;
2141
- try {
2142
- videoTrack = await options.input.getPrimaryVideoTrack();
2143
- }
2144
- catch (error) {
2145
- if (error instanceof options.UnsupportedInputFormatError) {
2146
- return {
2147
- canDecode: null,
2148
- codec: null,
2149
- container: null,
2150
- remote: options.remote,
2151
- timing: null,
2152
- trackCount: 0,
2153
- videoTrackCount: 0,
2154
- };
2155
- }
2156
- throw error;
2157
- }
2158
- const tracks = await options.input.getTracks();
2159
- const container = { indexPlacement: options.indexPlacement };
2160
- if (!videoTrack) {
2161
- return {
2162
- canDecode: null,
2163
- codec: null,
2164
- container,
2165
- remote: options.remote,
2166
- timing: null,
2167
- trackCount: tracks.length,
2168
- videoTrackCount: 0,
2169
- };
2170
- }
2171
- const [codec, canDecode] = await Promise.all([
2172
- videoTrack.getCodec(),
2173
- videoTrack.canDecode(),
2174
- ]);
2175
- return {
2176
- canDecode,
2177
- codec,
2178
- container,
2179
- remote: options.remote,
2180
- timing: await measureTiming(videoTrack, options.samplePackets),
2181
- trackCount: tracks.length,
2182
- videoTrackCount: countVideoTracks(tracks),
2183
- };
2184
- }
2185
- async function measureTiming(videoTrack, samplePackets) {
2186
- const { EncodedPacketSink } = await import('mediabunny');
2187
- const tickRate = await readTickRate(videoTrack);
2188
- const sink = new EncodedPacketSink(videoTrack);
2189
- const decodeOrderTicks = [];
2190
- let sampleComplete = true;
2191
- for await (const packet of sink.packets(undefined, undefined, {
2192
- metadataOnly: true,
2193
- })) {
2194
- decodeOrderTicks.push(Math.round(packet.timestamp * tickRate));
2195
- if (decodeOrderTicks.length >= samplePackets) {
2196
- sampleComplete = false;
2197
- break;
2198
- }
2199
- }
2200
- return summarizeTiming(decodeOrderTicks, tickRate, sampleComplete);
2201
- }
2202
- /**
2203
- * Turns a walk of packet timestamps into the timing facts, in ticks.
2204
- *
2205
- * Decode order is not presentation order, so the table is sorted before its
2206
- * gaps are read. A walk that stopped at the cap has read every packet up to a
2207
- * decode position and no further, which leaves the tail of the sorted table
2208
- * missing frames that sit between ones it did read; the deepest reordering seen
2209
- * bounds how much of that tail is untrustworthy, and it is dropped. A walk that
2210
- * ran to the end of the track has no such tail.
2211
- */
2212
- function summarizeTiming(decodeOrderTicks, tickRate, sampleComplete) {
2213
- const sorted = [...decodeOrderTicks].sort((first, second) => first - second);
2214
- const usable = sampleComplete
2215
- ? sorted
2216
- : sorted.slice(0, Math.max(2, sorted.length - reorderDepth(sorted, decodeOrderTicks)));
2217
- const gaps = [];
2218
- for (let index = 1; index < usable.length; index += 1) {
2219
- gaps.push(usable[index] - usable[index - 1]);
2220
- }
2221
- gaps.sort((first, second) => first - second);
2222
- return {
2223
- distinctGapCount: new Set(gaps).size,
2224
- duplicateTimestampCount: gaps.filter((gap) => gap === 0).length,
2225
- firstTimestampTicks: sorted.length > 0 ? sorted[0] : 0,
2226
- maxGapTicks: gaps.length > 0 ? gaps[gaps.length - 1] : 0,
2227
- medianGapTicks: gaps.length > 0 ? gaps[gaps.length >> 1] : 0,
2228
- minGapTicks: gaps.length > 0 ? gaps[0] : 0,
2229
- sampleComplete,
2230
- sampledPacketCount: decodeOrderTicks.length,
2231
- tickRate,
2232
- };
2233
- }
2234
- /** How far a packet travels between decode position and presentation position,
2235
- * at its furthest. */
2236
- function reorderDepth(sorted, decodeOrderTicks) {
2237
- const rankByTick = new Map();
2238
- for (let index = 0; index < sorted.length; index += 1) {
2239
- if (!rankByTick.has(sorted[index])) {
2240
- rankByTick.set(sorted[index], index);
2241
- }
2242
- }
2243
- let deepest = 0;
2244
- for (let index = 0; index < decodeOrderTicks.length; index += 1) {
2245
- const rank = rankByTick.get(decodeOrderTicks[index]) ?? index;
2246
- deepest = Math.max(deepest, Math.abs(rank - index));
2247
- }
2248
- return deepest;
2249
- }
2250
- async function readTickRate(videoTrack) {
2251
- return typeof videoTrack.getTimeResolution === "function"
2252
- ? await videoTrack.getTimeResolution()
2253
- : (videoTrack.timeResolution ?? 1);
2254
- }
2255
- function countVideoTracks(tracks) {
2256
- return tracks.filter((track) => {
2257
- return track.type === "video";
2258
- }).length;
2259
- }
2260
- function boxType(header) {
2261
- return String.fromCharCode(header[4], header[5], header[6], header[7]);
2262
- }
2263
- function readBoxSize(view, available) {
2264
- const size = view.getUint32(0);
2265
- if (size === LARGE_SIZE_MARKER) {
2266
- return available < BOX_HEADER_BYTES
2267
- ? null
2268
- : Number(view.getBigUint64(BOX_HEADER_MINIMUM_BYTES));
2269
- }
2270
- return size < BOX_HEADER_MINIMUM_BYTES ? null : size;
2271
- }
2272
- function toBytes(buffer) {
2273
- return new Uint8Array(buffer);
2274
- }
2275
-
2276
1669
  /** Creates an image-first renderer source without routing through Mediabunny. */
2277
1670
  function createStaticImageMediaSource(source) {
2278
1671
  return {
@@ -2301,7 +1694,7 @@ function createImageUrlMediaSource(src, options = {}) {
2301
1694
  return createStaticImageMediaSource(image);
2302
1695
  }
2303
1696
  function createSingleFrameSource(source, width, height) {
2304
- const sample = createSample$1(source, width, height);
1697
+ const sample = createSample(source, width, height);
2305
1698
  const sampleSink = {
2306
1699
  async getSample() {
2307
1700
  return sample;
@@ -2328,7 +1721,7 @@ function createSingleFrameSource(source, width, height) {
2328
1721
  sampleSink,
2329
1722
  };
2330
1723
  }
2331
- function createSample$1(source, width, height) {
1724
+ function createSample(source, width, height) {
2332
1725
  return {
2333
1726
  close() { },
2334
1727
  draw(context, dx, dy, dWidth = width, dHeight = height) {
@@ -2370,8 +1763,8 @@ async function waitForImage(image) {
2370
1763
  }
2371
1764
 
2372
1765
  const DEFAULT_MAX_BUFFERED_FRAMES = 8;
2373
- const DEFAULT_FRAME_RATE$2 = 30;
2374
- const TIMESTAMP_EPSILON_SECONDS$1 = 1e-6;
1766
+ const DEFAULT_FRAME_RATE$1 = 30;
1767
+ const TIMESTAMP_EPSILON_SECONDS = 1e-6;
2375
1768
  /**
2376
1769
  * Adapts a browser MediaStream to the renderer-owned decoded-media boundary.
2377
1770
  *
@@ -2402,7 +1795,7 @@ async function openMediaStreamMediaSource(stream, options) {
2402
1795
  video.muted = true;
2403
1796
  video.playsInline = true;
2404
1797
  video.srcObject = stream;
2405
- const fallbackFrameDuration = resolveFrameDuration$1(videoTracks[0]);
1798
+ const fallbackFrameDuration = resolveFrameDuration(videoTracks[0]);
2406
1799
  const bufferedFrames = [];
2407
1800
  const waiters = new Set();
2408
1801
  let disposed = false;
@@ -2469,7 +1862,7 @@ async function openMediaStreamMediaSource(stream, options) {
2469
1862
  frame.image.close();
2470
1863
  return;
2471
1864
  }
2472
- if (frame.timestamp <= lastCapturedTimestamp + TIMESTAMP_EPSILON_SECONDS$1) {
1865
+ if (frame.timestamp <= lastCapturedTimestamp + TIMESTAMP_EPSILON_SECONDS) {
2473
1866
  frame.image.close();
2474
1867
  return;
2475
1868
  }
@@ -2554,7 +1947,7 @@ async function openMediaStreamMediaSource(stream, options) {
2554
1947
  const takeFrame = (startTimestamp, preferLatest) => {
2555
1948
  const minimumTimestamp = startTimestamp ?? Number.NEGATIVE_INFINITY;
2556
1949
  while (bufferedFrames[0] &&
2557
- bufferedFrames[0].timestamp < minimumTimestamp - TIMESTAMP_EPSILON_SECONDS$1) {
1950
+ bufferedFrames[0].timestamp < minimumTimestamp - TIMESTAMP_EPSILON_SECONDS) {
2558
1951
  bufferedFrames.shift()?.image.close();
2559
1952
  }
2560
1953
  if (bufferedFrames.length === 0)
@@ -2593,7 +1986,7 @@ async function openMediaStreamMediaSource(stream, options) {
2593
1986
  const frame = await nextFrame(startTimestamp);
2594
1987
  if (!frame)
2595
1988
  return;
2596
- startTimestamp = frame.timestamp + TIMESTAMP_EPSILON_SECONDS$1;
1989
+ startTimestamp = frame.timestamp + TIMESTAMP_EPSILON_SECONDS;
2597
1990
  yield createDecodedSample(frame);
2598
1991
  }
2599
1992
  },
@@ -2697,304 +2090,10 @@ function resolveMaxBufferedFrames(value) {
2697
2090
  }
2698
2091
  return value;
2699
2092
  }
2700
- function resolveFrameDuration$1(track) {
2093
+ function resolveFrameDuration(track) {
2701
2094
  const frameRate = track?.getSettings?.().frameRate;
2702
2095
  return frameRate && Number.isFinite(frameRate) && frameRate > 0
2703
2096
  ? 1 / frameRate
2704
- : 1 / DEFAULT_FRAME_RATE$2;
2705
- }
2706
-
2707
- const DEFAULT_MAX_DEVICE_PIXEL_RATIO = 2;
2708
- /**
2709
- * The pixel ratio a host's display box rasterizes at, for every grid that lands
2710
- * in that box: the presentation surface, the mask rasters drawn onto it, and the
2711
- * decode under both. The ceiling is what keeps a 3x display from costing nine
2712
- * times the texels of a 1x one for sharpness nobody resolves on a moving
2713
- * picture. An id raster carries a detection per pixel and can only be sampled
2714
- * nearest, so a grid it does not share ragged its edges.
2715
- *
2716
- * A ceiling that is not a usable number states nothing, and takes the default.
2717
- */
2718
- function resolveDisplayPixelRatio(display) {
2719
- const { devicePixelRatio, maxDevicePixelRatio } = display;
2720
- if (!Number.isFinite(devicePixelRatio) || devicePixelRatio <= 0) {
2721
- return 1;
2722
- }
2723
- const ceiling = maxDevicePixelRatio !== undefined &&
2724
- Number.isFinite(maxDevicePixelRatio) &&
2725
- maxDevicePixelRatio > 0
2726
- ? maxDevicePixelRatio
2727
- : DEFAULT_MAX_DEVICE_PIXEL_RATIO;
2728
- return Math.min(devicePixelRatio, ceiling);
2729
- }
2730
-
2731
- const VIDEO_ENGINE_ENTRY = "supervision/web-video-engine";
2732
- const VIDEO_ENGINE_ANALYSIS_ENTRY = `${VIDEO_ENGINE_ENTRY}/analysis`;
2733
- /**
2734
- * A bundler renames the engine chunk after the module it was split from, and a
2735
- * browser reports the failing chunk's URL rather than the specifier that asked
2736
- * for it, so the name a failure can be recognised by is the module's, not the
2737
- * entry's.
2738
- */
2739
- const VIDEO_ENGINE_MODULE = "web-video-engine";
2740
- const MISSING_MODULE_ERROR_CODES = new Set([
2741
- "ERR_MODULE_NOT_FOUND",
2742
- "MODULE_NOT_FOUND",
2743
- ]);
2744
- const MISSING_MODULE_MESSAGE = /cannot find (?:module|package)|failed to (?:resolve|fetch dynamically imported) module|could not resolve/i;
2745
- const QUOTED_SPECIFIER = /['"]([^'"]+)['"]/;
2746
- const MAX_WRAPPED_ERROR_DEPTH = 8;
2747
- /**
2748
- * The engine is a chunk of this package that the browser fetches on demand, so
2749
- * the raw failure names a URL nobody wrote and reads like a bug in the caller's
2750
- * own code.
2751
- */
2752
- function rethrowEngineImportFailure(error, entry) {
2753
- if (isEngineResolutionFailure(error)) {
2754
- throw new Error(`openVideoEngineMediaSource needs "${entry}", which did not load. ` +
2755
- "The video engine is a lazily loaded chunk of supervision, so check " +
2756
- "that the deployed build still serves every chunk it emitted.", { cause: error });
2757
- }
2758
- throw error;
2759
- }
2760
- /**
2761
- * An engine that never loaded and an engine that loaded and then threw arrive
2762
- * at the same catch, and only the first is a deployment problem. Loaders that
2763
- * wrap what they caught put the load failure under the error they raise, so the
2764
- * whole cause chain is read, bounded because a chain can be cyclic.
2765
- */
2766
- function isEngineResolutionFailure(error) {
2767
- let cursor = error;
2768
- for (let depth = 0; depth <= MAX_WRAPPED_ERROR_DEPTH; depth += 1) {
2769
- if (!(cursor instanceof Error))
2770
- return false;
2771
- if (namesUnloadableEngine(cursor))
2772
- return true;
2773
- cursor = cursor.cause;
2774
- }
2775
- return false;
2776
- }
2777
- /**
2778
- * Node and bundlers quote the specifier they failed to resolve, so a dependency
2779
- * missing from an engine that did load is told apart by whose name is quoted; a
2780
- * browser reports an unquoted URL, which carries the chunk name too.
2781
- *
2782
- * Each bundler words this differently and one of them stubs the import with an
2783
- * error of its own, so the wording is matched as well as the code. A build that
2784
- * names the engine first and its importer second is the engine failing to load;
2785
- * one that names a dependency first is that dependency's problem, and reading
2786
- * only the first quoted specifier keeps the two apart.
2787
- */
2788
- function namesUnloadableEngine(error) {
2789
- const code = "code" in error ? String(error.code) : "";
2790
- if (!MISSING_MODULE_ERROR_CODES.has(code) &&
2791
- !MISSING_MODULE_MESSAGE.test(error.message)) {
2792
- return false;
2793
- }
2794
- const quoted = QUOTED_SPECIFIER.exec(error.message)?.[1];
2795
- return (quoted ?? error.message).includes(VIDEO_ENGINE_MODULE);
2796
- }
2797
-
2798
- const MILLISECONDS_PER_SECOND$2 = 1000;
2799
- const DEFAULT_FRAME_RATE$1 = 30;
2800
- const TIMESTAMP_EPSILON_SECONDS = 1e-6;
2801
- const FRAMES_PRESENTATION_PREVIEW_MAX_WIDTH_PX = 320;
2802
- /**
2803
- * Adapts Roboflow's video engine to the decoded-media source seam.
2804
- *
2805
- * The pull path, `sampleSink`, serves one-off reads such as thumbnails and
2806
- * single frame grabs, decoding each request from scratch through the engine's
2807
- * batch analysis entry. The push path serves playback and scrub presentation:
2808
- * the engine holds no canvas, paints nothing, and announces every frame that
2809
- * earns the screen, so a compositor subscribes to `engine` and never pulls
2810
- * samples here.
2811
- */
2812
- async function openVideoEngineMediaSource(options) {
2813
- const { VideoEngine, displayBoxResolution } = await importEngineEntry();
2814
- const { display, frameDecodeStrategy, ...engineOptions } = options;
2815
- const engine = new VideoEngine({
2816
- decodeStrategy: display ? displayBoxResolution(display) : undefined,
2817
- previewWidth: framesPreviewWidth(display),
2818
- ...engineOptions,
2819
- presentation: "frames",
2820
- });
2821
- try {
2822
- const snapshot = await engine.load();
2823
- const frames = createAnalysisFrameReader({
2824
- decodeStrategy: frameDecodeStrategy,
2825
- frameDuration: resolveFrameDuration(snapshot.nativeFps),
2826
- source: options.source,
2827
- });
2828
- return {
2829
- engine,
2830
- input: {
2831
- dispose() {
2832
- void frames.close();
2833
- void engine.dispose();
2834
- },
2835
- },
2836
- metadata: createMetadata(options.source, snapshot),
2837
- sampleSink: frames.sampleSink,
2838
- };
2839
- }
2840
- catch (error) {
2841
- await engine.dispose();
2842
- throw error;
2843
- }
2844
- }
2845
- function createVideoEngineMediaRendererSource(options) {
2846
- return {
2847
- open() {
2848
- return openVideoEngineMediaSource(options);
2849
- },
2850
- };
2851
- }
2852
- /**
2853
- * The engine ships inside this package, and arrives through a dynamic import so
2854
- * that it lands in its own chunk: a consumer who only annotates images never
2855
- * downloads the engine's embedded decode worker. A static import would fold it
2856
- * into the main entry.
2857
- */
2858
- async function importEngineEntry() {
2859
- try {
2860
- return await import('./web-video-engine/engine.js');
2861
- }
2862
- catch (error) {
2863
- rethrowEngineImportFailure(error, VIDEO_ENGINE_ENTRY);
2864
- }
2865
- }
2866
- async function importAnalysisEntry() {
2867
- try {
2868
- return await import('./web-video-engine/analysis.js');
2869
- }
2870
- catch (error) {
2871
- rethrowEngineImportFailure(error, VIDEO_ENGINE_ANALYSIS_ENTRY);
2872
- }
2873
- }
2874
- /**
2875
- * A wider coarse frame means fewer of them resident, and residency is what a
2876
- * drag spends: a scrub position holding no coarse frame paints nothing until a
2877
- * full decode returns. A stated box only ever lowers the width, since a preview
2878
- * wider than the picture it stands in for buys no sharpness and costs resident
2879
- * frames.
2880
- */
2881
- function framesPreviewWidth(display) {
2882
- if (display === undefined || !(display.boxWidth > 0)) {
2883
- return FRAMES_PRESENTATION_PREVIEW_MAX_WIDTH_PX;
2884
- }
2885
- return Math.min(FRAMES_PRESENTATION_PREVIEW_MAX_WIDTH_PX, Math.ceil(display.boxWidth * resolveDisplayPixelRatio(display)));
2886
- }
2887
- function createAnalysisFrameReader(options) {
2888
- let sessionPromise;
2889
- let closed = false;
2890
- const openSession = () => {
2891
- sessionPromise ??= (async () => {
2892
- const { AnalysisSession } = await importAnalysisEntry();
2893
- return AnalysisSession.open({
2894
- decodeStrategy: options.decodeStrategy,
2895
- source: options.source,
2896
- });
2897
- })();
2898
- return sessionPromise;
2899
- };
2900
- const extractAt = async (timestamp) => {
2901
- if (closed)
2902
- return null;
2903
- const session = await openSession();
2904
- const [frame] = await session.extractFrames([timestamp]);
2905
- return frame ?? null;
2906
- };
2907
- const sampleSink = {
2908
- async getSample(timestamp) {
2909
- const frame = await extractAt(timestamp);
2910
- return frame ? createSample(frame, options.frameDuration) : null;
2911
- },
2912
- async *samples(startTimestamp = 0, endTimestamp = Number.POSITIVE_INFINITY) {
2913
- let cursor = startTimestamp;
2914
- let lastTimestamp = Number.NEGATIVE_INFINITY;
2915
- while (cursor <= endTimestamp) {
2916
- const frame = await extractAt(cursor);
2917
- if (!frame)
2918
- return;
2919
- // The analysis entry answers with the frame on screen at the requested
2920
- // timestamp, so a request past the last frame answers with that frame
2921
- // again. A timestamp that does not advance is the end of the track.
2922
- if (frame.timestampS <= lastTimestamp + TIMESTAMP_EPSILON_SECONDS) {
2923
- return;
2924
- }
2925
- lastTimestamp = frame.timestampS;
2926
- cursor = Math.max(cursor, frame.timestampS) + options.frameDuration;
2927
- yield createSample(frame, options.frameDuration);
2928
- }
2929
- },
2930
- async *samplesAtTimestamps(timestamps) {
2931
- if (closed)
2932
- return;
2933
- const session = await openSession();
2934
- for await (const frame of session.framesAtTimestamps([...timestamps])) {
2935
- yield frame ? createSample(frame, options.frameDuration) : null;
2936
- }
2937
- },
2938
- };
2939
- const close = async () => {
2940
- closed = true;
2941
- const opening = sessionPromise;
2942
- sessionPromise = undefined;
2943
- if (!opening)
2944
- return;
2945
- try {
2946
- const session = await opening;
2947
- await session.close();
2948
- }
2949
- catch {
2950
- // A session that failed to open already rejected the pull that opened it.
2951
- }
2952
- };
2953
- return { close, sampleSink };
2954
- }
2955
- function createSample(frame, duration) {
2956
- let closed = false;
2957
- return {
2958
- close() {
2959
- closed = true;
2960
- },
2961
- draw(context, dx, dy, dWidth = frame.width, dHeight = frame.height) {
2962
- if (closed)
2963
- throw new Error("Cannot draw a closed video engine frame.");
2964
- context.drawImage(frame.canvas, dx, dy, dWidth, dHeight);
2965
- },
2966
- duration,
2967
- timestamp: frame.timestampS,
2968
- };
2969
- }
2970
- function createMetadata(source, snapshot) {
2971
- const duration = Number.isFinite(snapshot.durationMs)
2972
- ? snapshot.durationMs / MILLISECONDS_PER_SECOND$2
2973
- : null;
2974
- const estimatedFrameRate = snapshot.nativeFps !== null && snapshot.nativeFps > 0
2975
- ? snapshot.nativeFps
2976
- : null;
2977
- return {
2978
- audioTrackCount: 0,
2979
- canRead: snapshot.canDecode,
2980
- duration,
2981
- estimatedFrameCount: duration !== null && estimatedFrameRate !== null
2982
- ? Math.max(1, Math.round(duration * estimatedFrameRate))
2983
- : null,
2984
- estimatedFrameRate,
2985
- firstTimestamp: snapshot.firstTimestampMs / MILLISECONDS_PER_SECOND$2,
2986
- formatMimeType: null,
2987
- formatName: "video-engine",
2988
- mimeType: "stream" in source ? source.mimeType : null,
2989
- primaryVideoHeight: snapshot.naturalHeight,
2990
- primaryVideoWidth: snapshot.naturalWidth,
2991
- trackCount: 1,
2992
- videoTrackCount: 1,
2993
- };
2994
- }
2995
- function resolveFrameDuration(nativeFps) {
2996
- return nativeFps !== null && nativeFps > 0
2997
- ? 1 / nativeFps
2998
2097
  : 1 / DEFAULT_FRAME_RATE$1;
2999
2098
  }
3000
2099
 
@@ -3025,6 +2124,9 @@ var RenderEnginePreference;
3025
2124
  RenderEnginePreference["WebGL"] = "webgl";
3026
2125
  })(RenderEnginePreference || (RenderEnginePreference = {}));
3027
2126
  const ALREADY_PRESENTED_SAMPLE_EPSILON_SECONDS = 1e-6;
2127
+ const DEFAULT_RENDER_PREPARATION_GATE_MAX_WAIT_SECONDS = 2;
2128
+ const DEFAULT_RENDER_PREPARATION_GATE_RESUME_MARGIN_WALL_SECONDS = 0.2;
2129
+ const DEFAULT_RENDER_PREPARATION_GATE_STOP_BELOW_WALL_SECONDS = 0.1;
3028
2130
  const RENDER_ENGINE_PREFERENCE = RenderEnginePreference.WebGL;
3029
2131
 
3030
2132
  const PLAYBACK_SAMPLE_QUEUE_CAPACITY = 6;
@@ -3042,6 +2144,7 @@ function createMediaPlaybackController(options) {
3042
2144
  let activeSampleIteratorId = 0;
3043
2145
  let activePrefetch;
3044
2146
  let activeSampleIteratorExhausted = false;
2147
+ let activeSampleWait;
3045
2148
  const queuedSampleWaiters = new Set();
3046
2149
  const notifyQueuedSampleWaiters = () => {
3047
2150
  for (const resolve of queuedSampleWaiters)
@@ -3055,6 +2158,16 @@ function createMediaPlaybackController(options) {
3055
2158
  }
3056
2159
  };
3057
2160
  const isPlaybackRunActive = (runId) => !destroyed && playing && playbackRunId === runId;
2161
+ /**
2162
+ * Starts the run that supersedes every earlier one. A readiness wait is
2163
+ * awaited bare, so this is the only moment anything knows the sample it was
2164
+ * started for will never be presented, and the only chance to say so.
2165
+ */
2166
+ const beginPlaybackRun = () => {
2167
+ activeSampleWait?.abort();
2168
+ activeSampleWait = undefined;
2169
+ playbackRunId += 1;
2170
+ };
3058
2171
  const schedulePlaybackFrame = (runId) => {
3059
2172
  if (!isPlaybackRunActive(runId) || animationFrameHandle !== undefined) {
3060
2173
  return;
@@ -3105,7 +2218,7 @@ function createMediaPlaybackController(options) {
3105
2218
  return;
3106
2219
  }
3107
2220
  playing = false;
3108
- playbackRunId += 1;
2221
+ beginPlaybackRun();
3109
2222
  cancelScheduledFrame();
3110
2223
  closeQueuedSamples();
3111
2224
  stopActiveSampleIterator();
@@ -3183,10 +2296,16 @@ function createMediaPlaybackController(options) {
3183
2296
  queuedSampleWaiters.delete(resolveWait);
3184
2297
  return;
3185
2298
  }
3186
- await waitForQueuedSample;
3187
- // Let an iterator publish immediately available adjacent samples without
3188
- // making live playback wait for the next network frame.
3189
- await Promise.resolve();
2299
+ options.onSourceWait?.(true);
2300
+ try {
2301
+ await waitForQueuedSample;
2302
+ // Let an iterator publish immediately available adjacent samples without
2303
+ // making live playback wait for the next network frame.
2304
+ await Promise.resolve();
2305
+ }
2306
+ finally {
2307
+ options.onSourceWait?.(false);
2308
+ }
3190
2309
  };
3191
2310
  const shouldPresentSample = (sample, shouldPresentLoopStartSample) => sample.timestamp > currentTime + ALREADY_PRESENTED_SAMPLE_EPSILON_SECONDS ||
3192
2311
  (shouldPresentLoopStartSample &&
@@ -3227,7 +2346,7 @@ function createMediaPlaybackController(options) {
3227
2346
  if (playableEnd !== null && requestedMediaTime >= playableEnd) {
3228
2347
  if (!options.loop) {
3229
2348
  playing = false;
3230
- playbackRunId += 1;
2349
+ beginPlaybackRun();
3231
2350
  closeQueuedSamples();
3232
2351
  stopActiveSampleIterator();
3233
2352
  options.onEnded();
@@ -3261,7 +2380,7 @@ function createMediaPlaybackController(options) {
3261
2380
  }
3262
2381
  if (activeSampleIteratorExhausted && sampleQueue.length === 0) {
3263
2382
  playing = false;
3264
- playbackRunId += 1;
2383
+ beginPlaybackRun();
3265
2384
  stopActiveSampleIterator();
3266
2385
  options.onEnded();
3267
2386
  return;
@@ -3302,11 +2421,16 @@ function createMediaPlaybackController(options) {
3302
2421
  didNotifyWaiting = true;
3303
2422
  options.onWaiting?.();
3304
2423
  }, 0);
2424
+ const sampleWait = new AbortController();
2425
+ activeSampleWait = sampleWait;
3305
2426
  try {
3306
- await options.waitForSample(sample);
2427
+ await options.waitForSample(sample, sampleWait.signal);
3307
2428
  }
3308
2429
  finally {
3309
2430
  clearTimeout(waitingTimer);
2431
+ if (activeSampleWait === sampleWait) {
2432
+ activeSampleWait = undefined;
2433
+ }
3310
2434
  }
3311
2435
  if (!didNotifyWaiting) {
3312
2436
  return false;
@@ -3320,7 +2444,7 @@ function createMediaPlaybackController(options) {
3320
2444
  return;
3321
2445
  }
3322
2446
  playing = true;
3323
- playbackRunId += 1;
2447
+ beginPlaybackRun();
3324
2448
  playbackOriginMediaTime = currentTime;
3325
2449
  playbackOriginNow = performance.now();
3326
2450
  resetSampleIterator(currentTime);
@@ -3332,7 +2456,7 @@ function createMediaPlaybackController(options) {
3332
2456
  return;
3333
2457
  }
3334
2458
  playing = false;
3335
- playbackRunId += 1;
2459
+ beginPlaybackRun();
3336
2460
  cancelScheduledFrame();
3337
2461
  closeQueuedSamples();
3338
2462
  stopActiveSampleIterator();
@@ -3343,7 +2467,7 @@ function createMediaPlaybackController(options) {
3343
2467
  }
3344
2468
  const shouldResume = playing;
3345
2469
  playing = false;
3346
- playbackRunId += 1;
2470
+ beginPlaybackRun();
3347
2471
  cancelScheduledFrame();
3348
2472
  closeQueuedSamples();
3349
2473
  stopActiveSampleIterator();
@@ -3354,7 +2478,7 @@ function createMediaPlaybackController(options) {
3354
2478
  return;
3355
2479
  }
3356
2480
  playing = true;
3357
- playbackRunId += 1;
2481
+ beginPlaybackRun();
3358
2482
  resetSampleIterator(mediaTime);
3359
2483
  startSamplePrefetch(playbackRunId);
3360
2484
  schedulePlaybackFrame(playbackRunId);
@@ -3379,7 +2503,7 @@ function createMediaPlaybackController(options) {
3379
2503
  }
3380
2504
  destroyed = true;
3381
2505
  playing = false;
3382
- playbackRunId += 1;
2506
+ beginPlaybackRun();
3383
2507
  cancelScheduledFrame();
3384
2508
  closeQueuedSamples();
3385
2509
  stopActiveSampleIterator();
@@ -3537,6 +2661,7 @@ function createOffsetDetectionFrameSource(source, offsetSeconds) {
3537
2661
  function createLoadingMediaSourceState() {
3538
2662
  return {
3539
2663
  audioTrackCount: null,
2664
+ awaitingRead: false,
3540
2665
  canRead: null,
3541
2666
  duration: null,
3542
2667
  estimatedFrameCount: null,
@@ -3557,6 +2682,7 @@ function createLoadingMediaSourceState() {
3557
2682
  function createReadyMediaSourceState(metadata) {
3558
2683
  return {
3559
2684
  audioTrackCount: metadata.audioTrackCount,
2685
+ awaitingRead: false,
3560
2686
  canRead: metadata.canRead,
3561
2687
  duration: metadata.duration,
3562
2688
  estimatedFrameCount: metadata.estimatedFrameCount ?? null,
@@ -3589,12 +2715,14 @@ function createMediaRendererRuntimeState(options) {
3589
2715
  let rendererBackend = null;
3590
2716
  /** Frames put on screen, not paints. */
3591
2717
  let presentedFrames = 0;
2718
+ let presentedTime = null;
3592
2719
  let presentedFrameSerial = 0;
3593
2720
  let activeDetectionFrameTime = null;
3594
2721
  let activeDetectionFrameIndex = null;
3595
2722
  let activeDetectionCount = 0;
3596
2723
  let drawnMaskFrameTime = null;
3597
2724
  let maskHeldStale = false;
2725
+ let renderPreparationGateAbandoned = false;
3598
2726
  let seeking = false;
3599
2727
  let scrubbing = false;
3600
2728
  let lastFrameRenderTimings = null;
@@ -3627,6 +2755,8 @@ function createMediaRendererRuntimeState(options) {
3627
2755
  playbackGateReach: options.getPlaybackGateReach(),
3628
2756
  playbackState,
3629
2757
  presentedFrames,
2758
+ presentedTime,
2759
+ renderPreparationGateAbandoned,
3630
2760
  scrubbing,
3631
2761
  seeking,
3632
2762
  rendererBackend,
@@ -3653,8 +2783,11 @@ function createMediaRendererRuntimeState(options) {
3653
2783
  const emitState = () => {
3654
2784
  options.onState?.(createStateSnapshot());
3655
2785
  };
3656
- const adoptPresentedSample = (sample) => {
3657
- currentTime = sample.mediaTime;
2786
+ const adoptPresentedSample = (sample, adoptCurrentTime) => {
2787
+ if (adoptCurrentTime) {
2788
+ currentTime = sample.mediaTime;
2789
+ }
2790
+ presentedTime = sample.mediaTime;
3658
2791
  activeDetectionFrameIndex = sample.activeDetectionFrameIndex;
3659
2792
  activeDetectionFrameTime = sample.activeDetectionFrameTime;
3660
2793
  activeDetectionCount = sample.activeDetectionCount;
@@ -3665,6 +2798,9 @@ function createMediaRendererRuntimeState(options) {
3665
2798
  currentTime() {
3666
2799
  return currentTime;
3667
2800
  },
2801
+ presentedTime() {
2802
+ return presentedTime;
2803
+ },
3668
2804
  duration() {
3669
2805
  return duration;
3670
2806
  },
@@ -3701,6 +2837,23 @@ function createMediaRendererRuntimeState(options) {
3701
2837
  sourceState = createReadyMediaSourceState(metadata);
3702
2838
  emitSourceState();
3703
2839
  },
2840
+ setSourceAwaitingRead(awaitingRead) {
2841
+ if (sourceState.awaitingRead === awaitingRead) {
2842
+ return;
2843
+ }
2844
+ setSourceState({ awaitingRead });
2845
+ emitState();
2846
+ },
2847
+ setRenderPreparationGateAbandoned(abandoned) {
2848
+ if (renderPreparationGateAbandoned === abandoned) {
2849
+ return;
2850
+ }
2851
+ renderPreparationGateAbandoned = abandoned;
2852
+ emitState();
2853
+ },
2854
+ playbackRate() {
2855
+ return playbackRate;
2856
+ },
3704
2857
  recordPlayheadTime(nextCurrentTime) {
3705
2858
  if (currentTime === nextCurrentTime) {
3706
2859
  return;
@@ -3722,17 +2875,18 @@ function createMediaRendererRuntimeState(options) {
3722
2875
  currentFrameDuration = sample.duration ?? currentFrameDuration;
3723
2876
  presentedFrames += 1;
3724
2877
  presentedFrameSerial = sample.presentedFrameSerial;
3725
- adoptPresentedSample(sample);
2878
+ adoptPresentedSample(sample, true);
3726
2879
  lastFrameRenderTimings = sample.renderTimings ?? null;
3727
2880
  options.onFrame?.(createFrameDiagnostics(sample));
3728
2881
  emitState();
3729
2882
  },
3730
2883
  recordPresentationUpdate(sample) {
3731
- if (sample.presentedFrameSerial !== presentedFrameSerial) {
2884
+ const isNewPresentedFrame = sample.presentedFrameSerial !== presentedFrameSerial;
2885
+ if (isNewPresentedFrame) {
3732
2886
  presentedFrames += 1;
3733
2887
  presentedFrameSerial = sample.presentedFrameSerial;
3734
2888
  }
3735
- adoptPresentedSample(sample);
2889
+ adoptPresentedSample(sample, isNewPresentedFrame);
3736
2890
  lastFrameRenderTimings = sample.renderTimings ?? lastFrameRenderTimings;
3737
2891
  emitState();
3738
2892
  },
@@ -3805,7 +2959,7 @@ function createMediaRenderErrorSourcePatch(error) {
3805
2959
  };
3806
2960
  }
3807
2961
 
3808
- const MILLISECONDS_PER_SECOND$1 = 1000;
2962
+ const MILLISECONDS_PER_SECOND$2 = 1000;
3809
2963
  /**
3810
2964
  * The renderer's playback surface over a producer that owns the playhead.
3811
2965
  * Seconds meet milliseconds here and nowhere else.
@@ -3827,7 +2981,23 @@ function createMediaRendererTransport(options) {
3827
2981
  let playbackIntent = 0;
3828
2982
  /** Intent a readiness wait belongs to, or null while none is running. */
3829
2983
  let readinessHoldIntent = null;
3830
- const isHoldingForReadiness = () => readinessHoldIntent === playbackIntent;
2984
+ /** Whether a readiness hold still owes the producer its release. */
2985
+ let readinessFreezeHeld = false;
2986
+ let activeReadinessWait;
2987
+ let presentationWait = 0;
2988
+ let presentationHold = null;
2989
+ const isHoldingForReadiness = () => readinessHoldIntent === playbackIntent ||
2990
+ presentationHold?.intent === playbackIntent;
2991
+ /**
2992
+ * Supersedes every playback intent still in flight. A readiness wait is
2993
+ * awaited bare, so this is the only moment the gate can be told the play it
2994
+ * is holding open is over.
2995
+ */
2996
+ const beginPlaybackIntent = () => {
2997
+ activeReadinessWait?.abort();
2998
+ activeReadinessWait = undefined;
2999
+ return ++playbackIntent;
3000
+ };
3831
3001
  const publishSeekSignals = () => {
3832
3002
  options.onScrubbing(gestureInFlight);
3833
3003
  options.onSeeking(isSettling(channel.getStatus(), channel.getSeeking()));
@@ -3837,9 +3007,19 @@ function createMediaRendererTransport(options) {
3837
3007
  return;
3838
3008
  }
3839
3009
  gestureInFlight = false;
3010
+ // The producer holds one freeze, so a drag that took it over from a
3011
+ // readiness hold is releasing that hold's too.
3012
+ readinessFreezeHeld = false;
3840
3013
  publishSeekSignals();
3841
3014
  await channel.endInteractiveSeek();
3842
3015
  };
3016
+ const releaseReadinessFreeze = async () => {
3017
+ if (!readinessFreezeHeld || gestureInFlight) {
3018
+ return;
3019
+ }
3020
+ readinessFreezeHeld = false;
3021
+ await channel.endInteractiveSeek();
3022
+ };
3843
3023
  const publishPlaybackState = () => {
3844
3024
  const status = channel.getStatus();
3845
3025
  // A producer never loops itself, and its play() from ENDED restarts at the
@@ -3881,9 +3061,10 @@ function createMediaRendererTransport(options) {
3881
3061
  * uses, so a drag arriving mid-hold takes the hold over rather than fighting
3882
3062
  * it and the release it lands with is the one that resumes.
3883
3063
  */
3884
- const holdForReadiness = async (wait) => {
3064
+ const holdForReadiness = async (wait, readinessWait) => {
3885
3065
  const intent = playbackIntent;
3886
3066
  readinessHoldIntent = intent;
3067
+ readinessFreezeHeld = true;
3887
3068
  channel.beginInteractiveSeek();
3888
3069
  publishPlaybackState();
3889
3070
  try {
@@ -3894,12 +3075,18 @@ function createMediaRendererTransport(options) {
3894
3075
  // that starts the producer again. Whoever owns the failure reports it.
3895
3076
  }
3896
3077
  finally {
3078
+ if (activeReadinessWait === readinessWait) {
3079
+ activeReadinessWait = undefined;
3080
+ }
3897
3081
  if (readinessHoldIntent === intent) {
3898
3082
  readinessHoldIntent = null;
3899
3083
  }
3900
3084
  }
3901
- if (!gestureInFlight) {
3902
- await channel.endInteractiveSeek();
3085
+ // A play that superseded this hold is waiting for readiness of its own,
3086
+ // and the producer running before that lands is what the wait is there to
3087
+ // prevent. It inherits the freeze and releases it when it starts playback.
3088
+ if (readinessHoldIntent === null) {
3089
+ await releaseReadinessFreeze();
3903
3090
  }
3904
3091
  publishPlaybackState();
3905
3092
  };
@@ -3912,9 +3099,11 @@ function createMediaRendererTransport(options) {
3912
3099
  channel.getSeeking()) {
3913
3100
  return;
3914
3101
  }
3915
- const wait = options.holdForReadiness(channel.getPlayhead().mediaTimeS);
3102
+ const readinessWait = new AbortController();
3103
+ const wait = options.holdForReadiness(channel.getPlayhead().mediaTimeS, readinessWait.signal);
3916
3104
  if (wait) {
3917
- void holdForReadiness(wait);
3105
+ activeReadinessWait = readinessWait;
3106
+ void holdForReadiness(wait, readinessWait);
3918
3107
  }
3919
3108
  };
3920
3109
  const publishPlaybackRate = () => {
@@ -3928,39 +3117,66 @@ function createMediaRendererTransport(options) {
3928
3117
  ];
3929
3118
  const transport = {
3930
3119
  async play() {
3120
+ options.invalidatePresentedFrame?.();
3121
+ presentationWait += 1;
3122
+ presentationHold = null;
3931
3123
  // The producer answers on its own thread, so its status still reads the
3932
3124
  // previous playback until it does. Recording the ask here is what lets a
3933
3125
  // second toggle arriving in that window flip it.
3934
3126
  settledState = MediaRendererPlaybackState.Playing;
3935
- const intent = ++playbackIntent;
3127
+ const intent = beginPlaybackIntent();
3936
3128
  await releaseGesture();
3129
+ /* A play superseded while the gesture was releasing must not open a
3130
+ readiness hold: nothing would be left holding the one that replaced
3131
+ it. */
3132
+ if (intent !== playbackIntent) {
3133
+ publishPlaybackState();
3134
+ return;
3135
+ }
3937
3136
  if (options.waitForReadiness) {
3137
+ const readinessWait = new AbortController();
3138
+ activeReadinessWait = readinessWait;
3938
3139
  readinessHoldIntent = intent;
3939
3140
  publishPlaybackState();
3141
+ let readinessLanded = false;
3940
3142
  try {
3941
- await options.waitForReadiness(channel.getPlayhead().mediaTimeS);
3143
+ await options.waitForReadiness(channel.getPlayhead().mediaTimeS, readinessWait.signal);
3144
+ readinessLanded = true;
3942
3145
  }
3943
3146
  finally {
3147
+ if (activeReadinessWait === readinessWait) {
3148
+ activeReadinessWait = undefined;
3149
+ }
3944
3150
  if (readinessHoldIntent === intent) {
3945
3151
  readinessHoldIntent = null;
3946
3152
  }
3153
+ // A wait that failed takes the play with it, and the producer is
3154
+ // still frozen for a hold this play took over.
3155
+ if (!readinessLanded && intent === playbackIntent) {
3156
+ void releaseReadinessFreeze();
3157
+ }
3947
3158
  }
3948
3159
  if (intent !== playbackIntent) {
3949
3160
  publishPlaybackState();
3950
3161
  return;
3951
3162
  }
3952
3163
  }
3164
+ await releaseReadinessFreeze();
3953
3165
  await channel.play();
3954
3166
  // A producer already at speed reports no change, so nothing else would
3955
3167
  // retire the hold's own Buffering and it would stand for good.
3956
3168
  publishPlaybackState();
3957
3169
  },
3958
3170
  pause() {
3171
+ options.invalidatePresentedFrame?.();
3172
+ presentationWait += 1;
3173
+ presentationHold = null;
3959
3174
  settledState = MediaRendererPlaybackState.Paused;
3960
- playbackIntent++;
3175
+ beginPlaybackIntent();
3961
3176
  // A pause ends the producer's mechanical hold, so it lands ahead of the
3962
- // release the open gesture still owes.
3177
+ // releases a readiness hold or an open gesture still owe.
3963
3178
  channel.pause();
3179
+ void releaseReadinessFreeze();
3964
3180
  void releaseGesture();
3965
3181
  },
3966
3182
  async togglePlayback() {
@@ -3980,15 +3196,22 @@ function createMediaRendererTransport(options) {
3980
3196
  // A drag whose landing seek is still releasing the producer is the drag
3981
3197
  // this scrub belongs to. Opening a second one there would stop the
3982
3198
  // picture for a gesture nobody is holding, and nothing would release it.
3983
- playbackIntent++;
3199
+ options.invalidatePresentedFrame?.();
3200
+ presentationWait += 1;
3201
+ presentationHold = null;
3202
+ beginPlaybackIntent();
3984
3203
  if (!gestureInFlight && landingRelease === null) {
3985
3204
  gestureInFlight = true;
3986
3205
  channel.beginInteractiveSeek();
3987
3206
  publishSeekSignals();
3988
3207
  }
3989
- channel.scrub(mediaTime * MILLISECONDS_PER_SECOND$1, "gesture");
3208
+ channel.scrub(mediaTime * MILLISECONDS_PER_SECOND$2, "gesture");
3990
3209
  },
3991
3210
  async commit(mediaTime) {
3211
+ options.invalidatePresentedFrame?.();
3212
+ const navigationPresentation = ++presentationWait;
3213
+ presentationHold = null;
3214
+ beginPlaybackIntent();
3992
3215
  // Releasing first is what keeps a drag from freezing the picture on
3993
3216
  // release: the producer resumes on its own terms, and the landing decode
3994
3217
  // for a cold region no longer sits between the pointer coming up and
@@ -3997,23 +3220,100 @@ function createMediaRendererTransport(options) {
3997
3220
  landingRelease = release;
3998
3221
  try {
3999
3222
  await release;
3223
+ if (navigationPresentation === presentationWait) {
3224
+ await releaseReadinessFreeze();
3225
+ }
4000
3226
  }
4001
3227
  finally {
4002
3228
  if (landingRelease === release) {
4003
3229
  landingRelease = null;
4004
3230
  }
4005
3231
  }
4006
- await channel.commit(mediaTime * MILLISECONDS_PER_SECOND$1);
3232
+ const presentation = options.beginPresentedFrameNavigation?.();
3233
+ try {
3234
+ await channel.commit(mediaTime * MILLISECONDS_PER_SECOND$2);
3235
+ await presentation?.waitFor(channel.getPlayhead().frame);
3236
+ }
3237
+ catch (error) {
3238
+ presentation?.cancel();
3239
+ throw error;
3240
+ }
4007
3241
  },
4008
3242
  async step(direction) {
3243
+ options.invalidatePresentedFrame?.();
3244
+ const navigationPresentation = ++presentationWait;
3245
+ presentationHold = null;
3246
+ beginPlaybackIntent();
4009
3247
  await releaseGesture();
4010
- await channel.step(direction);
3248
+ if (navigationPresentation === presentationWait) {
3249
+ await releaseReadinessFreeze();
3250
+ }
3251
+ const presentation = options.beginPresentedFrameNavigation?.();
3252
+ const before = channel.getPlayhead().frame;
3253
+ try {
3254
+ await channel.step(direction);
3255
+ const after = channel.getPlayhead().frame;
3256
+ if (before.index === after.index && before.ticks === after.ticks) {
3257
+ // A boundary step intentionally emits no replacement frame. The
3258
+ // frame already on screen remains authoritative, but it predates
3259
+ // this navigation ticket and therefore cannot acknowledge it.
3260
+ presentation?.cancel();
3261
+ }
3262
+ else {
3263
+ await presentation?.waitFor(after);
3264
+ }
3265
+ }
3266
+ catch (error) {
3267
+ presentation?.cancel();
3268
+ throw error;
3269
+ }
4011
3270
  },
4012
3271
  setPlaybackRate(rate) {
4013
3272
  channel.setPlaybackRate(rate);
4014
3273
  },
3274
+ protectPresentation(mediaTime, signal) {
3275
+ if (!options.waitForPresentationReadiness)
3276
+ return null;
3277
+ const guarded = options.waitForPresentationReadiness(mediaTime, signal);
3278
+ if (!guarded) {
3279
+ if (presentationHold) {
3280
+ presentationWait += 1;
3281
+ presentationHold = null;
3282
+ void releaseReadinessFreeze().then(publishPlaybackState);
3283
+ }
3284
+ return null;
3285
+ }
3286
+ const wait = ++presentationWait;
3287
+ const intent = playbackIntent;
3288
+ presentationHold = { intent, wait };
3289
+ readinessFreezeHeld = true;
3290
+ channel.beginInteractiveSeek();
3291
+ publishPlaybackState();
3292
+ return (async () => {
3293
+ try {
3294
+ await guarded;
3295
+ }
3296
+ catch {
3297
+ // A readiness provider failure degrades annotations, not media. The
3298
+ // provider's diagnostics own the error while the frame stays visible.
3299
+ }
3300
+ finally {
3301
+ if (presentationHold?.wait === wait) {
3302
+ presentationHold = null;
3303
+ }
3304
+ // An aborted frame hands the existing freeze to a newer frame. A
3305
+ // navigation increments presentationWait and releases explicitly.
3306
+ if (wait === presentationWait && !signal.aborted) {
3307
+ await releaseReadinessFreeze();
3308
+ publishPlaybackState();
3309
+ }
3310
+ }
3311
+ })();
3312
+ },
4015
3313
  destroy() {
4016
- playbackIntent++;
3314
+ presentationWait += 1;
3315
+ presentationHold = null;
3316
+ beginPlaybackIntent();
4017
3317
  for (const unsubscribe of unsubscribes) {
4018
3318
  unsubscribe();
4019
3319
  }
@@ -4057,6 +3357,240 @@ function isSettling(status, seeking) {
4057
3357
  return seeking || status === "SEEKING";
4058
3358
  }
4059
3359
 
3360
+ /**
3361
+ * Holds the newest producer frame until both the scene and its readiness guard
3362
+ * can accept it. A superseding frame aborts the old guard and closes the old
3363
+ * VideoFrame, so stale readiness can never authorize presentation after a new
3364
+ * seek. The downstream handler owns a frame only after the guard succeeds.
3365
+ */
3366
+ function createProtectedPresentedFrameSource(upstream, onPresentationError = () => undefined) {
3367
+ let downstream = null;
3368
+ let protect = null;
3369
+ let pending = null;
3370
+ let active = null;
3371
+ let navigation = null;
3372
+ let generation = 0;
3373
+ let destroyed = false;
3374
+ let firstPresented = false;
3375
+ let resolveFirstPresentation;
3376
+ let rejectFirstPresentation;
3377
+ const firstPresentation = new Promise((resolve, reject) => {
3378
+ resolveFirstPresentation = resolve;
3379
+ rejectFirstPresentation = reject;
3380
+ });
3381
+ // A retained seed can fail its guard during activate(), before the core has
3382
+ // reached its await. Keep the rejection owned until that caller observes it.
3383
+ void firstPresentation.catch(() => undefined);
3384
+ const closeRun = (run) => {
3385
+ if (run.closed || run.handedOff)
3386
+ return;
3387
+ run.closed = true;
3388
+ run.frame.frame.close();
3389
+ };
3390
+ const frameKey = (frameId) => `${frameId.index}:${frameId.ticks}`;
3391
+ const settleNavigation = (ticket, error) => {
3392
+ if (ticket.settled)
3393
+ return;
3394
+ ticket.settled = true;
3395
+ if (navigation === ticket)
3396
+ navigation = null;
3397
+ if (error === undefined)
3398
+ ticket.resolve();
3399
+ else
3400
+ ticket.reject(error);
3401
+ };
3402
+ const acceptNavigationFrame = (frameId) => {
3403
+ const ticket = navigation;
3404
+ if (!ticket || ticket.settled)
3405
+ return;
3406
+ const accepted = frameKey(frameId);
3407
+ ticket.accepted.add(accepted);
3408
+ if (ticket.target === accepted)
3409
+ settleNavigation(ticket);
3410
+ };
3411
+ const failPresentation = (error) => {
3412
+ if (!firstPresented)
3413
+ rejectFirstPresentation(error);
3414
+ if (navigation)
3415
+ settleNavigation(navigation, error);
3416
+ if (firstPresented)
3417
+ onPresentationError(error);
3418
+ };
3419
+ const cancelActive = () => {
3420
+ const run = active;
3421
+ if (!run)
3422
+ return;
3423
+ active = null;
3424
+ run.controller.abort();
3425
+ closeRun(run);
3426
+ };
3427
+ const pump = () => {
3428
+ if (destroyed || active || !pending || !downstream || !protect)
3429
+ return;
3430
+ const frame = pending;
3431
+ pending = null;
3432
+ const controller = new AbortController();
3433
+ const run = {
3434
+ closed: false,
3435
+ controller,
3436
+ frame,
3437
+ generation,
3438
+ handedOff: false,
3439
+ };
3440
+ active = run;
3441
+ const handOff = () => {
3442
+ if (destroyed ||
3443
+ controller.signal.aborted ||
3444
+ run.generation !== generation) {
3445
+ closeRun(run);
3446
+ return;
3447
+ }
3448
+ if (active === run)
3449
+ active = null;
3450
+ run.handedOff = true;
3451
+ try {
3452
+ const acknowledge = frame.acknowledgePresentation;
3453
+ downstream(acknowledge
3454
+ ? {
3455
+ ...frame,
3456
+ acknowledgePresentation: () => {
3457
+ if (destroyed ||
3458
+ controller.signal.aborted ||
3459
+ run.generation !== generation) {
3460
+ return;
3461
+ }
3462
+ acknowledge();
3463
+ },
3464
+ }
3465
+ : frame);
3466
+ }
3467
+ catch (error) {
3468
+ failPresentation(error);
3469
+ return;
3470
+ }
3471
+ acceptNavigationFrame(frame.frameId);
3472
+ if (!firstPresented) {
3473
+ firstPresented = true;
3474
+ resolveFirstPresentation();
3475
+ }
3476
+ };
3477
+ let guarded;
3478
+ try {
3479
+ guarded = protect(frame, controller.signal);
3480
+ }
3481
+ catch (error) {
3482
+ active = null;
3483
+ closeRun(run);
3484
+ failPresentation(error);
3485
+ pump();
3486
+ return;
3487
+ }
3488
+ if (!guarded) {
3489
+ handOff();
3490
+ pump();
3491
+ return;
3492
+ }
3493
+ void guarded
3494
+ .then(() => {
3495
+ handOff();
3496
+ })
3497
+ .catch((error) => {
3498
+ closeRun(run);
3499
+ if (!controller.signal.aborted)
3500
+ failPresentation(error);
3501
+ })
3502
+ .finally(() => {
3503
+ if (active === run)
3504
+ active = null;
3505
+ pump();
3506
+ });
3507
+ };
3508
+ upstream.onPresentedFrame((presented) => {
3509
+ if (destroyed) {
3510
+ presented.frame.close();
3511
+ return;
3512
+ }
3513
+ generation += 1;
3514
+ cancelActive();
3515
+ pending?.frame.close();
3516
+ pending = presented;
3517
+ pump();
3518
+ });
3519
+ return {
3520
+ source: {
3521
+ onPresentedFrame(handler) {
3522
+ downstream = handler;
3523
+ pump();
3524
+ },
3525
+ },
3526
+ activate(nextProtect) {
3527
+ protect = nextProtect;
3528
+ pump();
3529
+ },
3530
+ invalidate() {
3531
+ generation += 1;
3532
+ cancelActive();
3533
+ pending?.frame.close();
3534
+ pending = null;
3535
+ if (navigation)
3536
+ settleNavigation(navigation);
3537
+ },
3538
+ beginNavigation() {
3539
+ generation += 1;
3540
+ cancelActive();
3541
+ pending?.frame.close();
3542
+ pending = null;
3543
+ if (navigation)
3544
+ settleNavigation(navigation);
3545
+ let resolve;
3546
+ let reject;
3547
+ const promise = new Promise((onResolve, onReject) => {
3548
+ resolve = onResolve;
3549
+ reject = onReject;
3550
+ });
3551
+ // A scene may fail before the transport reaches waitFor(). Keep the
3552
+ // rejection owned until the navigation command observes it.
3553
+ void promise.catch(() => undefined);
3554
+ const ticket = {
3555
+ accepted: new Set(),
3556
+ promise,
3557
+ reject,
3558
+ resolve,
3559
+ settled: false,
3560
+ target: null,
3561
+ };
3562
+ navigation = ticket;
3563
+ return {
3564
+ waitFor(frameId) {
3565
+ if (ticket.settled)
3566
+ return ticket.promise;
3567
+ ticket.target = frameKey(frameId);
3568
+ if (ticket.accepted.has(ticket.target))
3569
+ settleNavigation(ticket);
3570
+ return ticket.promise;
3571
+ },
3572
+ cancel() {
3573
+ settleNavigation(ticket);
3574
+ },
3575
+ };
3576
+ },
3577
+ waitForFirstPresentation: () => firstPresentation,
3578
+ destroy() {
3579
+ if (destroyed)
3580
+ return;
3581
+ destroyed = true;
3582
+ generation += 1;
3583
+ cancelActive();
3584
+ pending?.frame.close();
3585
+ pending = null;
3586
+ if (navigation)
3587
+ settleNavigation(navigation);
3588
+ upstream.onPresentedFrame((presented) => presented.frame.close());
3589
+ if (!firstPresented)
3590
+ resolveFirstPresentation();
3591
+ },
3592
+ };
3593
+ }
4060
3594
  /**
4061
3595
  * Reads the presented-frame plane an opened media source publishes as `engine`.
4062
3596
  * A source without one is pull-only, and the renderer keeps driving it by
@@ -4081,6 +3615,45 @@ function isPresentedFrameChannel(value) {
4081
3615
  typeof candidate.subscribe === "function");
4082
3616
  }
4083
3617
 
3618
+ const MILLISECONDS_PER_SECOND$1 = 1000;
3619
+ /**
3620
+ * Two seconds of frozen picture for masks that have not been cooked.
3621
+ *
3622
+ * Preparation is local work, so a lead it cannot rebuild in this long is one it
3623
+ * is losing rather than one that is late, and every further second is spent on
3624
+ * a picture that has stopped. The detection gate waits five times as long
3625
+ * because what it waits on is a model somewhere else.
3626
+ */
3627
+ function resolveRenderPreparationMaxWaitSeconds(value) {
3628
+ if (value === undefined) {
3629
+ return DEFAULT_RENDER_PREPARATION_GATE_MAX_WAIT_SECONDS;
3630
+ }
3631
+ if (value === Number.POSITIVE_INFINITY)
3632
+ return value;
3633
+ if (Number.isNaN(value)) {
3634
+ return DEFAULT_RENDER_PREPARATION_GATE_MAX_WAIT_SECONDS;
3635
+ }
3636
+ return Math.max(0, value);
3637
+ }
3638
+ /**
3639
+ * A tenth of a second of runway left, and a fifth of a second more banked
3640
+ * before the picture moves again.
3641
+ *
3642
+ * A viewer counts a stop in their own seconds rather than seconds of timeline,
3643
+ * so both are wall-clock and the playback rate converts them. The resume is
3644
+ * the stop plus a margin, which fixes the length of a stop at that margin: how
3645
+ * deep a bank preparation aims for is then a question about memory and cooks
3646
+ * alone.
3647
+ */
3648
+ /**
3649
+ * Ceiling on a stop threshold as a share of the lead that ends the stop.
3650
+ *
3651
+ * A requirement too small to fund the margin caps the resume lead below the
3652
+ * stop threshold the wall clock asks for, and a gate that stops and resumes at
3653
+ * one lead stops again on the frame it just released. Half the resume lead is
3654
+ * the widest stop threshold that still leaves a stop something to wait for.
3655
+ */
3656
+ const RENDER_PREPARATION_GATE_MAX_STOP_SHARE_OF_RESUME = 0.5;
4084
3657
  async function createMediaRendererCore(options, providers) {
4085
3658
  const fit = options.fit ?? MediaRendererFit.Contain;
4086
3659
  const initialPlaybackRate = options.playbackRate ?? 1;
@@ -4119,24 +3692,29 @@ async function createMediaRendererCore(options, providers) {
4119
3692
  mediaScene?.setPlaybackActive?.(runtimeState.isPlaybackActive() || isSeekGestureInFlight);
4120
3693
  };
4121
3694
  const endSeekGesture = () => {
3695
+ cancelPullScrubPreview();
4122
3696
  isSeekGestureInFlight = false;
4123
3697
  publishPlaybackActivity();
4124
3698
  };
3699
+ /**
3700
+ * A hold reads the rate once, so the thresholds it is waiting on are the
3701
+ * ones the old rate implied. Ending the hold hands the next frame back to
3702
+ * the gate, which asks again at the rate now in force.
3703
+ */
3704
+ let renderPreparationRateEpoch = new AbortController();
4125
3705
  const adoptPlaybackRate = (playbackRate) => {
3706
+ if (playbackRate === runtimeState.playbackRate()) {
3707
+ return;
3708
+ }
4126
3709
  runtimeState.setPlaybackRate(playbackRate);
3710
+ const supersededHolds = renderPreparationRateEpoch;
3711
+ renderPreparationRateEpoch = new AbortController();
3712
+ supersededHolds.abort();
4127
3713
  };
4128
- let presentsOwnFrames = false;
4129
3714
  const runtimeState = createMediaRendererRuntimeState({
4130
3715
  fit,
4131
3716
  playbackRate: initialPlaybackRate,
4132
- getPlaybackGateReach: () => !shouldGatePlayback
4133
- ? PlaybackGateReach.Off
4134
- : // Stopping a producer that owns the playhead is something only the
4135
- // detection gate does, so a self-presenting source held for render
4136
- // preparation alone still waits once and never again.
4137
- presentsOwnFrames && !shouldGateDetectionPlayback
4138
- ? PlaybackGateReach.StartOfPlayback
4139
- : PlaybackGateReach.EveryFrame,
3717
+ getPlaybackGateReach: () => shouldGatePlayback ? PlaybackGateReach.EveryFrame : PlaybackGateReach.Off,
4140
3718
  getDetectionBufferState: () => detectionTimeline?.getState() ?? createIdleDetectionBufferState(),
4141
3719
  onFrame: options.onFrame,
4142
3720
  onSource: options.onSource,
@@ -4149,9 +3727,44 @@ async function createMediaRendererCore(options, providers) {
4149
3727
  let mediaInput;
4150
3728
  let playbackController;
4151
3729
  let transport;
3730
+ let protectedPresentedFrames;
3731
+ let pushPresentationReady = false;
4152
3732
  let sampleSink;
4153
3733
  let firstTimestamp = 0;
4154
3734
  let navigationVersion = 0;
3735
+ let outstandingSourceReads = 0;
3736
+ let pendingPullScrubTime = null;
3737
+ let pullScrubEpoch = 0;
3738
+ let pullScrubReadInFlight = false;
3739
+ let pullScrubScheduled = false;
3740
+ const cancelPullScrubPreview = () => {
3741
+ pendingPullScrubTime = null;
3742
+ pullScrubEpoch += 1;
3743
+ if (pullScrubReadInFlight) {
3744
+ navigationVersion += 1;
3745
+ }
3746
+ };
3747
+ const beginSourceRead = () => {
3748
+ outstandingSourceReads += 1;
3749
+ if (outstandingSourceReads === 1) {
3750
+ runtimeState.setSourceAwaitingRead(true);
3751
+ }
3752
+ };
3753
+ const endSourceRead = () => {
3754
+ outstandingSourceReads -= 1;
3755
+ if (outstandingSourceReads === 0) {
3756
+ runtimeState.setSourceAwaitingRead(false);
3757
+ }
3758
+ };
3759
+ const trackSourceRead = async (read) => {
3760
+ beginSourceRead();
3761
+ try {
3762
+ return await read;
3763
+ }
3764
+ finally {
3765
+ endSourceRead();
3766
+ }
3767
+ };
4155
3768
  const adoptTransportPlaybackState = (state) => {
4156
3769
  // Not gated on isError: the producer owns playback truth here, so a
4157
3770
  // recovery after a transient error must be adopted, not ignored forever.
@@ -4169,7 +3782,8 @@ async function createMediaRendererCore(options, providers) {
4169
3782
  runtimeState.setPaused();
4170
3783
  break;
4171
3784
  case MediaRendererPlaybackState.Ready:
4172
- runtimeState.setReady();
3785
+ if (pushPresentationReady)
3786
+ runtimeState.setReady();
4173
3787
  break;
4174
3788
  case MediaRendererPlaybackState.Loading:
4175
3789
  runtimeState.setLoading();
@@ -4194,36 +3808,197 @@ async function createMediaRendererCore(options, providers) {
4194
3808
  const shouldGateDetectionPlayback = detectionPlaybackGate?.enabled === true;
4195
3809
  const shouldGateRenderPreparationPlayback = renderPreparationPlaybackGate?.enabled === true;
4196
3810
  const shouldGatePlayback = shouldGateDetectionPlayback || shouldGateRenderPreparationPlayback;
4197
- const waitForPlaybackReadiness = async (mediaTime) => {
3811
+ const renderPreparationGateMaxWaitSeconds = resolveRenderPreparationMaxWaitSeconds(renderPreparationPlaybackGate?.maxWaitSeconds);
3812
+ /**
3813
+ * The gate's wall-clock thresholds in seconds of timeline, which is the unit
3814
+ * a prepared lead is measured in. A rate is how many seconds of timeline a
3815
+ * second of runway is worth, so at 8x a tenth of a second of picture is
3816
+ * eight tenths of a second of frames.
3817
+ *
3818
+ * `requiredAheadSeconds` is a ceiling on the resume lead, and the stop
3819
+ * threshold is held under that lead: a requirement of a single frame still
3820
+ * buys a stop that ends higher than it began, at the price of a band
3821
+ * narrower than the wall clock asked for. Zero on both sides is the one pair
3822
+ * that meets, and there the lead gate is off and only an unprepared frame
3823
+ * under the playhead stops the picture.
3824
+ */
3825
+ const resolveRenderPreparationGateThresholds = () => {
3826
+ const playbackRate = runtimeState.playbackRate();
3827
+ const bankSeconds = Math.max(renderPreparationPlaybackGate?.requiredAheadSeconds ?? 0, 0);
3828
+ const stopBelowWallSeconds = Math.max(renderPreparationPlaybackGate?.stopBelowWallSeconds ??
3829
+ DEFAULT_RENDER_PREPARATION_GATE_STOP_BELOW_WALL_SECONDS, 0);
3830
+ const resumeMarginWallSeconds = Math.max(renderPreparationPlaybackGate?.resumeMarginWallSeconds ??
3831
+ DEFAULT_RENDER_PREPARATION_GATE_RESUME_MARGIN_WALL_SECONDS, 0);
3832
+ const resumeAtSeconds = Math.min(bankSeconds, (stopBelowWallSeconds + resumeMarginWallSeconds) * playbackRate);
3833
+ return {
3834
+ enabled: renderPreparationPlaybackGate?.enabled,
3835
+ resumeAtSeconds,
3836
+ stopBelowSeconds: Math.min(stopBelowWallSeconds * playbackRate, resumeAtSeconds * RENDER_PREPARATION_GATE_MAX_STOP_SHARE_OF_RESUME),
3837
+ };
3838
+ };
3839
+ /**
3840
+ * Where preparation stood when the hold that last gave up opened, or null
3841
+ * while the gate is armed. Preparation that has finished a frame since is
3842
+ * preparation that is running, however far behind the playhead it still is,
3843
+ * and the gate is worth spending again on it. The frame that says so lands
3844
+ * during the hold on a machine where cooking and decoding contend for one
3845
+ * core, which is why the mark is the hold's start and not its end.
3846
+ */
3847
+ let abandonedGateProgress = null;
3848
+ const readRenderPreparationProgress = () => mediaScene?.getRenderPreparationProgress?.() ?? 0;
3849
+ const hasAbandonedRenderPreparationGate = () => abandonedGateProgress !== null &&
3850
+ readRenderPreparationProgress() <= abandonedGateProgress;
3851
+ const armRenderPreparationGate = () => {
3852
+ abandonedGateProgress = null;
3853
+ runtimeState.setRenderPreparationGateAbandoned(false);
3854
+ };
3855
+ const abandonRenderPreparationGate = (progressAtHoldStart) => {
3856
+ abandonedGateProgress = progressAtHoldStart;
3857
+ runtimeState.setRenderPreparationGateAbandoned(true);
3858
+ };
3859
+ /**
3860
+ * Holds the picture for prepared artifacts, under the gate's bound.
3861
+ *
3862
+ * Preparation that has fallen behind is indistinguishable from a cook still
3863
+ * on its way, and the bound is the way back out of a hold on one that will
3864
+ * never answer. Giving up lasts only until preparation finishes another
3865
+ * frame: a preparer that is merely losing to the playhead is still a
3866
+ * preparer, and the picture it feeds is worth stopping for again. One that
3867
+ * has finished nothing since is stuck, and the gate stays out of its way
3868
+ * rather than spending the bound once a frame on it.
3869
+ */
3870
+ const waitForRenderPreparationWithin = async (mediaTime, signal) => {
3871
+ const bound = new AbortController();
3872
+ const abortBound = () => bound.abort();
3873
+ const rateEpoch = renderPreparationRateEpoch.signal;
3874
+ const progressAtHoldStart = readRenderPreparationProgress();
3875
+ const expiry = Number.isFinite(renderPreparationGateMaxWaitSeconds)
3876
+ ? setTimeout(() => {
3877
+ abandonRenderPreparationGate(progressAtHoldStart);
3878
+ bound.abort();
3879
+ }, renderPreparationGateMaxWaitSeconds * MILLISECONDS_PER_SECOND$1)
3880
+ : undefined;
3881
+ signal.addEventListener("abort", abortBound);
3882
+ rateEpoch.addEventListener("abort", abortBound);
3883
+ try {
3884
+ await mediaScene?.waitForRenderPreparation?.(mediaTime, resolveRenderPreparationGateThresholds(), bound.signal);
3885
+ }
3886
+ finally {
3887
+ clearTimeout(expiry);
3888
+ signal.removeEventListener("abort", abortBound);
3889
+ rateEpoch.removeEventListener("abort", abortBound);
3890
+ }
3891
+ };
3892
+ const waitForDetectionCoverage = (mediaTime) => detectionTimeline?.prepare(mediaTime, {
3893
+ duration: runtimeState.duration(),
3894
+ firstTimestamp,
3895
+ gatePlayback: true,
3896
+ });
3897
+ /**
3898
+ * The wait a play makes before the picture starts moving. A play is a fresh
3899
+ * intent, so it arms a gate an earlier run gave up on: the frame the viewer
3900
+ * is about to be shown is the one they asked to see annotated.
3901
+ */
3902
+ const waitForPlaybackReadiness = async (mediaTime, signal) => {
4198
3903
  if (shouldGateDetectionPlayback) {
4199
- await detectionTimeline?.prepare(mediaTime, {
4200
- duration: runtimeState.duration(),
4201
- firstTimestamp,
4202
- gatePlayback: true,
4203
- });
3904
+ await waitForDetectionCoverage(mediaTime);
4204
3905
  }
4205
3906
  if (shouldGateRenderPreparationPlayback) {
4206
- await mediaScene?.waitForRenderPreparation?.(mediaTime, renderPreparationPlaybackGate ?? {});
3907
+ armRenderPreparationGate();
3908
+ await waitForRenderPreparationWithin(mediaTime, signal);
4207
3909
  }
4208
3910
  };
4209
- /**
4210
- * The detection gate alone, because it is the one that bounds its own wait
4211
- * and gives up on a producer that has stopped answering. Render preparation
4212
- * waits until the artifacts exist, which is a wait to start playback and
4213
- * never one to stop it with.
4214
- */
4215
- const holdForDetectionCoverage = (mediaTime) => {
4216
- const prepareOptions = {
4217
- duration: runtimeState.duration(),
4218
- firstTimestamp,
4219
- };
4220
- if (detectionTimeline?.needsPlaybackGateWait?.(mediaTime, prepareOptions) !==
4221
- true) {
3911
+ /**
3912
+ * A gate that gave up comes back on preparation finishing another frame, and
3913
+ * on the playhead reaching frames preparation finished already: a loop or a
3914
+ * seek back lands inside a window that is covered, and coverage cooked before
3915
+ * the hold moves no progress count.
3916
+ */
3917
+ const holdForSampleReadiness = async (mediaTime, signal) => {
3918
+ if (shouldGateDetectionPlayback) {
3919
+ await waitForDetectionCoverage(mediaTime);
3920
+ }
3921
+ if (!shouldGateRenderPreparationPlayback) {
3922
+ return;
3923
+ }
3924
+ const isRenderPreparationCovered = mediaScene?.needsRenderPreparationWait?.(mediaTime, resolveRenderPreparationGateThresholds()) === false;
3925
+ if (!isRenderPreparationCovered && hasAbandonedRenderPreparationGate()) {
3926
+ return;
3927
+ }
3928
+ armRenderPreparationGate();
3929
+ await waitForRenderPreparationWithin(mediaTime, signal);
3930
+ };
3931
+ const holdForDetectionCoverage = (mediaTime) => {
3932
+ if (!detectionTimeline ||
3933
+ (detectionTimeline.needsBufferPrepare?.(mediaTime) !== true &&
3934
+ detectionTimeline.needsPlaybackGateWait?.(mediaTime, {
3935
+ duration: runtimeState.duration(),
3936
+ firstTimestamp,
3937
+ gatePlayback: true,
3938
+ }) !== true)) {
3939
+ return null;
3940
+ }
3941
+ return detectionTimeline.prepare(mediaTime, {
3942
+ duration: runtimeState.duration(),
3943
+ firstTimestamp,
3944
+ gatePlayback: true,
3945
+ });
3946
+ };
3947
+ const holdForRenderPreparation = (mediaTime, signal) => {
3948
+ if (mediaScene?.needsRenderPreparationWait?.(mediaTime, resolveRenderPreparationGateThresholds()) !== true) {
3949
+ armRenderPreparationGate();
3950
+ return null;
3951
+ }
3952
+ if (hasAbandonedRenderPreparationGate())
3953
+ return null;
3954
+ armRenderPreparationGate();
3955
+ return waitForRenderPreparationWithin(mediaTime, signal);
3956
+ };
3957
+ /** Detaches a readiness source that does not implement cancellation itself.
3958
+ * Its eventual rejection remains owned while a superseding frame can start
3959
+ * its own guard immediately. */
3960
+ const abortableReadiness = (wait, signal) => {
3961
+ if (signal.aborted) {
3962
+ void wait.catch(() => undefined);
3963
+ return Promise.resolve();
3964
+ }
3965
+ let onAbort = () => { };
3966
+ const aborted = new Promise((resolve) => {
3967
+ onAbort = resolve;
3968
+ signal.addEventListener("abort", onAbort, { once: true });
3969
+ });
3970
+ void wait.catch(() => undefined);
3971
+ return Promise.race([wait.then(() => undefined), aborted]).finally(() => {
3972
+ signal.removeEventListener("abort", onAbort);
3973
+ });
3974
+ };
3975
+ const holdForPresentationReadiness = (mediaTime, signal) => {
3976
+ const combined = new AbortController();
3977
+ const abortCombined = () => combined.abort();
3978
+ if (signal.aborted)
3979
+ abortCombined();
3980
+ else
3981
+ signal.addEventListener("abort", abortCombined, { once: true });
3982
+ const holds = [];
3983
+ const detection = shouldGateDetectionPlayback
3984
+ ? holdForDetectionCoverage(mediaTime)
3985
+ : null;
3986
+ const preparation = shouldGateRenderPreparationPlayback
3987
+ ? holdForRenderPreparation(mediaTime, combined.signal)
3988
+ : null;
3989
+ if (detection)
3990
+ holds.push(abortableReadiness(detection, combined.signal));
3991
+ if (preparation)
3992
+ holds.push(abortableReadiness(preparation, combined.signal));
3993
+ if (holds.length === 0) {
3994
+ signal.removeEventListener("abort", abortCombined);
4222
3995
  return null;
4223
3996
  }
4224
- return detectionTimeline.prepare(mediaTime, {
4225
- ...prepareOptions,
4226
- gatePlayback: true,
3997
+ return Promise.all(holds)
3998
+ .then(() => undefined)
3999
+ .finally(() => {
4000
+ signal.removeEventListener("abort", abortCombined);
4001
+ combined.abort();
4227
4002
  });
4228
4003
  };
4229
4004
  const presentSample = (sample) => {
@@ -4261,6 +4036,74 @@ async function createMediaRendererCore(options, providers) {
4261
4036
  }
4262
4037
  }
4263
4038
  };
4039
+ const seekPullSample = async (targetTime) => {
4040
+ if (!playbackController || !sampleSink) {
4041
+ throw new Error("Media renderer is not ready.");
4042
+ }
4043
+ // A seek taken while buffering should resume playback, not strand it:
4044
+ // buffering means playback was requested and is waiting for data.
4045
+ const wasPlaying = runtimeState.isPlaybackActive();
4046
+ const requestVersion = ++navigationVersion;
4047
+ playbackController.pause();
4048
+ try {
4049
+ const sample = await trackSourceRead(sampleSink.getSample(targetTime, { skipLiveWait: true }));
4050
+ if (!sample) {
4051
+ throw new Error("No decoded video sample was found for seek.");
4052
+ }
4053
+ if (requestVersion !== navigationVersion || runtimeState.isDestroyed()) {
4054
+ sample.close();
4055
+ return;
4056
+ }
4057
+ await prepareAndPresentSample(sample, () => requestVersion === navigationVersion && !runtimeState.isDestroyed());
4058
+ if (requestVersion !== navigationVersion || runtimeState.isDestroyed()) {
4059
+ return;
4060
+ }
4061
+ playbackController.seek(runtimeState.currentTime());
4062
+ if (wasPlaying) {
4063
+ runtimeState.setPlaying();
4064
+ playbackController.play();
4065
+ }
4066
+ else if (runtimeState.isBuffering()) {
4067
+ // Seeking always leaves the controller paused. Settle the reported
4068
+ // state so the session is paused rather than perpetually buffering.
4069
+ runtimeState.setPaused();
4070
+ }
4071
+ }
4072
+ catch (error) {
4073
+ if (requestVersion !== navigationVersion || runtimeState.isDestroyed()) {
4074
+ return;
4075
+ }
4076
+ runtimeState.setRenderError(error);
4077
+ throw error;
4078
+ }
4079
+ };
4080
+ const schedulePullScrubPreview = () => {
4081
+ if (pullScrubScheduled ||
4082
+ pullScrubReadInFlight ||
4083
+ pendingPullScrubTime === null) {
4084
+ return;
4085
+ }
4086
+ pullScrubScheduled = true;
4087
+ const scheduledEpoch = pullScrubEpoch;
4088
+ queueMicrotask(() => {
4089
+ pullScrubScheduled = false;
4090
+ if (scheduledEpoch !== pullScrubEpoch ||
4091
+ pendingPullScrubTime === null ||
4092
+ runtimeState.isDestroyed()) {
4093
+ schedulePullScrubPreview();
4094
+ return;
4095
+ }
4096
+ const targetTime = pendingPullScrubTime;
4097
+ pendingPullScrubTime = null;
4098
+ pullScrubReadInFlight = true;
4099
+ void seekPullSample(targetTime)
4100
+ .catch(() => undefined)
4101
+ .finally(() => {
4102
+ pullScrubReadInFlight = false;
4103
+ schedulePullScrubPreview();
4104
+ });
4105
+ });
4106
+ };
4264
4107
  const renderer = {
4265
4108
  async play() {
4266
4109
  if (runtimeState.isDestroyed()) {
@@ -4283,6 +4126,10 @@ async function createMediaRendererCore(options, providers) {
4283
4126
  if (runtimeState.isPlaying()) {
4284
4127
  return;
4285
4128
  }
4129
+ // A source the renderer pulls samples from has no readiness wait of its
4130
+ // own: its start of playback is the first per-sample hold, so the fresh
4131
+ // intent arms the gate here.
4132
+ armRenderPreparationGate();
4286
4133
  runtimeState.setPlaying();
4287
4134
  playbackController.play();
4288
4135
  },
@@ -4333,50 +4180,7 @@ async function createMediaRendererCore(options, providers) {
4333
4180
  await transport.commit(targetTime);
4334
4181
  return;
4335
4182
  }
4336
- if (!playbackController || !sampleSink) {
4337
- throw new Error("Media renderer is not ready.");
4338
- }
4339
- // A seek taken while buffering should resume playback, not strand it:
4340
- // buffering means playback was requested and is waiting for data.
4341
- const wasPlaying = runtimeState.isPlaybackActive();
4342
- const requestVersion = ++navigationVersion;
4343
- playbackController.pause();
4344
- try {
4345
- const sample = await sampleSink.getSample(targetTime, {
4346
- skipLiveWait: true,
4347
- });
4348
- if (!sample) {
4349
- throw new Error("No decoded video sample was found for seek.");
4350
- }
4351
- if (requestVersion !== navigationVersion ||
4352
- runtimeState.isDestroyed()) {
4353
- sample.close();
4354
- return;
4355
- }
4356
- await prepareAndPresentSample(sample, () => requestVersion === navigationVersion && !runtimeState.isDestroyed());
4357
- if (requestVersion !== navigationVersion ||
4358
- runtimeState.isDestroyed()) {
4359
- return;
4360
- }
4361
- playbackController.seek(runtimeState.currentTime());
4362
- if (wasPlaying) {
4363
- runtimeState.setPlaying();
4364
- playbackController.play();
4365
- }
4366
- else if (runtimeState.isBuffering()) {
4367
- // Seeking always leaves the controller paused. Settle the reported
4368
- // state so the session is paused rather than perpetually buffering.
4369
- runtimeState.setPaused();
4370
- }
4371
- }
4372
- catch (error) {
4373
- if (requestVersion !== navigationVersion ||
4374
- runtimeState.isDestroyed()) {
4375
- return;
4376
- }
4377
- runtimeState.setRenderError(error);
4378
- throw error;
4379
- }
4183
+ await seekPullSample(targetTime);
4380
4184
  },
4381
4185
  scrub(mediaTime) {
4382
4186
  if (runtimeState.isDestroyed() || runtimeState.isError()) {
@@ -4393,7 +4197,11 @@ async function createMediaRendererCore(options, providers) {
4393
4197
  transport.scrub(targetTime);
4394
4198
  return;
4395
4199
  }
4396
- void renderer.seek(targetTime).catch(() => undefined);
4200
+ pendingPullScrubTime = targetTime;
4201
+ if (pullScrubReadInFlight) {
4202
+ navigationVersion += 1;
4203
+ }
4204
+ schedulePullScrubPreview();
4397
4205
  },
4398
4206
  async stepForward() {
4399
4207
  await stepToAdjacentSample("forward");
@@ -4429,7 +4237,7 @@ async function createMediaRendererCore(options, providers) {
4429
4237
  if (!mediaScene) {
4430
4238
  throw new Error("Media renderer is not ready.");
4431
4239
  }
4432
- const mediaTime = runtimeState.currentTime();
4240
+ const mediaTime = runtimeState.presentedTime() ?? runtimeState.currentTime();
4433
4241
  const requestVersion = navigationVersion;
4434
4242
  await detectionTimeline?.prepare(mediaTime, {
4435
4243
  duration: runtimeState.duration(),
@@ -4475,7 +4283,8 @@ async function createMediaRendererCore(options, providers) {
4475
4283
  return;
4476
4284
  }
4477
4285
  currentPresentation = resolvePresentation(presentation);
4478
- const presentedSample = mediaScene?.setPresentation(currentPresentation, runtimeState.currentTime());
4286
+ const mediaTime = runtimeState.presentedTime() ?? runtimeState.currentTime();
4287
+ const presentedSample = mediaScene?.setPresentation(currentPresentation, mediaTime);
4479
4288
  if (presentedSample) {
4480
4289
  runtimeState.recordPresentationUpdate(presentedSample);
4481
4290
  }
@@ -4527,7 +4336,9 @@ async function createMediaRendererCore(options, providers) {
4527
4336
  if (runtimeState.isDestroyed()) {
4528
4337
  return;
4529
4338
  }
4339
+ cancelPullScrubPreview();
4530
4340
  runtimeState.markDestroyed();
4341
+ protectedPresentedFrames?.destroy();
4531
4342
  transport?.destroy();
4532
4343
  playbackController?.destroy();
4533
4344
  stopActiveIterator();
@@ -4571,7 +4382,18 @@ async function createMediaRendererCore(options, providers) {
4571
4382
  ...options.detectionBuffer,
4572
4383
  });
4573
4384
  const presentedFrameChannel = resolvePresentedFrameChannel(mediaSource);
4574
- presentsOwnFrames = presentedFrameChannel !== null;
4385
+ protectedPresentedFrames = presentedFrameChannel
4386
+ ? createProtectedPresentedFrameSource(presentedFrameChannel, (error) => {
4387
+ // A scene that can no longer accept pixels cannot recover because
4388
+ // the producer keeps running. Cut off future frames and state
4389
+ // signals before publishing the rendering failure.
4390
+ protectedPresentedFrames?.destroy();
4391
+ transport?.destroy();
4392
+ presentedFrameChannel.pause();
4393
+ if (!runtimeState.isDestroyed())
4394
+ runtimeState.setRenderError(error);
4395
+ })
4396
+ : undefined;
4575
4397
  const mediaDimensions = runtimeState.recordMediaMetadata(metadata);
4576
4398
  mediaScene = await providers.createScene({
4577
4399
  annotationOverlayStyle: currentPresentation.annotationOverlayStyle,
@@ -4602,7 +4424,7 @@ async function createMediaRendererCore(options, providers) {
4602
4424
  },
4603
4425
  polygonStyle: currentPresentation.polygonStyle,
4604
4426
  polylineStyle: currentPresentation.polylineStyle,
4605
- presentedFrames: presentedFrameChannel ?? undefined,
4427
+ presentedFrames: protectedPresentedFrames?.source,
4606
4428
  regionRenderers: resolveRegionRenderers(currentPresentation),
4607
4429
  previewOverlay: options.previewOverlay,
4608
4430
  renderPreparation: options.renderPreparation
@@ -4648,14 +4470,32 @@ async function createMediaRendererCore(options, providers) {
4648
4470
  waitForReadiness: shouldGatePlayback
4649
4471
  ? waitForPlaybackReadiness
4650
4472
  : undefined,
4651
- holdForReadiness: shouldGateDetectionPlayback
4652
- ? holdForDetectionCoverage
4473
+ waitForPresentationReadiness: shouldGatePlayback
4474
+ ? holdForPresentationReadiness
4653
4475
  : undefined,
4476
+ invalidatePresentedFrame: () => protectedPresentedFrames?.invalidate(),
4477
+ beginPresentedFrameNavigation: () => protectedPresentedFrames.beginNavigation(),
4654
4478
  });
4479
+ protectedPresentedFrames?.activate((presented, signal) => transport.protectPresentation(presented.mediaTimeS, signal));
4655
4480
  if (initialPlaybackRate !== 1) {
4656
4481
  transport.setPlaybackRate(initialPlaybackRate);
4657
4482
  }
4658
4483
  detectionTimeline?.prefetch(metadata.firstTimestamp);
4484
+ // Loading is not complete until the scene has accepted real media
4485
+ // pixels. A paused, non-autoplay source otherwise reports Ready over a
4486
+ // blank compositor because the producer's load settled before a frame
4487
+ // consumer existed.
4488
+ await presentedFrameChannel.commit(metadata.firstTimestamp * MILLISECONDS_PER_SECOND$1);
4489
+ await protectedPresentedFrames?.waitForFirstPresentation();
4490
+ // The first accepted frame does not prove the presentation path stayed
4491
+ // healthy until initialization finished. A producer may emit a newer
4492
+ // replacement before commit() resolves, and that handoff can fail after
4493
+ // the first-presentation latch has already opened. Preserve that fatal
4494
+ // state instead of publishing Ready over it or starting autoplay on a
4495
+ // producer the error path just stopped.
4496
+ if (runtimeState.isDestroyed() || runtimeState.isError())
4497
+ return renderer;
4498
+ pushPresentationReady = true;
4659
4499
  runtimeState.setReady();
4660
4500
  if (options.autoPlay ?? true) {
4661
4501
  await renderer.play();
@@ -4672,7 +4512,7 @@ async function createMediaRendererCore(options, providers) {
4672
4512
  await prepareAndPresentSample(firstSample);
4673
4513
  runtimeState.setReady();
4674
4514
  const waitForSample = shouldGatePlayback
4675
- ? (sample) => waitForPlaybackReadiness(sample.timestamp)
4515
+ ? (sample, signal) => holdForSampleReadiness(sample.timestamp, signal)
4676
4516
  : undefined;
4677
4517
  playbackController = createMediaPlaybackController({
4678
4518
  duration: runtimeState.duration(),
@@ -4692,6 +4532,14 @@ async function createMediaRendererCore(options, providers) {
4692
4532
  runtimeState.setPlaying();
4693
4533
  }
4694
4534
  },
4535
+ onSourceWait(waiting) {
4536
+ if (waiting) {
4537
+ beginSourceRead();
4538
+ }
4539
+ else {
4540
+ endSourceRead();
4541
+ }
4542
+ },
4695
4543
  onWaiting() {
4696
4544
  if (runtimeState.isPlaybackActive()) {
4697
4545
  runtimeState.setBuffering();
@@ -4736,14 +4584,16 @@ async function createMediaRendererCore(options, providers) {
4736
4584
  let sample = null;
4737
4585
  try {
4738
4586
  if (direction === "backward") {
4739
- sample = await sampleSink.getSample(Math.max(firstTimestamp, currentTime - epsilon), { skipLiveWait: true });
4587
+ sample = await trackSourceRead(sampleSink.getSample(Math.max(firstTimestamp, currentTime - epsilon), {
4588
+ skipLiveWait: true,
4589
+ }));
4740
4590
  }
4741
4591
  else {
4742
4592
  const iterator = sampleSink.samples(currentTime + epsilon, undefined, {
4743
4593
  skipLiveWait: true,
4744
4594
  });
4745
4595
  try {
4746
- const result = await iterator.next();
4596
+ const result = await trackSourceRead(iterator.next());
4747
4597
  sample = result.done ? null : result.value;
4748
4598
  }
4749
4599
  finally {
@@ -4873,7 +4723,7 @@ function encodeCanvas(canvas, type, quality) {
4873
4723
  * silently putting annotations from one moment over pixels from another.
4874
4724
  */
4875
4725
  /**
4876
- * The atomic present: one frame the producer put on screen becomes one screen.
4726
+ * The atomic present: one frame handed to the host becomes one composited screen.
4877
4727
  *
4878
4728
  * The media time is the one the producer published for the frame these pixels
4879
4729
  * are, and it is the only time any step below sees. Nothing here awaits, so no
@@ -4893,6 +4743,7 @@ function presentVideoFrame(presented, targets) {
4893
4743
  const { boxState, regionState } = drawFramePresentLayers(layers, mediaTime, presented);
4894
4744
  targets.render();
4895
4745
  targets.completePresentation(mediaTime, boxState, regionState);
4746
+ presented.acknowledgePresentation?.();
4896
4747
  }
4897
4748
  finally {
4898
4749
  presented.frame.close();
@@ -5079,18 +4930,33 @@ function createIdMaskRasterFrame(instructions, maxRasterWidth) {
5079
4930
  return undefined;
5080
4931
  }
5081
4932
  }
4933
+ /**
4934
+ * Whether the palette has a slot for every detection this frame masks. An id
4935
+ * names a detection by its index in the frame, so one masked detection past the
4936
+ * last slot leaves the whole frame without an id raster however few masks it
4937
+ * carries.
4938
+ */
4939
+ function canIdMaskPaletteNameFrame(instructions) {
4940
+ return instructions.every((instruction) => instruction.visible === false ||
4941
+ resolveIdMaskPaletteId(instruction.detectionIndex) !== undefined);
4942
+ }
5082
4943
  function createIdMaskPlane(instructions, maxRasterWidth) {
4944
+ // The plane carries the ids a failed cook could not. A frame the palette
4945
+ // cannot name has none to carry, and cooking it again reaches the same
4946
+ // refusal after rasterizing every mask a second time.
4947
+ if (!canIdMaskPaletteNameFrame(instructions)) {
4948
+ return undefined;
4949
+ }
5083
4950
  const frame = createIdMaskRasterFrame(instructions, maxRasterWidth);
5084
4951
  return frame
5085
4952
  ? { data: frame.data, height: frame.height, width: frame.width }
5086
4953
  : undefined;
5087
4954
  }
5088
4955
  function compositeInstruction(rgba, canvasWidth, instruction) {
5089
- const decodedMask = decodeCompressedRleMask(instruction.mask);
5090
4956
  const fill = resolveRgbaColor(instruction.color, instruction.alpha);
5091
- compositeMaskFill(rgba, canvasWidth, decodedMask, fill);
5092
- if (instruction.stroke) {
5093
- compositeMaskStroke(rgba, canvasWidth, decodedMask, instruction.stroke);
4957
+ const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);
4958
+ if (instruction.stroke && bounds) {
4959
+ compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);
5094
4960
  }
5095
4961
  }
5096
4962
  function materializeMaskInstructions(instructions) {
@@ -5110,25 +4976,59 @@ function materializeMaskInstructions(instructions) {
5110
4976
  };
5111
4977
  });
5112
4978
  }
5113
- function compositeMaskFill(rgba, canvasWidth, decodedMask, fill) {
5114
- for (let y = 0; y < decodedMask.height; y += 1) {
5115
- for (let x = 0; x < decodedMask.width; x += 1) {
5116
- const maskOffset = y * decodedMask.width + x;
5117
- if (!decodedMask.data[maskOffset]) {
5118
- continue;
4979
+ /**
4980
+ * Compressed RLE counts runs down each column in turn, so a foreground run is
4981
+ * a contiguous walk down one column that wraps into the next.
4982
+ */
4983
+ function compositeMaskFill(rgba, canvasWidth, mask, fill) {
4984
+ const counts = decodeCompressedRleCounts(mask.counts);
4985
+ const maskWidth = mask.width;
4986
+ const maskHeight = mask.height;
4987
+ const maskArea = maskWidth * maskHeight;
4988
+ let maskOffset = 0;
4989
+ let minX = maskWidth;
4990
+ let minY = maskHeight;
4991
+ let maxX = -1;
4992
+ let maxY = -1;
4993
+ for (let index = 0; index < counts.length; index += 1) {
4994
+ const runLength = counts[index] ?? 0;
4995
+ if (index % 2 === 0 || runLength <= 0) {
4996
+ maskOffset += runLength;
4997
+ continue;
4998
+ }
4999
+ let columnX = Math.floor(maskOffset / maskHeight);
5000
+ let columnY = maskOffset - columnX * maskHeight;
5001
+ for (let step = 0; step < runLength; step += 1) {
5002
+ // A run can outlast the columns the mask has; the pixel it then names
5003
+ // is wherever that column-major offset lands when read back row-major.
5004
+ const rowMajorOffset = columnY * maskWidth + columnX;
5005
+ if (rowMajorOffset < maskArea) {
5006
+ const x = rowMajorOffset % maskWidth;
5007
+ const y = (rowMajorOffset - x) / maskWidth;
5008
+ writePixel(rgba, canvasWidth, x, y, fill);
5009
+ minX = Math.min(minX, x);
5010
+ minY = Math.min(minY, y);
5011
+ maxX = Math.max(maxX, x);
5012
+ maxY = Math.max(maxY, y);
5013
+ }
5014
+ columnY += 1;
5015
+ if (columnY === maskHeight) {
5016
+ columnY = 0;
5017
+ columnX += 1;
5119
5018
  }
5120
- writePixel(rgba, canvasWidth, x, y, fill);
5121
5019
  }
5020
+ maskOffset += runLength;
5122
5021
  }
5022
+ return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };
5123
5023
  }
5124
- function compositeMaskStroke(rgba, canvasWidth, decodedMask, stroke) {
5024
+ function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke) {
5125
5025
  const width = Math.round(stroke.width);
5126
5026
  if (width <= 0) {
5127
5027
  return;
5128
5028
  }
5129
5029
  const strokeColor = resolveRgbaColor(stroke.color, stroke.alpha);
5130
- for (let y = 0; y < decodedMask.height; y += 1) {
5131
- for (let x = 0; x < decodedMask.width; x += 1) {
5030
+ for (let y = bounds.minY; y <= bounds.maxY; y += 1) {
5031
+ for (let x = bounds.minX; x <= bounds.maxX; x += 1) {
5132
5032
  if (!isMaskPixel(decodedMask, x, y) ||
5133
5033
  !isBoundaryPixel(decodedMask, x, y)) {
5134
5034
  continue;
@@ -5185,7 +5085,7 @@ function writePixel(rgba, canvasWidth, x, y, color) {
5185
5085
  rgba[rgbaOffset + 3] = color.alpha;
5186
5086
  }
5187
5087
 
5188
- const EMBEDDED_MASK_PREPARATION_WORKER_SOURCE = "(function () {\n 'use strict';\n\n /** COCO-compatible keypoint visibility values. */\n var KeypointVisibility;\n (function (KeypointVisibility) {\n KeypointVisibility[KeypointVisibility[\"NotLabeled\"] = 0] = \"NotLabeled\";\n KeypointVisibility[KeypointVisibility[\"Occluded\"] = 1] = \"Occluded\";\n KeypointVisibility[KeypointVisibility[\"Visible\"] = 2] = \"Visible\";\n })(KeypointVisibility || (KeypointVisibility = {}));\n var DetectionMaskEncoding;\n (function (DetectionMaskEncoding) {\n DetectionMaskEncoding[\"CompressedRle\"] = \"compressedRle\";\n })(DetectionMaskEncoding || (DetectionMaskEncoding = {}));\n\n var DetectionBufferStatus;\n (function (DetectionBufferStatus) {\n DetectionBufferStatus[\"Idle\"] = \"idle\";\n DetectionBufferStatus[\"Loading\"] = \"loading\";\n /**\n * The playback gate is holding playback until the source produces detections\n * for the requested range. {@link DetectionBufferStatus.Loading} fetches\n * detections the source already has; this one waits on a producer.\n */\n DetectionBufferStatus[\"AwaitingCoverage\"] = \"awaitingCoverage\";\n DetectionBufferStatus[\"Ready\"] = \"ready\";\n DetectionBufferStatus[\"Error\"] = \"error\";\n DetectionBufferStatus[\"Destroyed\"] = \"destroyed\";\n })(DetectionBufferStatus || (DetectionBufferStatus = {}));\n /**\n * How the renderer selects the active detection frame for a media timestamp.\n */\n var DetectionFrameSelectionMode;\n (function (DetectionFrameSelectionMode) {\n /**\n * Select the frame whose `[mediaTime, endTime)` interval contains the media\n * time. This is the default for interval annotations and timestamped sources.\n *\n * Frame times must come from the media itself. Times reconstructed from a\n * nominal frame rate drift against a clip whose real rate differs, and once\n * that drift passes the half-millisecond selection tolerance a playhead\n * landing on a frame boundary selects the previous frame's detections.\n *\n * A frame that carries no `endTime` stays active until the next frame starts,\n * which bridges an index the source never produced.\n * {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to\n * one grid step instead, so a missing index reads as no detections.\n */\n DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex` and\n * `frameRate`. This is useful when inference was run on normalized frames and\n * playback should snap detections to that grid.\n *\n * A frame speaks for one grid step starting at its own media time, and an\n * index the source never produced selects nothing rather than a neighbour.\n *\n * A frame carrying no `frameIndex` speaks for its step on the same terms,\n * reached by the time it starts at rather than by an index that names it. Its\n * `endTime` does not widen it past that step, so a source that labels only\n * part of what it writes cannot bridge the indexes it never produced.\n */\n DetectionFrameSelectionMode[\"NearestFrameIndex\"] = \"nearestFrameIndex\";\n })(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));\n var DetectionFrameRetentionMode;\n (function (DetectionFrameRetentionMode) {\n /**\n * Keep writable detections only in an in-memory store. Useful for ephemeral\n * live streams where old predictions should disappear when evicted.\n */\n DetectionFrameRetentionMode[\"MemoryOnly\"] = \"memoryOnly\";\n /**\n * Persist every written detection frame. Useful for finite media where seek\n * and replay should not require recomputing detections.\n */\n DetectionFrameRetentionMode[\"PersistAll\"] = \"persistAll\";\n /**\n * Persist only the most recent retention window. Useful for long-running\n * streams where replay is bounded to a recent time horizon.\n */\n DetectionFrameRetentionMode[\"PersistWindow\"] = \"persistWindow\";\n })(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));\n function decodeCompressedRleMask(mask) {\n if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {\n throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);\n }\n const data = new Uint8Array(mask.width * mask.height);\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n const isForeground = index % 2 === 1;\n if (isForeground) {\n for (let runOffset = 0; runOffset < runLength; runOffset += 1) {\n const maskOffset = offset + runOffset;\n const x = Math.floor(maskOffset / mask.height);\n const y = maskOffset % mask.height;\n const rowMajorOffset = y * mask.width + x;\n if (rowMajorOffset < data.length) {\n data[rowMajorOffset] = 1;\n }\n }\n }\n offset += runLength;\n }\n return {\n data,\n height: mask.height,\n width: mask.width,\n };\n }\n function decodeCompressedRleCounts(counts) {\n const decoded = [];\n let index = 0;\n while (index < counts.length) {\n let value = 0;\n let shift = 0;\n let charCode;\n do {\n charCode = counts.charCodeAt(index) - 48;\n index += 1;\n value |= (charCode & 0x1f) << shift;\n shift += 5;\n } while (charCode & 0x20);\n if (charCode & 0x10) {\n value |= -1 << shift;\n }\n if (decoded.length > 2) {\n value += decoded[decoded.length - 2] ?? 0;\n }\n decoded.push(value);\n }\n return decoded;\n }\n function encodeCompressedRleCounts(counts) {\n return counts\n .map((count, index) => {\n let value = index > 2 ? count - counts[index - 2] : count;\n let encoded = \"\";\n let more = true;\n while (more) {\n let charCode = value & 0x1f;\n value >>= 5;\n more = !((value === 0 && (charCode & 0x10) === 0) ||\n (value === -1 && (charCode & 0x10) !== 0));\n if (more) {\n charCode |= 0x20;\n }\n encoded += String.fromCharCode(charCode + 48);\n }\n return encoded;\n })\n .join(\"\");\n }\n\n var AnnotationFrameMutationKind;\n (function (AnnotationFrameMutationKind) {\n AnnotationFrameMutationKind[\"Add\"] = \"add\";\n AnnotationFrameMutationKind[\"Remove\"] = \"remove\";\n AnnotationFrameMutationKind[\"Replace\"] = \"replace\";\n AnnotationFrameMutationKind[\"Transact\"] = \"transact\";\n AnnotationFrameMutationKind[\"Update\"] = \"update\";\n })(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));\n\n /** Geometry projected into a tracker. The original detection payload is retained. */\n var TrackingGeometry;\n (function (TrackingGeometry) {\n TrackingGeometry[\"Box\"] = \"box\";\n TrackingGeometry[\"Mask\"] = \"mask\";\n TrackingGeometry[\"Keypoints\"] = \"keypoints\";\n })(TrackingGeometry || (TrackingGeometry = {}));\n\n var DetectionMaskPayloadFormat;\n (function (DetectionMaskPayloadFormat) {\n DetectionMaskPayloadFormat[\"RawCocoRle\"] = \"rawCocoRle\";\n DetectionMaskPayloadFormat[\"DeflatedBase64\"] = \"deflatedBase64\";\n })(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));\n function encodeBinaryMask(data, width, height) {\n assertMaskDimensions(data, width, height);\n const runs = [];\n let currentValue = 0;\n let runLength = 0;\n for (let x = 0; x < width; x += 1) {\n for (let y = 0; y < height; y += 1) {\n const value = data[y * width + x] ? 1 : 0;\n if (value === currentValue) {\n runLength += 1;\n }\n else {\n runs.push(runLength);\n currentValue = value;\n runLength = 1;\n }\n }\n }\n runs.push(runLength);\n return {\n counts: encodeCompressedRleCounts(runs),\n encoding: DetectionMaskEncoding.CompressedRle,\n height,\n width,\n };\n }\n function assertMaskSize(width, height) {\n if (!Number.isInteger(width) || width <= 0) {\n throw new Error(\"Mask width must be a positive integer.\");\n }\n if (!Number.isInteger(height) || height <= 0) {\n throw new Error(\"Mask height must be a positive integer.\");\n }\n }\n function assertMaskDimensions(data, width, height) {\n assertMaskSize(width, height);\n if (data.length !== width * height) {\n throw new Error(\"Mask data length must equal width * height.\");\n }\n }\n\n function centerRectToTopLeftRect(rect) {\n return {\n height: rect.height,\n width: rect.width,\n x: rect.x - rect.width / 2,\n y: rect.y - rect.height / 2,\n };\n }\n function getPointsRect(points) {\n if (points.length === 0) {\n return undefined;\n }\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const point of points) {\n minX = Math.min(minX, point.x);\n minY = Math.min(minY, point.y);\n maxX = Math.max(maxX, point.x);\n maxY = Math.max(maxY, point.y);\n }\n return {\n height: maxY - minY,\n width: maxX - minX,\n x: (minX + maxX) / 2,\n y: (minY + maxY) / 2,\n };\n }\n\n var DetectionPickTarget;\n (function (DetectionPickTarget) {\n DetectionPickTarget[\"Box\"] = \"box\";\n DetectionPickTarget[\"Edge\"] = \"edge\";\n DetectionPickTarget[\"Keypoint\"] = \"keypoint\";\n DetectionPickTarget[\"Label\"] = \"label\";\n DetectionPickTarget[\"Mask\"] = \"mask\";\n DetectionPickTarget[\"Polygon\"] = \"polygon\";\n DetectionPickTarget[\"Polyline\"] = \"polyline\";\n })(DetectionPickTarget || (DetectionPickTarget = {}));\n var MediaInteractionMode;\n (function (MediaInteractionMode) {\n MediaInteractionMode[\"Always\"] = \"always\";\n MediaInteractionMode[\"Disabled\"] = \"disabled\";\n MediaInteractionMode[\"PausedOnly\"] = \"pausedOnly\";\n })(MediaInteractionMode || (MediaInteractionMode = {}));\n\n var AnnotationGeometryKind;\n (function (AnnotationGeometryKind) {\n AnnotationGeometryKind[\"Box\"] = \"box\";\n AnnotationGeometryKind[\"Polygon\"] = \"polygon\";\n AnnotationGeometryKind[\"Polyline\"] = \"polyline\";\n AnnotationGeometryKind[\"Keypoints\"] = \"keypoints\";\n AnnotationGeometryKind[\"Mask\"] = \"mask\";\n })(AnnotationGeometryKind || (AnnotationGeometryKind = {}));\n var AnnotationGestureStateKind;\n (function (AnnotationGestureStateKind) {\n AnnotationGestureStateKind[\"Idle\"] = \"idle\";\n AnnotationGestureStateKind[\"Creating\"] = \"creating\";\n AnnotationGestureStateKind[\"Moving\"] = \"moving\";\n AnnotationGestureStateKind[\"Resizing\"] = \"resizing\";\n AnnotationGestureStateKind[\"DragSelecting\"] = \"dragSelecting\";\n })(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));\n var AnnotationHandleKind;\n (function (AnnotationHandleKind) {\n AnnotationHandleKind[\"Resize\"] = \"resize\";\n AnnotationHandleKind[\"Vertex\"] = \"vertex\";\n AnnotationHandleKind[\"AddVertex\"] = \"addVertex\";\n AnnotationHandleKind[\"Keypoint\"] = \"keypoint\";\n })(AnnotationHandleKind || (AnnotationHandleKind = {}));\n\n /** Alignment for strokes applied to closed rendered geometry. */\n var StrokeAlignment;\n (function (StrokeAlignment) {\n StrokeAlignment[\"Inside\"] = \"inside\";\n StrokeAlignment[\"Center\"] = \"center\";\n StrokeAlignment[\"Outside\"] = \"outside\";\n })(StrokeAlignment || (StrokeAlignment = {}));\n\n var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n\n var FocusTargetMode;\n (function (FocusTargetMode) {\n FocusTargetMode[\"Hovered\"] = \"hovered\";\n FocusTargetMode[\"Selected\"] = \"selected\";\n FocusTargetMode[\"HoveredAndSelected\"] = \"hoveredAndSelected\";\n FocusTargetMode[\"Ambient\"] = \"ambient\";\n })(FocusTargetMode || (FocusTargetMode = {}));\n\n var DetectionInteractionState;\n (function (DetectionInteractionState) {\n DetectionInteractionState[\"Hovered\"] = \"hovered\";\n DetectionInteractionState[\"Selected\"] = \"selected\";\n })(DetectionInteractionState || (DetectionInteractionState = {}));\n\n var LabelPlacement;\n (function (LabelPlacement) {\n LabelPlacement[\"Top\"] = \"top\";\n LabelPlacement[\"Bottom\"] = \"bottom\";\n LabelPlacement[\"InsideTop\"] = \"insideTop\";\n LabelPlacement[\"InsideBottom\"] = \"insideBottom\";\n LabelPlacement[\"Center\"] = \"center\";\n })(LabelPlacement || (LabelPlacement = {}));\n var LabelVisibilityMode;\n (function (LabelVisibilityMode) {\n LabelVisibilityMode[\"Always\"] = \"always\";\n LabelVisibilityMode[\"HoveredOnly\"] = \"hoveredOnly\";\n })(LabelVisibilityMode || (LabelVisibilityMode = {}));\n\n var MaskRenderMode;\n (function (MaskRenderMode) {\n MaskRenderMode[\"FillAndStroke\"] = \"fillAndStroke\";\n MaskRenderMode[\"FillOnly\"] = \"fillOnly\";\n MaskRenderMode[\"StrokeOnly\"] = \"strokeOnly\";\n })(MaskRenderMode || (MaskRenderMode = {}));\n\n /**\n * Discriminator for renderer-neutral shape draw instructions.\n */\n var ShapeInstructionKind;\n (function (ShapeInstructionKind) {\n ShapeInstructionKind[\"Ellipse\"] = \"ellipse\";\n ShapeInstructionKind[\"Marker\"] = \"marker\";\n ShapeInstructionKind[\"Path\"] = \"path\";\n })(ShapeInstructionKind || (ShapeInstructionKind = {}));\n var MarkerShape;\n (function (MarkerShape) {\n MarkerShape[\"Circle\"] = \"circle\";\n MarkerShape[\"Cross\"] = \"cross\";\n MarkerShape[\"Square\"] = \"square\";\n MarkerShape[\"Triangle\"] = \"triangle\";\n })(MarkerShape || (MarkerShape = {}));\n /**\n * Coordinate space for marker sizing. Media sizes scale with the viewport;\n * screen sizes stay constant on screen, matching stroke-width semantics.\n */\n var MarkerSizeSpace;\n (function (MarkerSizeSpace) {\n MarkerSizeSpace[\"Media\"] = \"media\";\n MarkerSizeSpace[\"Screen\"] = \"screen\";\n })(MarkerSizeSpace || (MarkerSizeSpace = {}));\n\n var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\n function rasterizePolygonToMask(points, dimensions) {\n const data = createEmptyMask(dimensions);\n if (points.length < 3) {\n return data;\n }\n const bounds = centerRectToTopLeftRect(getPointsRect(points));\n const startY = Math.max(0, Math.floor(bounds.y));\n const endY = Math.min(dimensions.height - 1, Math.ceil(bounds.y + bounds.height));\n for (let y = startY; y <= endY; y += 1) {\n const scanY = y + 0.5;\n const intersections = [];\n for (let index = 0; index < points.length; index += 1) {\n const current = points[index];\n const next = points[(index + 1) % points.length];\n if ((current.y <= scanY && next.y > scanY) ||\n (next.y <= scanY && current.y > scanY)) {\n const ratio = (scanY - current.y) / (next.y - current.y);\n intersections.push(current.x + ratio * (next.x - current.x));\n }\n }\n intersections.sort((left, right) => left - right);\n for (let index = 0; index < intersections.length - 1; index += 2) {\n const left = Math.max(0, Math.round(intersections[index]));\n const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));\n for (let x = left; x < right; x += 1) {\n data[y * dimensions.width + x] = 1;\n }\n }\n }\n return data;\n }\n function createEmptyMask(dimensions) {\n if (!Number.isInteger(dimensions.width) ||\n dimensions.width <= 0 ||\n !Number.isInteger(dimensions.height) ||\n dimensions.height <= 0) {\n throw new Error(\"Media dimensions must be positive integers.\");\n }\n return new Uint8Array(dimensions.width * dimensions.height);\n }\n\n /**\n * Palette slots an id raster can name, one of them the background. A GLSL\n * fragment stage is guaranteed only 224 uniform vectors, and a palette entry\n * costs 2.25 of them, so this stays a multiple of four and well inside that\n * budget; past it a frame draws from the RGBA composite instead.\n */\n const MAX_ID_MASK_PALETTE_ENTRIES = 80;\n const MAX_ID_MASK_STROKE_WIDTH = 16;\n function createIdMaskFrame(instructions, options = {}) {\n if (instructions.length === 0) {\n return undefined;\n }\n const maskWidth = Math.max(...instructions.map(({ mask }) => mask.width));\n const maskHeight = Math.max(...instructions.map(({ mask }) => mask.height));\n const width = resolveRasterWidth(maskWidth, options.maxWidth);\n const height = width === maskWidth\n ? maskHeight\n : Math.max(1, Math.round((width * maskHeight) / maskWidth));\n const strokeScale = width / maskWidth;\n const scaledAxes = width === maskWidth\n ? undefined\n : createScaledMaskAxes({ height, maskHeight, maskWidth, width });\n const data = new Uint8Array(new ArrayBuffer(width * height));\n const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokeWidths = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));\n let hasStroke = false;\n let maxStrokeWidth = 0;\n for (const instruction of instructions) {\n const detectionMaskId = instruction.detectionIndex + 1;\n if (detectionMaskId <= 0 ||\n detectionMaskId >= MAX_ID_MASK_PALETTE_ENTRIES) {\n return undefined;\n }\n writePaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);\n if (instruction.stroke && instruction.stroke.width > 0) {\n const strokeWidth = Math.min(resolveStrokeTexels(instruction.stroke.width, strokeScale), MAX_ID_MASK_STROKE_WIDTH);\n hasStroke = true;\n strokeWidths[detectionMaskId] = strokeWidth;\n maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);\n writePaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);\n }\n if (scaledAxes) {\n writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);\n }\n else {\n writeMaskRuns(data, width, instruction.mask, detectionMaskId);\n }\n }\n return {\n data,\n fillPalette,\n hasStroke,\n height,\n maxStrokeWidth,\n strokePalette,\n strokeWidths,\n width,\n };\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function writeMaskRuns(data, frameWidth, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n let maskOffset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let frameOffset = y * frameWidth + x;\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[frameOffset] = detectionMaskId;\n y += 1;\n frameOffset += frameWidth;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n frameOffset = x;\n }\n }\n maskOffset += runLength;\n }\n }\n /**\n * The scaled twin of writeMaskRuns. Every source pixel marks the texel it lands\n * in, so a mask keeps every part of itself the smaller raster can hold and\n * gains up to a texel at its edges.\n */\n function writeScaledMaskRuns(data, axes, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n let maskOffset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let column = axes.columns[x];\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[axes.rows[y] + column] = detectionMaskId;\n y += 1;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n column = axes.columns[x];\n }\n }\n maskOffset += runLength;\n }\n }\n function resolveRasterWidth(maskWidth, maxWidth) {\n return maxWidth !== undefined && maxWidth > 0 && maxWidth < maskWidth\n ? Math.max(1, Math.floor(maxWidth))\n : maskWidth;\n }\n /**\n * A stroke is measured in texels of the raster it is drawn on, so a coarser\n * raster measures it in coarser texels. A stroke of a texel or more keeps at\n * least one, the thinnest line the shader can draw; a narrower one keeps its\n * own width, which the shader draws as an inner boundary at any scale.\n */\n function resolveStrokeTexels(strokeWidth, scale) {\n return Math.max(strokeWidth * scale, Math.min(strokeWidth, 1));\n }\n function createScaledMaskAxes(frame) {\n return {\n columns: createMaskAxisMap(frame.maskWidth, frame.width, 1),\n rows: createMaskAxisMap(frame.maskHeight, frame.height, frame.width),\n };\n }\n /** Rows hold their destination offset, so writing a run is two lookups. */\n function createMaskAxisMap(sourceLength, targetLength, stride) {\n const map = new Int32Array(sourceLength);\n const scale = targetLength / sourceLength;\n for (let index = 0; index < sourceLength; index += 1) {\n map[index] = Math.min(targetLength - 1, Math.floor(index * scale)) * stride;\n }\n return map;\n }\n function writePaletteEntry(palette, id, color, alpha) {\n const offset = id * 4;\n palette[offset] = ((color >> 16) & 0xff) / 255;\n palette[offset + 1] = ((color >> 8) & 0xff) / 255;\n palette[offset + 2] = (color & 0xff) / 255;\n palette[offset + 3] = Math.max(0, Math.min(alpha, 1));\n }\n\n var MediaRendererFit;\n (function (MediaRendererFit) {\n /**\n * Preserve media aspect ratio and fit the full frame inside the canvas.\n */\n MediaRendererFit[\"Contain\"] = \"contain\";\n /**\n * Preserve media aspect ratio and fill the canvas, cropping if necessary.\n */\n MediaRendererFit[\"Cover\"] = \"cover\";\n })(MediaRendererFit || (MediaRendererFit = {}));\n /**\n * Playback lifecycle state reported by a platform renderer.\n */\n var MediaRendererPlaybackState;\n (function (MediaRendererPlaybackState) {\n MediaRendererPlaybackState[\"Loading\"] = \"loading\";\n MediaRendererPlaybackState[\"Ready\"] = \"ready\";\n MediaRendererPlaybackState[\"Playing\"] = \"playing\";\n MediaRendererPlaybackState[\"Buffering\"] = \"buffering\";\n MediaRendererPlaybackState[\"Paused\"] = \"paused\";\n MediaRendererPlaybackState[\"Error\"] = \"error\";\n MediaRendererPlaybackState[\"Destroyed\"] = \"destroyed\";\n })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));\n /**\n * Stable classification of a media failure.\n *\n * Applications branch on these kinds instead of parsing decoder, demuxer, or\n * container error text, and still own their localized user-facing copy. New\n * kinds may be added over time, so treat unrecognized values like `Unknown`.\n */\n var MediaErrorKind;\n (function (MediaErrorKind) {\n /** The media could not be opened or read at all. */\n MediaErrorKind[\"Unreadable\"] = \"unreadable\";\n /** The container or codec is not supported by this platform. */\n MediaErrorKind[\"UnsupportedFormat\"] = \"unsupportedFormat\";\n /** The media opened but carries no usable video track. */\n MediaErrorKind[\"NoVideoTrack\"] = \"noVideoTrack\";\n /** Decoding a sample failed after the source opened. */\n MediaErrorKind[\"Decode\"] = \"decode\";\n /** The media could not be fetched. */\n MediaErrorKind[\"Network\"] = \"network\";\n /** The host environment lacks the APIs this media source requires. */\n MediaErrorKind[\"EnvironmentUnsupported\"] = \"environmentUnsupported\";\n /** The failure could not be classified. */\n MediaErrorKind[\"Unknown\"] = \"unknown\";\n })(MediaErrorKind || (MediaErrorKind = {}));\n /**\n * Lower-level media source readiness.\n */\n var MediaSourceStatus;\n (function (MediaSourceStatus) {\n MediaSourceStatus[\"Loading\"] = \"loading\";\n MediaSourceStatus[\"Ready\"] = \"ready\";\n MediaSourceStatus[\"Error\"] = \"error\";\n MediaSourceStatus[\"Destroyed\"] = \"destroyed\";\n })(MediaSourceStatus || (MediaSourceStatus = {}));\n /**\n * Current renderer state.\n */\n /**\n * What a playback gate holds back.\n *\n * A source the renderer pulls samples from can be held between any two frames,\n * because the renderer decides when each one is drawn. A source that presents\n * its own frames owns the playhead, so holding it means stopping the producer\n * and starting it again, which the detection gate does and the\n * render-preparation gate does not.\n */\n var PlaybackGateReach;\n (function (PlaybackGateReach) {\n /** No gate: the picture moves and unprepared layers are absent from it. */\n PlaybackGateReach[\"Off\"] = \"off\";\n /**\n * Playback waits to begin, and stops again at any frame whose artifacts are\n * missing, until they arrive or the gate's own wait bound gives up on them.\n */\n PlaybackGateReach[\"EveryFrame\"] = \"everyFrame\";\n /** Playback waits to begin; frames after that are not held. */\n PlaybackGateReach[\"StartOfPlayback\"] = \"startOfPlayback\";\n })(PlaybackGateReach || (PlaybackGateReach = {}));\n\n /**\n * Media-session operating mode.\n *\n * Core owns the names because file-like and stream-like lifecycle choices are\n * platform-neutral. Platform packages decide how these modes tune media,\n * storage, and renderer defaults.\n */\n var MediaSessionMode;\n (function (MediaSessionMode) {\n /**\n * Finite media. Defaults usually favor seek/replay and persistent detection\n * storage.\n */\n MediaSessionMode[\"File\"] = \"file\";\n /**\n * Live or append-only media. Defaults usually favor rolling windows and\n * bounded retention.\n */\n MediaSessionMode[\"Stream\"] = \"stream\";\n })(MediaSessionMode || (MediaSessionMode = {}));\n /**\n * Aggregate lifecycle state for a media session.\n */\n var MediaSessionStatus;\n (function (MediaSessionStatus) {\n MediaSessionStatus[\"Buffering\"] = \"buffering\";\n MediaSessionStatus[\"Destroyed\"] = \"destroyed\";\n MediaSessionStatus[\"Error\"] = \"error\";\n MediaSessionStatus[\"Loading\"] = \"loading\";\n MediaSessionStatus[\"Paused\"] = \"paused\";\n MediaSessionStatus[\"Playing\"] = \"playing\";\n MediaSessionStatus[\"Processing\"] = \"processing\";\n MediaSessionStatus[\"Ready\"] = \"ready\";\n })(MediaSessionStatus || (MediaSessionStatus = {}));\n /**\n * Subsystem currently affecting session readiness or presentation.\n */\n var MediaSessionActivityKind;\n (function (MediaSessionActivityKind) {\n /**\n * Playback is held waiting for a producer to emit detections for the frame\n * about to be shown, which is a wait on inference rather than on transfer or\n * decode. `DetectionsBuffering` is the transfer of detections that already\n * exist, and `PlaybackBuffering` is the wait for media bytes.\n */\n MediaSessionActivityKind[\"DetectionsAwaitingCoverage\"] = \"detectionsAwaitingCoverage\";\n MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\n MediaSessionActivityKind[\"RenderPreparing\"] = \"renderPreparing\";\n })(MediaSessionActivityKind || (MediaSessionActivityKind = {}));\n /**\n * State of one session activity.\n */\n var MediaSessionActivityStatus;\n (function (MediaSessionActivityStatus) {\n MediaSessionActivityStatus[\"Error\"] = \"error\";\n MediaSessionActivityStatus[\"Running\"] = \"running\";\n MediaSessionActivityStatus[\"Waiting\"] = \"waiting\";\n })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));\n\n /** Builds compact, independent alpha crops so overlapping masks stay exact. */\n function createRegionMaskCoverageFrame(instructions) {\n const entries = [];\n for (const instruction of instructions) {\n if (!instruction.regionCoverageMask) {\n continue;\n }\n const entry = cropCoverageMask(instruction.detectionIndex, decodeCompressedRleMask(instruction.regionCoverageMask));\n if (entry) {\n entries.push(entry);\n }\n }\n return entries.length > 0 ? { entries } : undefined;\n }\n function cropCoverageMask(detectionIndex, decodedMask) {\n let minX = decodedMask.width;\n let minY = decodedMask.height;\n let maxX = -1;\n let maxY = -1;\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n }\n if (maxX < minX || maxY < minY) {\n return undefined;\n }\n const width = maxX - minX + 1;\n const height = maxY - minY + 1;\n const data = new Uint8Array(new ArrayBuffer(width * height));\n for (let y = minY; y <= maxY; y += 1) {\n for (let x = minX; x <= maxX; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n data[(y - minY) * width + (x - minX)] = 255;\n }\n }\n return { data, detectionIndex, height, width, x: minX, y: minY };\n }\n function compositeMaskFrame(instructions) {\n const maskInstructions = materializeMaskInstructions(instructions);\n if (maskInstructions.length === 0) {\n return undefined;\n }\n const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));\n const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));\n const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));\n for (const instruction of maskInstructions) {\n compositeInstruction(data, width, instruction);\n }\n return { data, height, width };\n }\n function createIdMaskRasterFrame(instructions, maxRasterWidth) {\n try {\n return createIdMaskFrame(materializeMaskInstructions(instructions), {\n maxWidth: maxRasterWidth,\n });\n }\n catch {\n // The id raster is the fast path, not the only one: answering with nothing\n // puts the caller on the RGBA composite, which draws the same picture.\n return undefined;\n }\n }\n function createIdMaskPlane(instructions, maxRasterWidth) {\n const frame = createIdMaskRasterFrame(instructions, maxRasterWidth);\n return frame\n ? { data: frame.data, height: frame.height, width: frame.width }\n : undefined;\n }\n function compositeInstruction(rgba, canvasWidth, instruction) {\n const decodedMask = decodeCompressedRleMask(instruction.mask);\n const fill = resolveRgbaColor(instruction.color, instruction.alpha);\n compositeMaskFill(rgba, canvasWidth, decodedMask, fill);\n if (instruction.stroke) {\n compositeMaskStroke(rgba, canvasWidth, decodedMask, instruction.stroke);\n }\n }\n function materializeMaskInstructions(instructions) {\n return instructions\n .filter((instruction) => instruction.visible !== false)\n .map((instruction) => {\n if (instruction.mask) {\n return instruction;\n }\n const { height, points, width } = instruction.polygon;\n return {\n alpha: instruction.alpha,\n color: instruction.color,\n detectionIndex: instruction.detectionIndex,\n mask: encodeBinaryMask(rasterizePolygonToMask(points, { height, width }), width, height),\n stroke: instruction.stroke,\n };\n });\n }\n function compositeMaskFill(rgba, canvasWidth, decodedMask, fill) {\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n const maskOffset = y * decodedMask.width + x;\n if (!decodedMask.data[maskOffset]) {\n continue;\n }\n writePixel(rgba, canvasWidth, x, y, fill);\n }\n }\n }\n function compositeMaskStroke(rgba, canvasWidth, decodedMask, stroke) {\n const width = Math.round(stroke.width);\n if (width <= 0) {\n return;\n }\n const strokeColor = resolveRgbaColor(stroke.color, stroke.alpha);\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n if (!isMaskPixel(decodedMask, x, y) ||\n !isBoundaryPixel(decodedMask, x, y)) {\n continue;\n }\n for (let offsetY = -width; offsetY <= width; offsetY += 1) {\n for (let offsetX = -width; offsetX <= width; offsetX += 1) {\n const strokeX = x + offsetX;\n const strokeY = y + offsetY;\n if (isOutsideMaskBounds(decodedMask, strokeX, strokeY) ||\n isMaskPixel(decodedMask, strokeX, strokeY)) {\n continue;\n }\n writePixel(rgba, canvasWidth, strokeX, strokeY, strokeColor);\n }\n }\n }\n }\n }\n function isBoundaryPixel(mask, x, y) {\n for (let offsetY = -1; offsetY <= 1; offsetY += 1) {\n for (let offsetX = -1; offsetX <= 1; offsetX += 1) {\n if (offsetX === 0 && offsetY === 0) {\n continue;\n }\n const neighborX = x + offsetX;\n const neighborY = y + offsetY;\n if (isOutsideMaskBounds(mask, neighborX, neighborY) ||\n !isMaskPixel(mask, neighborX, neighborY)) {\n return true;\n }\n }\n }\n return false;\n }\n function isMaskPixel(mask, x, y) {\n return mask.data[y * mask.width + x] === 1;\n }\n function isOutsideMaskBounds(mask, x, y) {\n return x < 0 || y < 0 || x >= mask.width || y >= mask.height;\n }\n function resolveRgbaColor(color, alpha) {\n return {\n alpha: Math.round(Math.max(0, Math.min(alpha, 1)) * 255),\n blue: color & 0xff,\n green: (color >> 8) & 0xff,\n red: (color >> 16) & 0xff,\n };\n }\n function writePixel(rgba, canvasWidth, x, y, color) {\n const rgbaOffset = (y * canvasWidth + x) * 4;\n rgba[rgbaOffset] = color.red;\n rgba[rgbaOffset + 1] = color.green;\n rgba[rgbaOffset + 2] = color.blue;\n rgba[rgbaOffset + 3] = color.alpha;\n }\n\n var PreparedMaskFrameKind;\n (function (PreparedMaskFrameKind) {\n PreparedMaskFrameKind[\"IdMask\"] = \"idMask\";\n PreparedMaskFrameKind[\"RgbaImage\"] = \"rgbaImage\";\n })(PreparedMaskFrameKind || (PreparedMaskFrameKind = {}));\n /**\n * How an id raster is laid out for the texture it is uploaded into. WebGPU\n * takes any bytesPerRow, so ids go up one byte per pixel. WebGL aligns every\n * uploaded row to four bytes and rejects a single-channel upload whose width\n * is not a multiple of four, which is what the four-channel layout is for.\n */\n var IdMaskTextureFormat;\n (function (IdMaskTextureFormat) {\n IdMaskTextureFormat[\"R8\"] = \"r8unorm\";\n IdMaskTextureFormat[\"Rgba8\"] = \"rgba8unorm\";\n })(IdMaskTextureFormat || (IdMaskTextureFormat = {}));\n\n var MaskPreparationWorkerMessageType;\n (function (MaskPreparationWorkerMessageType) {\n MaskPreparationWorkerMessageType[\"Complete\"] = \"complete\";\n MaskPreparationWorkerMessageType[\"Empty\"] = \"empty\";\n MaskPreparationWorkerMessageType[\"Error\"] = \"error\";\n MaskPreparationWorkerMessageType[\"Prepare\"] = \"prepare\";\n })(MaskPreparationWorkerMessageType || (MaskPreparationWorkerMessageType = {}));\n\n const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n prepareMaskFrame(message);\n });\n function prepareMaskFrame(message) {\n try {\n const regionMaskCoverage = createRegionMaskCoverageFrame(message.job.instructions);\n const coverageTransfers = getRegionMaskCoverageTransfers(regionMaskCoverage);\n const idMaskFrame = createIdMaskRasterFrame(message.job.instructions, message.job.maxRasterWidth);\n if (idMaskFrame) {\n workerScope.postMessage({\n artifactKind: PreparedMaskFrameKind.IdMask,\n fillPalette: idMaskFrame.fillPalette,\n hasStroke: idMaskFrame.hasStroke,\n height: idMaskFrame.height,\n key: message.job.key,\n maxStrokeWidth: idMaskFrame.maxStrokeWidth,\n raster: idMaskFrame.data,\n regionMaskCoverage,\n requestId: message.requestId,\n strokePalette: idMaskFrame.strokePalette,\n strokeWidths: idMaskFrame.strokeWidths,\n type: MaskPreparationWorkerMessageType.Complete,\n width: idMaskFrame.width,\n }, [\n idMaskFrame.data.buffer,\n idMaskFrame.fillPalette.buffer,\n idMaskFrame.strokePalette.buffer,\n idMaskFrame.strokeWidths.buffer,\n ...coverageTransfers,\n ]);\n return;\n }\n const compositedFrame = compositeMaskFrame(message.job.instructions);\n if (!compositedFrame && !regionMaskCoverage) {\n workerScope.postMessage({\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Empty,\n });\n return;\n }\n const preparedPixels = compositedFrame ?? createTransparentCoverageCarrier();\n const imageData = new ImageData(preparedPixels.data, preparedPixels.width, preparedPixels.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n const idMaskPlane = createIdMaskPlane(message.job.instructions, message.job.maxRasterWidth);\n const idMaskTransfers = idMaskPlane ? [idMaskPlane.data.buffer] : [];\n if (imageBitmap) {\n workerScope.postMessage({\n idMaskPlane,\n imageBitmap,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [imageBitmap, ...idMaskTransfers, ...coverageTransfers]);\n return;\n }\n workerScope.postMessage({\n idMaskPlane,\n imageData,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer, ...idMaskTransfers, ...coverageTransfers]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to prepare mask frame.\",\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Error,\n });\n }\n }\n /**\n * A frame that carries only region coverage has nothing to composite, and the\n * RGBA branch still has to produce an artifact for the coverage to ride on.\n */\n function createTransparentCoverageCarrier() {\n return {\n data: new Uint8ClampedArray(new ArrayBuffer(4)),\n height: 1,\n width: 1,\n };\n }\n function getRegionMaskCoverageTransfers(coverage) {\n return coverage?.entries.map(({ data }) => data.buffer) ?? [];\n }\n function createImageBitmapFromImageData(imageData) {\n if (typeof OffscreenCanvas === \"undefined\") {\n return null;\n }\n const canvas = new OffscreenCanvas(imageData.width, imageData.height);\n const context = canvas.getContext(\"2d\");\n if (!context) {\n return null;\n }\n context.putImageData(imageData, 0, 0);\n return canvas.transferToImageBitmap();\n }\n\n})();";
5088
+ const EMBEDDED_MASK_PREPARATION_WORKER_SOURCE = "(function () {\n 'use strict';\n\n /** COCO-compatible keypoint visibility values. */\n var KeypointVisibility;\n (function (KeypointVisibility) {\n KeypointVisibility[KeypointVisibility[\"NotLabeled\"] = 0] = \"NotLabeled\";\n KeypointVisibility[KeypointVisibility[\"Occluded\"] = 1] = \"Occluded\";\n KeypointVisibility[KeypointVisibility[\"Visible\"] = 2] = \"Visible\";\n })(KeypointVisibility || (KeypointVisibility = {}));\n var DetectionMaskEncoding;\n (function (DetectionMaskEncoding) {\n DetectionMaskEncoding[\"CompressedRle\"] = \"compressedRle\";\n })(DetectionMaskEncoding || (DetectionMaskEncoding = {}));\n\n var DetectionBufferStatus;\n (function (DetectionBufferStatus) {\n DetectionBufferStatus[\"Idle\"] = \"idle\";\n DetectionBufferStatus[\"Loading\"] = \"loading\";\n /**\n * The playback gate is holding playback until the source produces detections\n * for the requested range. {@link DetectionBufferStatus.Loading} fetches\n * detections the source already has; this one waits on a producer.\n */\n DetectionBufferStatus[\"AwaitingCoverage\"] = \"awaitingCoverage\";\n DetectionBufferStatus[\"Ready\"] = \"ready\";\n DetectionBufferStatus[\"Error\"] = \"error\";\n DetectionBufferStatus[\"Destroyed\"] = \"destroyed\";\n })(DetectionBufferStatus || (DetectionBufferStatus = {}));\n /**\n * How the renderer selects the active detection frame for a media timestamp.\n */\n var DetectionFrameSelectionMode;\n (function (DetectionFrameSelectionMode) {\n /**\n * Select the frame whose `[mediaTime, endTime)` interval contains the media\n * time. This is the default for interval annotations and timestamped sources.\n *\n * Frame times must come from the media itself. Times reconstructed from a\n * nominal frame rate drift against a clip whose real rate differs, and once\n * that drift passes the half-millisecond selection tolerance a playhead\n * landing on a frame boundary selects the previous frame's detections.\n *\n * A frame that carries no `endTime` stays active until the next frame starts,\n * which bridges an index the source never produced.\n * {@link DetectionFrameSelectionMode.NearestFrameIndex} bounds each frame to\n * one grid step instead, so a missing index reads as no detections.\n */\n DetectionFrameSelectionMode[\"Interval\"] = \"interval\";\n /**\n * Select from a known inference frame grid using `frameIndex` and\n * `frameRate`. This is useful when inference was run on normalized frames and\n * playback should snap detections to that grid.\n *\n * A frame speaks for one grid step starting at its own media time, and an\n * index the source never produced selects nothing rather than a neighbour.\n *\n * A frame carrying no `frameIndex` speaks for its step on the same terms,\n * reached by the time it starts at rather than by an index that names it. Its\n * `endTime` does not widen it past that step, so a source that labels only\n * part of what it writes cannot bridge the indexes it never produced.\n */\n DetectionFrameSelectionMode[\"NearestFrameIndex\"] = \"nearestFrameIndex\";\n })(DetectionFrameSelectionMode || (DetectionFrameSelectionMode = {}));\n var DetectionFrameRetentionMode;\n (function (DetectionFrameRetentionMode) {\n /**\n * Keep writable detections only in an in-memory store. Useful for ephemeral\n * live streams where old predictions should disappear when evicted.\n */\n DetectionFrameRetentionMode[\"MemoryOnly\"] = \"memoryOnly\";\n /**\n * Persist every written detection frame. Useful for finite media where seek\n * and replay should not require recomputing detections.\n */\n DetectionFrameRetentionMode[\"PersistAll\"] = \"persistAll\";\n /**\n * Persist only the most recent retention window. Useful for long-running\n * streams where replay is bounded to a recent time horizon.\n */\n DetectionFrameRetentionMode[\"PersistWindow\"] = \"persistWindow\";\n })(DetectionFrameRetentionMode || (DetectionFrameRetentionMode = {}));\n function decodeCompressedRleMask(mask) {\n if (mask.encoding !== DetectionMaskEncoding.CompressedRle) {\n throw new Error(`Unsupported detection mask encoding: ${mask.encoding}`);\n }\n const data = new Uint8Array(mask.width * mask.height);\n const counts = decodeCompressedRleCounts(mask.counts);\n let offset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n const isForeground = index % 2 === 1;\n if (isForeground) {\n for (let runOffset = 0; runOffset < runLength; runOffset += 1) {\n const maskOffset = offset + runOffset;\n const x = Math.floor(maskOffset / mask.height);\n const y = maskOffset % mask.height;\n const rowMajorOffset = y * mask.width + x;\n if (rowMajorOffset < data.length) {\n data[rowMajorOffset] = 1;\n }\n }\n }\n offset += runLength;\n }\n return {\n data,\n height: mask.height,\n width: mask.width,\n };\n }\n function decodeCompressedRleCounts(counts) {\n const decoded = [];\n let index = 0;\n while (index < counts.length) {\n let value = 0;\n let shift = 0;\n let charCode;\n do {\n charCode = counts.charCodeAt(index) - 48;\n index += 1;\n value |= (charCode & 0x1f) << shift;\n shift += 5;\n } while (charCode & 0x20);\n if (charCode & 0x10) {\n value |= -1 << shift;\n }\n if (decoded.length > 2) {\n value += decoded[decoded.length - 2] ?? 0;\n }\n decoded.push(value);\n }\n return decoded;\n }\n function encodeCompressedRleCounts(counts) {\n return counts\n .map((count, index) => {\n let value = index > 2 ? count - counts[index - 2] : count;\n let encoded = \"\";\n let more = true;\n while (more) {\n let charCode = value & 0x1f;\n value >>= 5;\n more = !((value === 0 && (charCode & 0x10) === 0) ||\n (value === -1 && (charCode & 0x10) !== 0));\n if (more) {\n charCode |= 0x20;\n }\n encoded += String.fromCharCode(charCode + 48);\n }\n return encoded;\n })\n .join(\"\");\n }\n\n var AnnotationFrameMutationKind;\n (function (AnnotationFrameMutationKind) {\n AnnotationFrameMutationKind[\"Add\"] = \"add\";\n AnnotationFrameMutationKind[\"Remove\"] = \"remove\";\n AnnotationFrameMutationKind[\"Replace\"] = \"replace\";\n AnnotationFrameMutationKind[\"Transact\"] = \"transact\";\n AnnotationFrameMutationKind[\"Update\"] = \"update\";\n })(AnnotationFrameMutationKind || (AnnotationFrameMutationKind = {}));\n\n /** Geometry projected into a tracker. The original detection payload is retained. */\n var TrackingGeometry;\n (function (TrackingGeometry) {\n TrackingGeometry[\"Box\"] = \"box\";\n TrackingGeometry[\"Mask\"] = \"mask\";\n TrackingGeometry[\"Keypoints\"] = \"keypoints\";\n })(TrackingGeometry || (TrackingGeometry = {}));\n\n var DetectionMaskPayloadFormat;\n (function (DetectionMaskPayloadFormat) {\n DetectionMaskPayloadFormat[\"RawCocoRle\"] = \"rawCocoRle\";\n DetectionMaskPayloadFormat[\"DeflatedBase64\"] = \"deflatedBase64\";\n })(DetectionMaskPayloadFormat || (DetectionMaskPayloadFormat = {}));\n function encodeBinaryMask(data, width, height) {\n assertMaskDimensions(data, width, height);\n const runs = [];\n let currentValue = 0;\n let runLength = 0;\n for (let x = 0; x < width; x += 1) {\n for (let y = 0; y < height; y += 1) {\n const value = data[y * width + x] ? 1 : 0;\n if (value === currentValue) {\n runLength += 1;\n }\n else {\n runs.push(runLength);\n currentValue = value;\n runLength = 1;\n }\n }\n }\n runs.push(runLength);\n return {\n counts: encodeCompressedRleCounts(runs),\n encoding: DetectionMaskEncoding.CompressedRle,\n height,\n width,\n };\n }\n function assertMaskSize(width, height) {\n if (!Number.isInteger(width) || width <= 0) {\n throw new Error(\"Mask width must be a positive integer.\");\n }\n if (!Number.isInteger(height) || height <= 0) {\n throw new Error(\"Mask height must be a positive integer.\");\n }\n }\n function assertMaskDimensions(data, width, height) {\n assertMaskSize(width, height);\n if (data.length !== width * height) {\n throw new Error(\"Mask data length must equal width * height.\");\n }\n }\n\n function centerRectToTopLeftRect(rect) {\n return {\n height: rect.height,\n width: rect.width,\n x: rect.x - rect.width / 2,\n y: rect.y - rect.height / 2,\n };\n }\n function getPointsRect(points) {\n if (points.length === 0) {\n return undefined;\n }\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const point of points) {\n minX = Math.min(minX, point.x);\n minY = Math.min(minY, point.y);\n maxX = Math.max(maxX, point.x);\n maxY = Math.max(maxY, point.y);\n }\n return {\n height: maxY - minY,\n width: maxX - minX,\n x: (minX + maxX) / 2,\n y: (minY + maxY) / 2,\n };\n }\n\n var DetectionPickTarget;\n (function (DetectionPickTarget) {\n DetectionPickTarget[\"Box\"] = \"box\";\n DetectionPickTarget[\"Edge\"] = \"edge\";\n DetectionPickTarget[\"Keypoint\"] = \"keypoint\";\n DetectionPickTarget[\"Label\"] = \"label\";\n DetectionPickTarget[\"Mask\"] = \"mask\";\n DetectionPickTarget[\"Polygon\"] = \"polygon\";\n DetectionPickTarget[\"Polyline\"] = \"polyline\";\n })(DetectionPickTarget || (DetectionPickTarget = {}));\n var MediaInteractionMode;\n (function (MediaInteractionMode) {\n MediaInteractionMode[\"Always\"] = \"always\";\n MediaInteractionMode[\"Disabled\"] = \"disabled\";\n MediaInteractionMode[\"PausedOnly\"] = \"pausedOnly\";\n })(MediaInteractionMode || (MediaInteractionMode = {}));\n\n var AnnotationGeometryKind;\n (function (AnnotationGeometryKind) {\n AnnotationGeometryKind[\"Box\"] = \"box\";\n AnnotationGeometryKind[\"Polygon\"] = \"polygon\";\n AnnotationGeometryKind[\"Polyline\"] = \"polyline\";\n AnnotationGeometryKind[\"Keypoints\"] = \"keypoints\";\n AnnotationGeometryKind[\"Mask\"] = \"mask\";\n })(AnnotationGeometryKind || (AnnotationGeometryKind = {}));\n var AnnotationGestureStateKind;\n (function (AnnotationGestureStateKind) {\n AnnotationGestureStateKind[\"Idle\"] = \"idle\";\n AnnotationGestureStateKind[\"Creating\"] = \"creating\";\n AnnotationGestureStateKind[\"Moving\"] = \"moving\";\n AnnotationGestureStateKind[\"Resizing\"] = \"resizing\";\n AnnotationGestureStateKind[\"DragSelecting\"] = \"dragSelecting\";\n })(AnnotationGestureStateKind || (AnnotationGestureStateKind = {}));\n var AnnotationHandleKind;\n (function (AnnotationHandleKind) {\n AnnotationHandleKind[\"Resize\"] = \"resize\";\n AnnotationHandleKind[\"Vertex\"] = \"vertex\";\n AnnotationHandleKind[\"AddVertex\"] = \"addVertex\";\n AnnotationHandleKind[\"Keypoint\"] = \"keypoint\";\n })(AnnotationHandleKind || (AnnotationHandleKind = {}));\n\n /** Alignment for strokes applied to closed rendered geometry. */\n var StrokeAlignment;\n (function (StrokeAlignment) {\n StrokeAlignment[\"Inside\"] = \"inside\";\n StrokeAlignment[\"Center\"] = \"center\";\n StrokeAlignment[\"Outside\"] = \"outside\";\n })(StrokeAlignment || (StrokeAlignment = {}));\n\n var BoxShape;\n (function (BoxShape) {\n BoxShape[\"Rect\"] = \"rect\";\n BoxShape[\"RoundedRect\"] = \"roundedRect\";\n })(BoxShape || (BoxShape = {}));\n\n var FocusTargetMode;\n (function (FocusTargetMode) {\n FocusTargetMode[\"Hovered\"] = \"hovered\";\n FocusTargetMode[\"Selected\"] = \"selected\";\n FocusTargetMode[\"HoveredAndSelected\"] = \"hoveredAndSelected\";\n FocusTargetMode[\"Ambient\"] = \"ambient\";\n })(FocusTargetMode || (FocusTargetMode = {}));\n\n var DetectionInteractionState;\n (function (DetectionInteractionState) {\n DetectionInteractionState[\"Hovered\"] = \"hovered\";\n DetectionInteractionState[\"Selected\"] = \"selected\";\n })(DetectionInteractionState || (DetectionInteractionState = {}));\n\n var LabelPlacement;\n (function (LabelPlacement) {\n LabelPlacement[\"Top\"] = \"top\";\n LabelPlacement[\"Bottom\"] = \"bottom\";\n LabelPlacement[\"InsideTop\"] = \"insideTop\";\n LabelPlacement[\"InsideBottom\"] = \"insideBottom\";\n LabelPlacement[\"Center\"] = \"center\";\n })(LabelPlacement || (LabelPlacement = {}));\n var LabelVisibilityMode;\n (function (LabelVisibilityMode) {\n LabelVisibilityMode[\"Always\"] = \"always\";\n LabelVisibilityMode[\"HoveredOnly\"] = \"hoveredOnly\";\n })(LabelVisibilityMode || (LabelVisibilityMode = {}));\n\n var MaskRenderMode;\n (function (MaskRenderMode) {\n MaskRenderMode[\"FillAndStroke\"] = \"fillAndStroke\";\n MaskRenderMode[\"FillOnly\"] = \"fillOnly\";\n MaskRenderMode[\"StrokeOnly\"] = \"strokeOnly\";\n })(MaskRenderMode || (MaskRenderMode = {}));\n\n /**\n * Discriminator for renderer-neutral shape draw instructions.\n */\n var ShapeInstructionKind;\n (function (ShapeInstructionKind) {\n ShapeInstructionKind[\"Ellipse\"] = \"ellipse\";\n ShapeInstructionKind[\"Marker\"] = \"marker\";\n ShapeInstructionKind[\"Path\"] = \"path\";\n })(ShapeInstructionKind || (ShapeInstructionKind = {}));\n var MarkerShape;\n (function (MarkerShape) {\n MarkerShape[\"Circle\"] = \"circle\";\n MarkerShape[\"Cross\"] = \"cross\";\n MarkerShape[\"Square\"] = \"square\";\n MarkerShape[\"Triangle\"] = \"triangle\";\n })(MarkerShape || (MarkerShape = {}));\n /**\n * Coordinate space for marker sizing. Media sizes scale with the viewport;\n * screen sizes stay constant on screen, matching stroke-width semantics.\n */\n var MarkerSizeSpace;\n (function (MarkerSizeSpace) {\n MarkerSizeSpace[\"Media\"] = \"media\";\n MarkerSizeSpace[\"Screen\"] = \"screen\";\n })(MarkerSizeSpace || (MarkerSizeSpace = {}));\n\n var KeypointMarkerShape;\n (function (KeypointMarkerShape) {\n KeypointMarkerShape[\"Circle\"] = \"circle\";\n KeypointMarkerShape[\"Cross\"] = \"cross\";\n })(KeypointMarkerShape || (KeypointMarkerShape = {}));\n function rasterizePolygonToMask(points, dimensions) {\n const data = createEmptyMask(dimensions);\n if (points.length < 3) {\n return data;\n }\n const bounds = centerRectToTopLeftRect(getPointsRect(points));\n const startY = Math.max(0, Math.floor(bounds.y));\n const endY = Math.min(dimensions.height - 1, Math.ceil(bounds.y + bounds.height));\n for (let y = startY; y <= endY; y += 1) {\n const scanY = y + 0.5;\n const intersections = [];\n for (let index = 0; index < points.length; index += 1) {\n const current = points[index];\n const next = points[(index + 1) % points.length];\n if ((current.y <= scanY && next.y > scanY) ||\n (next.y <= scanY && current.y > scanY)) {\n const ratio = (scanY - current.y) / (next.y - current.y);\n intersections.push(current.x + ratio * (next.x - current.x));\n }\n }\n intersections.sort((left, right) => left - right);\n for (let index = 0; index < intersections.length - 1; index += 2) {\n const left = Math.max(0, Math.round(intersections[index]));\n const right = Math.min(dimensions.width, Math.round(intersections[index + 1]));\n for (let x = left; x < right; x += 1) {\n data[y * dimensions.width + x] = 1;\n }\n }\n }\n return data;\n }\n function createEmptyMask(dimensions) {\n if (!Number.isInteger(dimensions.width) ||\n dimensions.width <= 0 ||\n !Number.isInteger(dimensions.height) ||\n dimensions.height <= 0) {\n throw new Error(\"Media dimensions must be positive integers.\");\n }\n return new Uint8Array(dimensions.width * dimensions.height);\n }\n\n /**\n * Palette slots an id raster can name, one of them the background. A GLSL\n * fragment stage is guaranteed only 224 uniform vectors, and a palette entry\n * costs 2.25 of them, so this stays a multiple of four and well inside that\n * budget; past it a frame draws from the RGBA composite instead.\n */\n const MAX_ID_MASK_PALETTE_ENTRIES = 80;\n const MAX_ID_MASK_STROKE_WIDTH = 16;\n function createIdMaskFrame(instructions, options = {}) {\n if (instructions.length === 0) {\n return undefined;\n }\n const maskWidth = Math.max(...instructions.map(({ mask }) => mask.width));\n const maskHeight = Math.max(...instructions.map(({ mask }) => mask.height));\n const width = resolveRasterWidth(maskWidth, options.maxWidth);\n const height = width === maskWidth\n ? maskHeight\n : Math.max(1, Math.round((width * maskHeight) / maskWidth));\n const scaledAxes = width === maskWidth\n ? undefined\n : createScaledMaskAxes({ height, maskHeight, maskWidth, width });\n const data = new Uint8Array(new ArrayBuffer(width * height));\n const fillPalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokePalette = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4 * 4));\n const strokeWidths = new Float32Array(new ArrayBuffer(MAX_ID_MASK_PALETTE_ENTRIES * 4));\n let hasStroke = false;\n let maxStrokeWidth = 0;\n for (const instruction of instructions) {\n const detectionMaskId = resolveIdMaskPaletteId(instruction.detectionIndex);\n if (detectionMaskId === undefined) {\n return undefined;\n }\n writeIdMaskPaletteEntry(fillPalette, detectionMaskId, instruction.color, instruction.alpha);\n if (instruction.stroke && instruction.stroke.width > 0) {\n const strokeWidth = resolveIdMaskStrokeTexels(instruction.stroke.width, maskWidth, width);\n hasStroke = true;\n strokeWidths[detectionMaskId] = strokeWidth;\n maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidth);\n writeIdMaskPaletteEntry(strokePalette, detectionMaskId, instruction.stroke.color, instruction.stroke.alpha);\n }\n if (scaledAxes) {\n writeScaledMaskRuns(data, scaledAxes, instruction.mask, detectionMaskId);\n }\n else {\n writeMaskRuns(data, width, instruction.mask, detectionMaskId);\n }\n }\n return {\n data,\n fillPalette,\n hasStroke,\n height,\n maxStrokeWidth,\n sourceWidth: maskWidth,\n strokePalette,\n strokeWidths,\n width,\n };\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function writeMaskRuns(data, frameWidth, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n let maskOffset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let frameOffset = y * frameWidth + x;\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[frameOffset] = detectionMaskId;\n y += 1;\n frameOffset += frameWidth;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n frameOffset = x;\n }\n }\n maskOffset += runLength;\n }\n }\n /**\n * The scaled twin of writeMaskRuns. Every source pixel marks the texel it lands\n * in, so a mask keeps every part of itself the smaller raster can hold and\n * gains up to a texel at its edges.\n */\n function writeScaledMaskRuns(data, axes, mask, detectionMaskId) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n let maskOffset = 0;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let x = Math.floor(maskOffset / maskHeight);\n let y = maskOffset - x * maskHeight;\n let column = axes.columns[x];\n for (let step = 0; step < runLength; step += 1) {\n if (x >= maskWidth) {\n break;\n }\n data[axes.rows[y] + column] = detectionMaskId;\n y += 1;\n if (y === maskHeight) {\n y = 0;\n x += 1;\n column = axes.columns[x];\n }\n }\n maskOffset += runLength;\n }\n }\n function resolveRasterWidth(maskWidth, maxWidth) {\n return maxWidth !== undefined && maxWidth > 0 && maxWidth < maskWidth\n ? Math.max(1, Math.floor(maxWidth))\n : maskWidth;\n }\n /**\n * A stroke is measured in texels of the raster it is drawn on, so a coarser\n * raster measures it in coarser texels. A stroke of a texel or more keeps at\n * least one, the thinnest line the shader can draw; a narrower one keeps its\n * own width, which the shader draws as an inner boundary at any scale. The\n * ceiling is the widest neighbourhood the shaders scan, so a wider stroke would\n * be drawn at the ceiling anyway and every layer drawing the same annotation\n * has to arrive at the same width.\n */\n function resolveIdMaskStrokeTexels(strokeWidth, maskWidth, rasterWidth) {\n const scale = maskWidth > 0 ? rasterWidth / maskWidth : 1;\n return Math.min(Math.max(strokeWidth * scale, Math.min(strokeWidth, 1)), MAX_ID_MASK_STROKE_WIDTH);\n }\n function createScaledMaskAxes(frame) {\n return {\n columns: createMaskAxisMap(frame.maskWidth, frame.width, 1),\n rows: createMaskAxisMap(frame.maskHeight, frame.height, frame.width),\n };\n }\n /** Rows hold their destination offset, so writing a run is two lookups. */\n function createMaskAxisMap(sourceLength, targetLength, stride) {\n const map = new Int32Array(sourceLength);\n const scale = targetLength / sourceLength;\n for (let index = 0; index < sourceLength; index += 1) {\n map[index] = Math.min(targetLength - 1, Math.floor(index * scale)) * stride;\n }\n return map;\n }\n /**\n * The palette slot a detection index names, or undefined when the palette has\n * no slot for it. Slot zero is the background, so the ids start at one and the\n * last detection the palette can name sits one index below the last slot.\n */\n function resolveIdMaskPaletteId(detectionIndex) {\n const detectionMaskId = detectionIndex + 1;\n return detectionMaskId > 0 && detectionMaskId < MAX_ID_MASK_PALETTE_ENTRIES\n ? detectionMaskId\n : undefined;\n }\n function writeIdMaskPaletteEntry(palette, detectionMaskId, color, alpha) {\n const offset = detectionMaskId * 4;\n palette[offset] = ((color >> 16) & 0xff) / 255;\n palette[offset + 1] = ((color >> 8) & 0xff) / 255;\n palette[offset + 2] = (color & 0xff) / 255;\n palette[offset + 3] = Math.max(0, Math.min(alpha, 1));\n }\n\n var MediaRendererFit;\n (function (MediaRendererFit) {\n /**\n * Preserve media aspect ratio and fit the full frame inside the canvas.\n */\n MediaRendererFit[\"Contain\"] = \"contain\";\n /**\n * Preserve media aspect ratio and fill the canvas, cropping if necessary.\n */\n MediaRendererFit[\"Cover\"] = \"cover\";\n })(MediaRendererFit || (MediaRendererFit = {}));\n /**\n * Playback lifecycle state reported by a platform renderer.\n */\n var MediaRendererPlaybackState;\n (function (MediaRendererPlaybackState) {\n MediaRendererPlaybackState[\"Loading\"] = \"loading\";\n MediaRendererPlaybackState[\"Ready\"] = \"ready\";\n MediaRendererPlaybackState[\"Playing\"] = \"playing\";\n MediaRendererPlaybackState[\"Buffering\"] = \"buffering\";\n MediaRendererPlaybackState[\"Paused\"] = \"paused\";\n MediaRendererPlaybackState[\"Error\"] = \"error\";\n MediaRendererPlaybackState[\"Destroyed\"] = \"destroyed\";\n })(MediaRendererPlaybackState || (MediaRendererPlaybackState = {}));\n /**\n * Stable classification of a media failure.\n *\n * Applications branch on these kinds instead of parsing decoder, demuxer, or\n * container error text, and still own their localized user-facing copy. New\n * kinds may be added over time, so treat unrecognized values like `Unknown`.\n */\n var MediaErrorKind;\n (function (MediaErrorKind) {\n /** The media could not be opened or read, or was refused before any read. */\n MediaErrorKind[\"Unreadable\"] = \"unreadable\";\n /** The container or codec is not supported by this platform. */\n MediaErrorKind[\"UnsupportedFormat\"] = \"unsupportedFormat\";\n /** The media opened but carries no usable video track. */\n MediaErrorKind[\"NoVideoTrack\"] = \"noVideoTrack\";\n /** Decoding a sample failed after the source opened. */\n MediaErrorKind[\"Decode\"] = \"decode\";\n /** The media could not be fetched. */\n MediaErrorKind[\"Network\"] = \"network\";\n /** The host environment lacks the APIs this media source requires. */\n MediaErrorKind[\"EnvironmentUnsupported\"] = \"environmentUnsupported\";\n /** The failure could not be classified. */\n MediaErrorKind[\"Unknown\"] = \"unknown\";\n })(MediaErrorKind || (MediaErrorKind = {}));\n /**\n * Lower-level media source readiness.\n */\n var MediaSourceStatus;\n (function (MediaSourceStatus) {\n MediaSourceStatus[\"Loading\"] = \"loading\";\n MediaSourceStatus[\"Ready\"] = \"ready\";\n MediaSourceStatus[\"Error\"] = \"error\";\n MediaSourceStatus[\"Destroyed\"] = \"destroyed\";\n })(MediaSourceStatus || (MediaSourceStatus = {}));\n /**\n * Current renderer state.\n */\n /**\n * What a playback gate holds back.\n *\n * A source the renderer pulls samples from can be held between any two frames,\n * because the renderer decides when each one is drawn. A source that presents\n * its own frames owns the playhead, so holding it means stopping the producer\n * and starting it again, and a gate that stops one has to bound its own wait\n * or a producer nothing answers for never runs again.\n */\n var PlaybackGateReach;\n (function (PlaybackGateReach) {\n /** No gate: the picture moves and unprepared layers are absent from it. */\n PlaybackGateReach[\"Off\"] = \"off\";\n /**\n * Playback waits to begin, and stops again at any frame whose artifacts are\n * missing, until they arrive or the gate's own wait bound gives up on them.\n */\n PlaybackGateReach[\"EveryFrame\"] = \"everyFrame\";\n })(PlaybackGateReach || (PlaybackGateReach = {}));\n\n /**\n * Media-session operating mode.\n *\n * Core owns the names because file-like and stream-like lifecycle choices are\n * platform-neutral. Platform packages decide how these modes tune media,\n * storage, and renderer defaults.\n */\n var MediaSessionMode;\n (function (MediaSessionMode) {\n /**\n * Finite media. Defaults usually favor seek/replay and persistent detection\n * storage.\n */\n MediaSessionMode[\"File\"] = \"file\";\n /**\n * Live or append-only media. Defaults usually favor rolling windows and\n * bounded retention.\n */\n MediaSessionMode[\"Stream\"] = \"stream\";\n })(MediaSessionMode || (MediaSessionMode = {}));\n /**\n * Aggregate lifecycle state for a media session.\n */\n var MediaSessionStatus;\n (function (MediaSessionStatus) {\n MediaSessionStatus[\"Buffering\"] = \"buffering\";\n MediaSessionStatus[\"Destroyed\"] = \"destroyed\";\n MediaSessionStatus[\"Error\"] = \"error\";\n MediaSessionStatus[\"Loading\"] = \"loading\";\n MediaSessionStatus[\"Paused\"] = \"paused\";\n MediaSessionStatus[\"Playing\"] = \"playing\";\n MediaSessionStatus[\"Processing\"] = \"processing\";\n MediaSessionStatus[\"Ready\"] = \"ready\";\n })(MediaSessionStatus || (MediaSessionStatus = {}));\n /**\n * Subsystem currently affecting session readiness or presentation.\n */\n var MediaSessionActivityKind;\n (function (MediaSessionActivityKind) {\n /**\n * Playback is held waiting for a producer to emit detections for the frame\n * about to be shown, which is a wait on inference rather than on transfer or\n * decode. `DetectionsBuffering` is the transfer of detections that already\n * exist, and `PlaybackBuffering` is the wait for media bytes.\n */\n MediaSessionActivityKind[\"DetectionsAwaitingCoverage\"] = \"detectionsAwaitingCoverage\";\n MediaSessionActivityKind[\"DetectionsBuffering\"] = \"detectionsBuffering\";\n MediaSessionActivityKind[\"DetectionsLoading\"] = \"detectionsLoading\";\n MediaSessionActivityKind[\"Error\"] = \"error\";\n MediaSessionActivityKind[\"MediaNormalizing\"] = \"mediaNormalizing\";\n MediaSessionActivityKind[\"MediaOpening\"] = \"mediaOpening\";\n /**\n * The picture is waiting on media the source has not handed over yet, which\n * is the bytes and their decode rather than anything downstream of them.\n * `PlaybackBuffering` is what a transport reports once it has already\n * stopped; this is reported from the read itself, including the reads a seek\n * makes while the transport still reads as paused.\n */\n MediaSessionActivityKind[\"MediaSourceReading\"] = \"mediaSourceReading\";\n MediaSessionActivityKind[\"PlaybackBuffering\"] = \"playbackBuffering\";\n /**\n * The render-preparation gate held the picture for artifacts that never\n * arrived and let it go. Nothing is blocked: the frames reaching the screen\n * are the ones whose artifacts were given up on. What is waited on is\n * preparation finishing another frame, which is what lets the gate hold the\n * picture again; `RenderPreparing` covers the holds that are running.\n */\n MediaSessionActivityKind[\"RenderPreparationAbandoned\"] = \"renderPreparationAbandoned\";\n MediaSessionActivityKind[\"RenderPreparing\"] = \"renderPreparing\";\n })(MediaSessionActivityKind || (MediaSessionActivityKind = {}));\n /**\n * State of one session activity.\n */\n var MediaSessionActivityStatus;\n (function (MediaSessionActivityStatus) {\n MediaSessionActivityStatus[\"Error\"] = \"error\";\n MediaSessionActivityStatus[\"Running\"] = \"running\";\n MediaSessionActivityStatus[\"Waiting\"] = \"waiting\";\n })(MediaSessionActivityStatus || (MediaSessionActivityStatus = {}));\n\n /** Builds compact, independent alpha crops so overlapping masks stay exact. */\n function createRegionMaskCoverageFrame(instructions) {\n const entries = [];\n for (const instruction of instructions) {\n if (!instruction.regionCoverageMask) {\n continue;\n }\n const entry = cropCoverageMask(instruction.detectionIndex, decodeCompressedRleMask(instruction.regionCoverageMask));\n if (entry) {\n entries.push(entry);\n }\n }\n return entries.length > 0 ? { entries } : undefined;\n }\n function cropCoverageMask(detectionIndex, decodedMask) {\n let minX = decodedMask.width;\n let minY = decodedMask.height;\n let maxX = -1;\n let maxY = -1;\n for (let y = 0; y < decodedMask.height; y += 1) {\n for (let x = 0; x < decodedMask.width; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n }\n if (maxX < minX || maxY < minY) {\n return undefined;\n }\n const width = maxX - minX + 1;\n const height = maxY - minY + 1;\n const data = new Uint8Array(new ArrayBuffer(width * height));\n for (let y = minY; y <= maxY; y += 1) {\n for (let x = minX; x <= maxX; x += 1) {\n if (!decodedMask.data[y * decodedMask.width + x]) {\n continue;\n }\n data[(y - minY) * width + (x - minX)] = 255;\n }\n }\n return { data, detectionIndex, height, width, x: minX, y: minY };\n }\n function compositeMaskFrame(instructions) {\n const maskInstructions = materializeMaskInstructions(instructions);\n if (maskInstructions.length === 0) {\n return undefined;\n }\n const width = Math.max(...maskInstructions.map(({ mask }) => mask.width));\n const height = Math.max(...maskInstructions.map(({ mask }) => mask.height));\n const data = new Uint8ClampedArray(new ArrayBuffer(width * height * 4));\n for (const instruction of maskInstructions) {\n compositeInstruction(data, width, instruction);\n }\n return { data, height, width };\n }\n function createIdMaskRasterFrame(instructions, maxRasterWidth) {\n try {\n return createIdMaskFrame(materializeMaskInstructions(instructions), {\n maxWidth: maxRasterWidth,\n });\n }\n catch {\n // The id raster is the fast path, not the only one: answering with nothing\n // puts the caller on the RGBA composite, which draws the same picture.\n return undefined;\n }\n }\n /**\n * Whether the palette has a slot for every detection this frame masks. An id\n * names a detection by its index in the frame, so one masked detection past the\n * last slot leaves the whole frame without an id raster however few masks it\n * carries.\n */\n function canIdMaskPaletteNameFrame(instructions) {\n return instructions.every((instruction) => instruction.visible === false ||\n resolveIdMaskPaletteId(instruction.detectionIndex) !== undefined);\n }\n function createIdMaskPlane(instructions, maxRasterWidth) {\n // The plane carries the ids a failed cook could not. A frame the palette\n // cannot name has none to carry, and cooking it again reaches the same\n // refusal after rasterizing every mask a second time.\n if (!canIdMaskPaletteNameFrame(instructions)) {\n return undefined;\n }\n const frame = createIdMaskRasterFrame(instructions, maxRasterWidth);\n return frame\n ? { data: frame.data, height: frame.height, width: frame.width }\n : undefined;\n }\n function compositeInstruction(rgba, canvasWidth, instruction) {\n const fill = resolveRgbaColor(instruction.color, instruction.alpha);\n const bounds = compositeMaskFill(rgba, canvasWidth, instruction.mask, fill);\n if (instruction.stroke && bounds) {\n compositeMaskStroke(rgba, canvasWidth, decodeCompressedRleMask(instruction.mask), bounds, instruction.stroke);\n }\n }\n function materializeMaskInstructions(instructions) {\n return instructions\n .filter((instruction) => instruction.visible !== false)\n .map((instruction) => {\n if (instruction.mask) {\n return instruction;\n }\n const { height, points, width } = instruction.polygon;\n return {\n alpha: instruction.alpha,\n color: instruction.color,\n detectionIndex: instruction.detectionIndex,\n mask: encodeBinaryMask(rasterizePolygonToMask(points, { height, width }), width, height),\n stroke: instruction.stroke,\n };\n });\n }\n /**\n * Compressed RLE counts runs down each column in turn, so a foreground run is\n * a contiguous walk down one column that wraps into the next.\n */\n function compositeMaskFill(rgba, canvasWidth, mask, fill) {\n const counts = decodeCompressedRleCounts(mask.counts);\n const maskWidth = mask.width;\n const maskHeight = mask.height;\n const maskArea = maskWidth * maskHeight;\n let maskOffset = 0;\n let minX = maskWidth;\n let minY = maskHeight;\n let maxX = -1;\n let maxY = -1;\n for (let index = 0; index < counts.length; index += 1) {\n const runLength = counts[index] ?? 0;\n if (index % 2 === 0 || runLength <= 0) {\n maskOffset += runLength;\n continue;\n }\n let columnX = Math.floor(maskOffset / maskHeight);\n let columnY = maskOffset - columnX * maskHeight;\n for (let step = 0; step < runLength; step += 1) {\n // A run can outlast the columns the mask has; the pixel it then names\n // is wherever that column-major offset lands when read back row-major.\n const rowMajorOffset = columnY * maskWidth + columnX;\n if (rowMajorOffset < maskArea) {\n const x = rowMajorOffset % maskWidth;\n const y = (rowMajorOffset - x) / maskWidth;\n writePixel(rgba, canvasWidth, x, y, fill);\n minX = Math.min(minX, x);\n minY = Math.min(minY, y);\n maxX = Math.max(maxX, x);\n maxY = Math.max(maxY, y);\n }\n columnY += 1;\n if (columnY === maskHeight) {\n columnY = 0;\n columnX += 1;\n }\n }\n maskOffset += runLength;\n }\n return maxX < minX || maxY < minY ? undefined : { maxX, maxY, minX, minY };\n }\n function compositeMaskStroke(rgba, canvasWidth, decodedMask, bounds, stroke) {\n const width = Math.round(stroke.width);\n if (width <= 0) {\n return;\n }\n const strokeColor = resolveRgbaColor(stroke.color, stroke.alpha);\n for (let y = bounds.minY; y <= bounds.maxY; y += 1) {\n for (let x = bounds.minX; x <= bounds.maxX; x += 1) {\n if (!isMaskPixel(decodedMask, x, y) ||\n !isBoundaryPixel(decodedMask, x, y)) {\n continue;\n }\n for (let offsetY = -width; offsetY <= width; offsetY += 1) {\n for (let offsetX = -width; offsetX <= width; offsetX += 1) {\n const strokeX = x + offsetX;\n const strokeY = y + offsetY;\n if (isOutsideMaskBounds(decodedMask, strokeX, strokeY) ||\n isMaskPixel(decodedMask, strokeX, strokeY)) {\n continue;\n }\n writePixel(rgba, canvasWidth, strokeX, strokeY, strokeColor);\n }\n }\n }\n }\n }\n function isBoundaryPixel(mask, x, y) {\n for (let offsetY = -1; offsetY <= 1; offsetY += 1) {\n for (let offsetX = -1; offsetX <= 1; offsetX += 1) {\n if (offsetX === 0 && offsetY === 0) {\n continue;\n }\n const neighborX = x + offsetX;\n const neighborY = y + offsetY;\n if (isOutsideMaskBounds(mask, neighborX, neighborY) ||\n !isMaskPixel(mask, neighborX, neighborY)) {\n return true;\n }\n }\n }\n return false;\n }\n function isMaskPixel(mask, x, y) {\n return mask.data[y * mask.width + x] === 1;\n }\n function isOutsideMaskBounds(mask, x, y) {\n return x < 0 || y < 0 || x >= mask.width || y >= mask.height;\n }\n function resolveRgbaColor(color, alpha) {\n return {\n alpha: Math.round(Math.max(0, Math.min(alpha, 1)) * 255),\n blue: color & 0xff,\n green: (color >> 8) & 0xff,\n red: (color >> 16) & 0xff,\n };\n }\n function writePixel(rgba, canvasWidth, x, y, color) {\n const rgbaOffset = (y * canvasWidth + x) * 4;\n rgba[rgbaOffset] = color.red;\n rgba[rgbaOffset + 1] = color.green;\n rgba[rgbaOffset + 2] = color.blue;\n rgba[rgbaOffset + 3] = color.alpha;\n }\n\n var PreparedMaskFrameKind;\n (function (PreparedMaskFrameKind) {\n PreparedMaskFrameKind[\"IdMask\"] = \"idMask\";\n PreparedMaskFrameKind[\"RgbaImage\"] = \"rgbaImage\";\n })(PreparedMaskFrameKind || (PreparedMaskFrameKind = {}));\n /**\n * How an id raster is laid out for the texture it is uploaded into. WebGPU\n * takes any bytesPerRow, so ids go up one byte per pixel. WebGL aligns every\n * uploaded row to four bytes and rejects a single-channel upload whose width\n * is not a multiple of four, which is what the four-channel layout is for.\n */\n var IdMaskTextureFormat;\n (function (IdMaskTextureFormat) {\n IdMaskTextureFormat[\"R8\"] = \"r8unorm\";\n IdMaskTextureFormat[\"Rgba8\"] = \"rgba8unorm\";\n })(IdMaskTextureFormat || (IdMaskTextureFormat = {}));\n\n var MaskPreparationWorkerMessageType;\n (function (MaskPreparationWorkerMessageType) {\n MaskPreparationWorkerMessageType[\"Complete\"] = \"complete\";\n MaskPreparationWorkerMessageType[\"Empty\"] = \"empty\";\n MaskPreparationWorkerMessageType[\"Error\"] = \"error\";\n MaskPreparationWorkerMessageType[\"Prepare\"] = \"prepare\";\n })(MaskPreparationWorkerMessageType || (MaskPreparationWorkerMessageType = {}));\n\n const workerScope = globalThis;\n workerScope.addEventListener(\"message\", (event) => {\n const message = event.data;\n if (message.type !== MaskPreparationWorkerMessageType.Prepare) {\n return;\n }\n prepareMaskFrame(message);\n });\n function prepareMaskFrame(message) {\n try {\n const regionMaskCoverage = createRegionMaskCoverageFrame(message.job.instructions);\n const coverageTransfers = getRegionMaskCoverageTransfers(regionMaskCoverage);\n const idMaskFrame = createIdMaskRasterFrame(message.job.instructions, message.job.maxRasterWidth);\n if (idMaskFrame) {\n workerScope.postMessage({\n artifactKind: PreparedMaskFrameKind.IdMask,\n fillPalette: idMaskFrame.fillPalette,\n hasStroke: idMaskFrame.hasStroke,\n height: idMaskFrame.height,\n key: message.job.key,\n maxStrokeWidth: idMaskFrame.maxStrokeWidth,\n raster: idMaskFrame.data,\n regionMaskCoverage,\n requestId: message.requestId,\n sourceWidth: idMaskFrame.sourceWidth,\n strokePalette: idMaskFrame.strokePalette,\n strokeWidths: idMaskFrame.strokeWidths,\n type: MaskPreparationWorkerMessageType.Complete,\n width: idMaskFrame.width,\n }, [\n idMaskFrame.data.buffer,\n idMaskFrame.fillPalette.buffer,\n idMaskFrame.strokePalette.buffer,\n idMaskFrame.strokeWidths.buffer,\n ...coverageTransfers,\n ]);\n return;\n }\n const compositedFrame = compositeMaskFrame(message.job.instructions);\n if (!compositedFrame && !regionMaskCoverage) {\n workerScope.postMessage({\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Empty,\n });\n return;\n }\n const preparedPixels = compositedFrame ?? createTransparentCoverageCarrier();\n const imageData = new ImageData(preparedPixels.data, preparedPixels.width, preparedPixels.height);\n const imageBitmap = createImageBitmapFromImageData(imageData);\n const idMaskPlane = createIdMaskPlane(message.job.instructions, message.job.maxRasterWidth);\n const idMaskTransfers = idMaskPlane ? [idMaskPlane.data.buffer] : [];\n if (imageBitmap) {\n workerScope.postMessage({\n idMaskPlane,\n imageBitmap,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [imageBitmap, ...idMaskTransfers, ...coverageTransfers]);\n return;\n }\n workerScope.postMessage({\n idMaskPlane,\n imageData,\n key: message.job.key,\n regionMaskCoverage,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Complete,\n }, [imageData.data.buffer, ...idMaskTransfers, ...coverageTransfers]);\n }\n catch (error) {\n workerScope.postMessage({\n error: error instanceof Error\n ? error.message\n : \"Unable to prepare mask frame.\",\n key: message.job.key,\n requestId: message.requestId,\n type: MaskPreparationWorkerMessageType.Error,\n });\n }\n }\n /**\n * A frame that carries only region coverage has nothing to composite, and the\n * RGBA branch still has to produce an artifact for the coverage to ride on.\n */\n function createTransparentCoverageCarrier() {\n return {\n data: new Uint8ClampedArray(new ArrayBuffer(4)),\n height: 1,\n width: 1,\n };\n }\n function getRegionMaskCoverageTransfers(coverage) {\n return coverage?.entries.map(({ data }) => data.buffer) ?? [];\n }\n function createImageBitmapFromImageData(imageData) {\n if (typeof OffscreenCanvas === \"undefined\") {\n return null;\n }\n const canvas = new OffscreenCanvas(imageData.width, imageData.height);\n const context = canvas.getContext(\"2d\");\n if (!context) {\n return null;\n }\n context.putImageData(imageData, 0, 0);\n return canvas.transferToImageBitmap();\n }\n\n})();";
5189
5089
 
5190
5090
  const DEFAULT_WORKER_NAME = "supervision-js-render-preparation";
5191
5091
  let defaultWorkerUrl;
@@ -5443,6 +5343,7 @@ function createPreparedIdMaskFrame(job, regionMaskCoverage) {
5443
5343
  maxStrokeWidth: frame.maxStrokeWidth,
5444
5344
  raster: frame.data,
5445
5345
  regionMaskCoverage,
5346
+ sourceWidth: frame.sourceWidth,
5446
5347
  strokePalette: frame.strokePalette,
5447
5348
  strokeWidths: frame.strokeWidths,
5448
5349
  width: frame.width,
@@ -5552,7 +5453,8 @@ function createPreparedFrameFromWorkerResponse(message) {
5552
5453
  !message.strokePalette ||
5553
5454
  !message.strokeWidths ||
5554
5455
  !message.width ||
5555
- !message.height) {
5456
+ !message.height ||
5457
+ !message.sourceWidth) {
5556
5458
  throw new Error("Mask preparation worker returned an incomplete ID mask artifact.");
5557
5459
  }
5558
5460
  return {
@@ -5565,6 +5467,7 @@ function createPreparedFrameFromWorkerResponse(message) {
5565
5467
  maxStrokeWidth: message.maxStrokeWidth ?? 0,
5566
5468
  raster: message.raster,
5567
5469
  regionMaskCoverage: message.regionMaskCoverage,
5470
+ sourceWidth: message.sourceWidth,
5568
5471
  strokePalette: message.strokePalette,
5569
5472
  strokeWidths: message.strokeWidths,
5570
5473
  width: message.width,
@@ -5721,9 +5624,9 @@ function resolvePreparedWindowFrameCount(options) {
5721
5624
  }
5722
5625
  function createPreparedRenderWindow(options) {
5723
5626
  const maskFrameOptions = options.renderPreparation?.maskFrame;
5724
- const maxMaskFrameCacheSize = Math.max(1, options.maxMaskFrameCacheSize ??
5627
+ const maxMaskFrameCacheSize = Math.max(1, Math.floor(options.maxMaskFrameCacheSize ??
5725
5628
  maskFrameOptions?.maxCacheFrameCount ??
5726
- DEFAULT_MASK_FRAME_CACHE_SIZE);
5629
+ DEFAULT_MASK_FRAME_CACHE_SIZE) || DEFAULT_MASK_FRAME_CACHE_SIZE);
5727
5630
  const prefetchFrameCount = resolvePreparedWindowFrameCount(options);
5728
5631
  const preparedWindowScanIntervalSeconds = Math.max(0, options.preparedWindowScanIntervalSeconds ??
5729
5632
  maskFrameOptions?.scanIntervalSeconds ??
@@ -5753,6 +5656,7 @@ function createPreparedRenderWindow(options) {
5753
5656
  let isPlayheadSettled = true;
5754
5657
  let isDestroyed = false;
5755
5658
  let generation = 0;
5659
+ let preparationProgress = 0;
5756
5660
  const preparedMaskFrames = new Map();
5757
5661
  const pendingMaskFrames = new Map();
5758
5662
  const queuedMaskFrameKeys = [];
@@ -5852,6 +5756,7 @@ function createPreparedRenderWindow(options) {
5852
5756
  });
5853
5757
  if (instructions.length === 0) {
5854
5758
  emptyMaskFrameKeys.add(key);
5759
+ preparationProgress += 1;
5855
5760
  pendingMaskFrames.delete(key);
5856
5761
  inFlightMaskFrames.delete(job);
5857
5762
  schedulePreparedTargetBatch();
@@ -5881,12 +5786,14 @@ function createPreparedRenderWindow(options) {
5881
5786
  }
5882
5787
  if (!maskFrame) {
5883
5788
  emptyMaskFrameKeys.add(key);
5789
+ preparationProgress += 1;
5884
5790
  schedulePreparedTargetBatch();
5885
5791
  emitDiagnostics();
5886
5792
  pumpMaskFrameQueue();
5887
5793
  return;
5888
5794
  }
5889
5795
  preparedMaskFrames.set(key, maskFrame);
5796
+ preparationProgress += 1;
5890
5797
  evictPreparedMaskFrames();
5891
5798
  options.onMaskFramePrepared?.(maskFrame);
5892
5799
  schedulePreparedTargetBatch();
@@ -6077,7 +5984,9 @@ function createPreparedRenderWindow(options) {
6077
5984
  if (isDestroyed || terminalPreparationError) {
6078
5985
  return;
6079
5986
  }
6080
- if (!isPlayheadSettled && !batchOptions.force) {
5987
+ /* A wait held at the gate does not ask again until it is let through, so
5988
+ the hold is what has to keep preparation running. */
5989
+ if (!isPlayheadSettled && !batchOptions.force && getGateHold() === null) {
6081
5990
  return;
6082
5991
  }
6083
5992
  let scheduledFrameCount = 0;
@@ -6098,6 +6007,9 @@ function createPreparedRenderWindow(options) {
6098
6007
  }
6099
6008
  return {
6100
6009
  getFrame,
6010
+ getPreparationProgress() {
6011
+ return preparationProgress;
6012
+ },
6101
6013
  isArtifactPrepared(mediaTime) {
6102
6014
  if (isDestroyed || !maskStyle) {
6103
6015
  return true;
@@ -6109,8 +6021,14 @@ function createPreparedRenderWindow(options) {
6109
6021
  return (getMaskStatus(getFrameKey(detectionFrame)) !==
6110
6022
  PreparedRenderFrameMaskStatus.Pending);
6111
6023
  },
6112
- waitForReady(mediaTime, waitOptions) {
6113
- if (waitOptions.enabled === false) {
6024
+ needsPlaybackGateWait(mediaTime, waitOptions) {
6025
+ if (isDestroyed || waitOptions.enabled === false) {
6026
+ return false;
6027
+ }
6028
+ return !isReadyForPresentation(mediaTime, getStopBelowSeconds(waitOptions));
6029
+ },
6030
+ waitForReady(mediaTime, waitOptions, signal) {
6031
+ if (waitOptions.enabled === false || signal?.aborted) {
6114
6032
  return Promise.resolve();
6115
6033
  }
6116
6034
  if (terminalPreparationError) {
@@ -6120,17 +6038,22 @@ function createPreparedRenderWindow(options) {
6120
6038
  if (terminalPreparationError) {
6121
6039
  return Promise.reject(terminalPreparationError);
6122
6040
  }
6123
- if (isReadyForPresentation(mediaTime, getMinimumAheadSeconds(waitOptions))) {
6041
+ if (isReadyForPresentation(mediaTime, getStopBelowSeconds(waitOptions))) {
6124
6042
  return Promise.resolve();
6125
6043
  }
6126
6044
  return new Promise((resolve, reject) => {
6127
6045
  const activeWait = {
6128
6046
  mediaTime,
6129
- requiredAheadSeconds: getRequiredAheadSeconds(waitOptions),
6047
+ resumeAtSeconds: getResumeAtSeconds(waitOptions),
6130
6048
  };
6131
6049
  const endWait = () => {
6132
6050
  readinessWaiters.delete(checkReady);
6133
6051
  activeReadinessWaits.delete(activeWait);
6052
+ signal?.removeEventListener("abort", abandonWait);
6053
+ };
6054
+ const abandonWait = () => {
6055
+ endWait();
6056
+ resolve();
6134
6057
  };
6135
6058
  const checkReady = () => {
6136
6059
  if (terminalPreparationError) {
@@ -6139,7 +6062,7 @@ function createPreparedRenderWindow(options) {
6139
6062
  return;
6140
6063
  }
6141
6064
  if (!isDestroyed &&
6142
- !isReadyForPresentation(mediaTime, activeWait.requiredAheadSeconds)) {
6065
+ !isReadyForPresentation(mediaTime, activeWait.resumeAtSeconds)) {
6143
6066
  return;
6144
6067
  }
6145
6068
  endWait();
@@ -6147,6 +6070,7 @@ function createPreparedRenderWindow(options) {
6147
6070
  };
6148
6071
  readinessWaiters.add(checkReady);
6149
6072
  activeReadinessWaits.add(activeWait);
6073
+ signal?.addEventListener("abort", abandonWait);
6150
6074
  emitDiagnostics();
6151
6075
  });
6152
6076
  },
@@ -6377,6 +6301,16 @@ function createPreparedRenderWindow(options) {
6377
6301
  }
6378
6302
  return getPreparedAheadDiagnosticsFor(activeMaskFrame);
6379
6303
  }
6304
+ /**
6305
+ * How far prepared work reaches in front of a frame, and how much of that
6306
+ * reach is finished. The walk crosses a frame the scheduler already holds
6307
+ * without counting it and stops at one nobody has asked for: a queued or
6308
+ * in-flight frame is runway that arrives on its own, while a gap no job
6309
+ * covers is runway that never fills. A source still appending detections
6310
+ * drops a fresh uncooked frame into this span on every record it writes.
6311
+ * Whether the frame about to be presented is itself ready is a separate
6312
+ * question, asked separately.
6313
+ */
6380
6314
  function getPreparedAheadDiagnosticsFor(frameRef) {
6381
6315
  const targetFrameIndex = lastPreparedTargetFrames.findIndex((frame) => getFrameKey(frame) === frameRef.key);
6382
6316
  const frames = targetFrameIndex >= 0
@@ -6392,11 +6326,14 @@ function createPreparedRenderWindow(options) {
6392
6326
  let latestPreparedTime = frameRef.mediaTime;
6393
6327
  for (const frame of frames.slice(activeFrameIndex)) {
6394
6328
  const key = getFrameKey(frame);
6395
- if (!preparedMaskFrames.has(key) && !emptyMaskFrameKeys.has(key)) {
6329
+ if (preparedMaskFrames.has(key) || emptyMaskFrameKeys.has(key)) {
6330
+ frameCount += 1;
6331
+ latestPreparedTime = frame.mediaTime;
6332
+ continue;
6333
+ }
6334
+ if (!pendingMaskFrames.has(key)) {
6396
6335
  break;
6397
6336
  }
6398
- frameCount += 1;
6399
- latestPreparedTime = frame.mediaTime;
6400
6337
  }
6401
6338
  return {
6402
6339
  frameCount,
@@ -6410,6 +6347,21 @@ function createPreparedRenderWindow(options) {
6410
6347
  }
6411
6348
  return lastPreparedWindowFrames.length - activeFrameIndex;
6412
6349
  }
6350
+ /**
6351
+ * The furthest a run of prepared frames starting here can ever reach. The run
6352
+ * is read out of a cache holding a fixed number of frames, and eviction takes
6353
+ * the frame farthest from the playhead, so a lead demanded beyond this span
6354
+ * is one no amount of preparation delivers. Unbounded while the playhead sits
6355
+ * outside the scanned window, the only place the span can be read from.
6356
+ */
6357
+ function getCacheReachableAheadSeconds(frameRef) {
6358
+ const activeFrameIndex = lastPreparedWindowFrames.findIndex((frame) => getFrameKey(frame) === frameRef.key);
6359
+ if (activeFrameIndex < 0) {
6360
+ return Number.POSITIVE_INFINITY;
6361
+ }
6362
+ const lastReachableFrame = lastPreparedWindowFrames[Math.min(lastPreparedWindowFrames.length, activeFrameIndex + maxMaskFrameCacheSize) - 1];
6363
+ return timeline.getFrameDistance(lastReachableFrame.mediaTime, frameRef.mediaTime);
6364
+ }
6413
6365
  function getPreparedTargetAheadSeconds(frameRef) {
6414
6366
  const activeFrameIndex = lastPreparedTargetFrames.findIndex((frame) => getFrameKey(frame) === frameRef.key);
6415
6367
  if (activeFrameIndex < 0) {
@@ -6441,7 +6393,7 @@ function createPreparedRenderWindow(options) {
6441
6393
  mediaTime: detectionFrame.mediaTime,
6442
6394
  };
6443
6395
  const activeStatus = getMaskStatus(frameRef.key);
6444
- const requiredLeadSeconds = Math.min(Math.max(requiredAheadSeconds, 0), getPreparedTargetAheadSeconds(frameRef));
6396
+ const requiredLeadSeconds = Math.min(Math.max(requiredAheadSeconds, 0), getPreparedTargetAheadSeconds(frameRef), getCacheReachableAheadSeconds(frameRef));
6445
6397
  if (activeStatus === PreparedRenderFrameMaskStatus.Pending) {
6446
6398
  return {
6447
6399
  reason: RenderPreparationGateHoldReason.ActiveFrameUnprepared,
@@ -6468,7 +6420,7 @@ function createPreparedRenderWindow(options) {
6468
6420
  */
6469
6421
  function getGateHold() {
6470
6422
  for (const wait of activeReadinessWaits) {
6471
- const hold = getPresentationHold(wait.mediaTime, wait.requiredAheadSeconds);
6423
+ const hold = getPresentationHold(wait.mediaTime, wait.resumeAtSeconds);
6472
6424
  if (hold) {
6473
6425
  return hold;
6474
6426
  }
@@ -6480,12 +6432,11 @@ function createPreparedRenderWindow(options) {
6480
6432
  waiter();
6481
6433
  }
6482
6434
  }
6483
- function getRequiredAheadSeconds(waitOptions) {
6484
- return Math.max(waitOptions.requiredAheadSeconds ?? 0, 0);
6435
+ function getResumeAtSeconds(waitOptions) {
6436
+ return Math.max(waitOptions.resumeAtSeconds, 0);
6485
6437
  }
6486
- function getMinimumAheadSeconds(waitOptions) {
6487
- const requiredAheadSeconds = getRequiredAheadSeconds(waitOptions);
6488
- return Math.min(Math.max(waitOptions.minimumAheadSeconds ?? requiredAheadSeconds, 0), requiredAheadSeconds);
6438
+ function getStopBelowSeconds(waitOptions) {
6439
+ return Math.min(Math.max(waitOptions.stopBelowSeconds, 0), getResumeAtSeconds(waitOptions));
6489
6440
  }
6490
6441
  function createPreparer() {
6491
6442
  return createMaskFramePreparer({
@@ -6815,6 +6766,35 @@ void main(void) {
6815
6766
  }
6816
6767
  `;
6817
6768
 
6769
+ /**
6770
+ * Pixi keys its program cache by source, so one program is shared by every
6771
+ * shader built from it. `destroy(true)` takes that program with it, leaving
6772
+ * both the peers still rendering through it and the next shader built from the
6773
+ * same source bound to nothing.
6774
+ */
6775
+ function destroyShaderKeepingProgram(shader) {
6776
+ try {
6777
+ shader.destroy();
6778
+ }
6779
+ catch {
6780
+ // Pixi has already invalidated this shader's resource group.
6781
+ }
6782
+ }
6783
+ /**
6784
+ * Pixi uploads a placeholder while it builds a shader's first bind group, and
6785
+ * WebGPU rejects a canvas that was never given a rendering context.
6786
+ */
6787
+ function createShaderPlaceholderCanvas() {
6788
+ if (typeof document === "undefined") {
6789
+ return { height: 1, width: 1 };
6790
+ }
6791
+ const canvas = document.createElement("canvas");
6792
+ canvas.height = 1;
6793
+ canvas.width = 1;
6794
+ canvas.getContext("2d");
6795
+ return canvas;
6796
+ }
6797
+
6818
6798
  /**
6819
6799
  * The halo a detection actually paints, or `undefined`. A style may answer an
6820
6800
  * instruction that draws nothing, and both the coverage the halo prepares and
@@ -6860,7 +6840,7 @@ function createPixiMaskHaloRenderer(options) {
6860
6840
  autoGenerateMipmaps: false,
6861
6841
  dynamic: false,
6862
6842
  height: 1,
6863
- resource: createPlaceholderCanvas$4(),
6843
+ resource: createShaderPlaceholderCanvas(),
6864
6844
  scaleMode: "nearest",
6865
6845
  width: 1,
6866
6846
  });
@@ -6886,9 +6866,7 @@ function createPixiMaskHaloRenderer(options) {
6886
6866
  destroy() {
6887
6867
  for (const pass of passes) {
6888
6868
  pass.mesh.destroy();
6889
- // Never destroy(true): the program cache is keyed by source and shared
6890
- // by every shader built from it, including other halos still rendering.
6891
- pass.shader.destroy();
6869
+ destroyShaderKeepingProgram(pass.shader);
6892
6870
  }
6893
6871
  passes.length = 0;
6894
6872
  display.destroy();
@@ -6952,12 +6930,7 @@ function createPixiMaskHaloRenderer(options) {
6952
6930
  pass.shader.resources.uSampler = source.style;
6953
6931
  }
6954
6932
  catch {
6955
- try {
6956
- pass.shader.destroy();
6957
- }
6958
- catch {
6959
- // Pixi has already invalidated this shader's resource group.
6960
- }
6933
+ destroyShaderKeepingProgram(pass.shader);
6961
6934
  pass.shader = createShader(pass.uniforms);
6962
6935
  pass.mesh.shader = pass.shader;
6963
6936
  pass.shader.resources.uTexture = source;
@@ -7007,15 +6980,6 @@ function buildMaskHaloPalette(halos) {
7007
6980
  }
7008
6981
  return palette;
7009
6982
  }
7010
- function createPlaceholderCanvas$4() {
7011
- const canvas = document.createElement("canvas");
7012
- canvas.height = 1;
7013
- canvas.width = 1;
7014
- // WebGPU rejects a canvas that was never given a rendering context, and Pixi
7015
- // uploads this placeholder while it builds the shader's first bind group.
7016
- canvas.getContext("2d");
7017
- return canvas;
7018
- }
7019
6983
  const maskHaloFragmentShader = `#version 300 es
7020
6984
  precision highp float;
7021
6985
  precision highp int;
@@ -7358,6 +7322,7 @@ function createPixiFocusLayer(options) {
7358
7322
  let cutoutMediaTime = null;
7359
7323
  let cutoutFrameTime = null;
7360
7324
  let drawnOverlayWithoutCutout = null;
7325
+ let isDrawnFocusCleared = false;
7361
7326
  let isHoldingOverlay = false;
7362
7327
  let holdStartedAtMs = null;
7363
7328
  // A frame can fall back to a composited RGBA texture when its colored ID-mask
@@ -7406,7 +7371,7 @@ function createPixiFocusLayer(options) {
7406
7371
  return;
7407
7372
  }
7408
7373
  if (!context.frame) {
7409
- holdOverlay(context.mediaTime, context.heldMaskFrameTime ?? null);
7374
+ holdOverlay(context.mediaTime, context.heldMaskFrameTime ?? null, context.isMaskArtifactOwed === true);
7410
7375
  return;
7411
7376
  }
7412
7377
  const resolvedInstruction = focusStyle.resolve({
@@ -7607,7 +7572,7 @@ function createPixiFocusLayer(options) {
7607
7572
  * Everything dimmed for a moment reads as a pause; the picture flashing to
7608
7573
  * full brightness and back reads as a fault.
7609
7574
  */
7610
- function holdOverlay(mediaTime, heldMaskFrameTime) {
7575
+ function holdOverlay(mediaTime, heldMaskFrameTime, isMaskArtifactOwed) {
7611
7576
  if (!heldFill) {
7612
7577
  transitionToHidden();
7613
7578
  return;
@@ -7619,6 +7584,12 @@ function createPixiFocusLayer(options) {
7619
7584
  if (isDrawnCutoutStillCurrent(mediaTime, heldMaskFrameTime)) {
7620
7585
  return;
7621
7586
  }
7587
+ if (isMaskArtifactOwed) {
7588
+ // The mask is absent from this frame as well, so a dim with no hole cut
7589
+ // in it darkens the subject itself: a harsher absence than no focus.
7590
+ clearDrawnFocus();
7591
+ return;
7592
+ }
7622
7593
  drawOverlayWithoutCutout(heldFill);
7623
7594
  }
7624
7595
  /**
@@ -7650,11 +7621,13 @@ function createPixiFocusLayer(options) {
7650
7621
  }
7651
7622
  }
7652
7623
  function markCutoutDrawn(mediaTime, frameTime) {
7624
+ isDrawnFocusCleared = false;
7653
7625
  cutoutMediaTime = mediaTime;
7654
7626
  cutoutFrameTime = frameTime;
7655
7627
  drawnOverlayWithoutCutout = null;
7656
7628
  }
7657
7629
  function resetHeldFocus() {
7630
+ isDrawnFocusCleared = false;
7658
7631
  cutoutMediaTime = null;
7659
7632
  cutoutFrameTime = null;
7660
7633
  drawnOverlayWithoutCutout = null;
@@ -7671,11 +7644,23 @@ function createPixiFocusLayer(options) {
7671
7644
  return (idMaskRenderer?.isDrawnFrameIntact() === true ||
7672
7645
  focusGraphics?.visible === true);
7673
7646
  }
7647
+ function clearDrawnFocus() {
7648
+ if (isDrawnFocusCleared) {
7649
+ return;
7650
+ }
7651
+ isDrawnFocusCleared = true;
7652
+ cutoutMediaTime = null;
7653
+ cutoutFrameTime = null;
7654
+ drawnOverlayWithoutCutout = null;
7655
+ idMaskRenderer?.hide();
7656
+ hideVectorFocus();
7657
+ }
7674
7658
  function drawOverlayWithoutCutout(fill) {
7675
7659
  if (drawnOverlayWithoutCutout?.alpha === fill.alpha &&
7676
7660
  drawnOverlayWithoutCutout.color === fill.color) {
7677
7661
  return;
7678
7662
  }
7663
+ isDrawnFocusCleared = false;
7679
7664
  cutoutMediaTime = null;
7680
7665
  cutoutFrameTime = null;
7681
7666
  drawnOverlayWithoutCutout = fill;
@@ -7775,7 +7760,7 @@ function createFocusIdMaskRenderer(options) {
7775
7760
  autoGenerateMipmaps: false,
7776
7761
  dynamic: false,
7777
7762
  height: 1,
7778
- resource: createPlaceholderCanvas$3(),
7763
+ resource: createShaderPlaceholderCanvas(),
7779
7764
  scaleMode: "nearest",
7780
7765
  width: 1,
7781
7766
  });
@@ -7807,7 +7792,7 @@ function createFocusIdMaskRenderer(options) {
7807
7792
  return {
7808
7793
  destroy() {
7809
7794
  mesh.destroy();
7810
- shader.destroy();
7795
+ destroyShaderKeepingProgram(shader);
7811
7796
  geometry.destroy();
7812
7797
  placeholderSource.destroy();
7813
7798
  },
@@ -7907,14 +7892,7 @@ function createFocusIdMaskRenderer(options) {
7907
7892
  });
7908
7893
  }
7909
7894
  function rebuildShader() {
7910
- try {
7911
- // Never destroy(true): the program cache is keyed by source and shared
7912
- // by every shader built from it; a destroyed entry poisons the rebuild.
7913
- shader.destroy();
7914
- }
7915
- catch {
7916
- // Pixi has already invalidated this shader's resource group.
7917
- }
7895
+ destroyShaderKeepingProgram(shader);
7918
7896
  shader = createShader();
7919
7897
  mesh.shader = shader;
7920
7898
  }
@@ -7926,15 +7904,6 @@ function writePremultipliedColor(target, fill) {
7926
7904
  target[2] = ((fill.color & 0xff) / 255) * alpha;
7927
7905
  target[3] = alpha;
7928
7906
  }
7929
- function createPlaceholderCanvas$3() {
7930
- const canvas = document.createElement("canvas");
7931
- canvas.height = 1;
7932
- canvas.width = 1;
7933
- // WebGPU rejects a canvas that was never given a rendering context, and Pixi
7934
- // uploads this placeholder while it builds the shader's first bind group.
7935
- canvas.getContext("2d");
7936
- return canvas;
7937
- }
7938
7907
  const focusIdMaskFragmentShader = `#version 300 es
7939
7908
  precision highp float;
7940
7909
  precision highp int;
@@ -9212,7 +9181,6 @@ function detectionKey(detection, detectionIndex) {
9212
9181
  : `id:${String(detection.id)}`;
9213
9182
  }
9214
9183
 
9215
- const MAX_INTERACTION_STROKE_RADIUS = 12;
9216
9184
  function createPixiInteractionPresentationLayer(options) {
9217
9185
  let interactionStyle = options.interactionStyle === undefined
9218
9186
  ? new BaseInteractionStyle()
@@ -9603,7 +9571,7 @@ function createInteractionMaskRenderer(options) {
9603
9571
  autoGenerateMipmaps: false,
9604
9572
  dynamic: false,
9605
9573
  height: 1,
9606
- resource: createPlaceholderCanvas$2(),
9574
+ resource: createShaderPlaceholderCanvas(),
9607
9575
  scaleMode: "nearest",
9608
9576
  width: 1,
9609
9577
  });
@@ -9629,7 +9597,7 @@ function createInteractionMaskRenderer(options) {
9629
9597
  return {
9630
9598
  destroy() {
9631
9599
  mesh.destroy();
9632
- shader.destroy();
9600
+ destroyShaderKeepingProgram(shader);
9633
9601
  geometry.destroy();
9634
9602
  placeholderSource.destroy();
9635
9603
  },
@@ -9648,14 +9616,16 @@ function createInteractionMaskRenderer(options) {
9648
9616
  strokeWidths.fill(0);
9649
9617
  let maxStrokeWidth = 0;
9650
9618
  for (const { detectionIndex, instruction } of instructions) {
9651
- const maskId = detectionIndex + 1;
9652
- if (maskId <= 0 || maskId >= MAX_ID_MASK_PALETTE_ENTRIES) {
9619
+ const maskId = resolveIdMaskPaletteId(detectionIndex);
9620
+ // A raster naming this detection is one the palette accepted, so an id
9621
+ // it cannot hold is an id no texel of this raster carries either.
9622
+ if (maskId === undefined) {
9653
9623
  continue;
9654
9624
  }
9655
- writePaletteEntry(fillPalette, maskId, instruction.color, instruction.alpha);
9625
+ writeIdMaskPaletteEntry(fillPalette, maskId, instruction.color, instruction.alpha);
9656
9626
  if (instruction.stroke && instruction.stroke.width > 0) {
9657
- writePaletteEntry(strokePalette, maskId, instruction.stroke.color, instruction.stroke.alpha);
9658
- strokeWidths[maskId] = Math.min(resolveRasterStrokeTexels(instruction.stroke.width, instruction.mask.width, frame.width), MAX_INTERACTION_STROKE_RADIUS);
9627
+ writeIdMaskPaletteEntry(strokePalette, maskId, instruction.stroke.color, instruction.stroke.alpha);
9628
+ strokeWidths[maskId] = resolveIdMaskStrokeTexels(instruction.stroke.width, frame.sourceWidth, frame.width);
9659
9629
  maxStrokeWidth = Math.max(maxStrokeWidth, strokeWidths[maskId] ?? 0);
9660
9630
  }
9661
9631
  }
@@ -9666,7 +9636,7 @@ function createInteractionMaskRenderer(options) {
9666
9636
  frame.width,
9667
9637
  frame.height,
9668
9638
  ]);
9669
- uniforms.uniforms.uMaxStrokeWidth = Math.min(maxStrokeWidth, MAX_INTERACTION_STROKE_RADIUS);
9639
+ uniforms.uniforms.uMaxStrokeWidth = maxStrokeWidth;
9670
9640
  uniforms.update();
9671
9641
  mesh.visible = true;
9672
9642
  },
@@ -9706,46 +9676,11 @@ function createInteractionMaskRenderer(options) {
9706
9676
  });
9707
9677
  }
9708
9678
  function rebuildShader() {
9709
- try {
9710
- // Never destroy(true): the program cache is keyed by source and shared
9711
- // by every shader built from it; a destroyed entry poisons the rebuild.
9712
- shader.destroy();
9713
- }
9714
- catch {
9715
- // Pixi has already invalidated this shader's resource group.
9716
- }
9679
+ destroyShaderKeepingProgram(shader);
9717
9680
  shader = createShader();
9718
9681
  mesh.shader = shader;
9719
9682
  }
9720
9683
  }
9721
- /**
9722
- * A stroke is measured in texels of the raster the shader samples, so a raster
9723
- * cooked below the mask's own resolution measures it in coarser texels. A
9724
- * stroke of a texel or more keeps at least one, the thinnest line the shader
9725
- * can draw; a narrower one keeps its own width, which the shader draws as an
9726
- * inner boundary at any scale.
9727
- */
9728
- function resolveRasterStrokeTexels(strokeWidth, maskWidth, rasterWidth) {
9729
- const scale = maskWidth > 0 ? rasterWidth / maskWidth : 1;
9730
- return Math.max(strokeWidth * scale, Math.min(strokeWidth, 1));
9731
- }
9732
- function writePaletteEntry(palette, maskId, color, alpha) {
9733
- const offset = maskId * 4;
9734
- const clampedAlpha = Math.max(0, Math.min(alpha, 1));
9735
- palette[offset] = ((color >> 16) & 0xff) / 255;
9736
- palette[offset + 1] = ((color >> 8) & 0xff) / 255;
9737
- palette[offset + 2] = (color & 0xff) / 255;
9738
- palette[offset + 3] = clampedAlpha;
9739
- }
9740
- function createPlaceholderCanvas$2() {
9741
- const canvas = document.createElement("canvas");
9742
- canvas.height = 1;
9743
- canvas.width = 1;
9744
- // WebGPU rejects a canvas that was never given a rendering context, and Pixi
9745
- // uploads this placeholder while it builds the shader's first bind group.
9746
- canvas.getContext("2d");
9747
- return canvas;
9748
- }
9749
9684
  const interactionMaskFragmentShader = `#version 300 es
9750
9685
  precision highp float;
9751
9686
  precision highp int;
@@ -9776,7 +9711,7 @@ bool differs(float left, float right) {
9776
9711
  // The winning candidate is the max-(offsetY, offsetX) passing offset, so the
9777
9712
  // scan runs backwards and exits on the first hit.
9778
9713
  float findNeighborStrokeId(float centerId, vec2 texel) {
9779
- int radius = int(min(uMaxStrokeWidth, float(${MAX_INTERACTION_STROKE_RADIUS})));
9714
+ int radius = int(min(uMaxStrokeWidth, float(${MAX_ID_MASK_STROKE_WIDTH})));
9780
9715
 
9781
9716
  for (int offsetY = radius; offsetY >= -radius; offsetY -= 1) {
9782
9717
  for (int offsetX = radius; offsetX >= -radius; offsetX -= 1) {
@@ -9873,7 +9808,7 @@ fn differs(left: f32, right: f32) -> bool {
9873
9808
  // The winning candidate is the max-(offsetY, offsetX) passing offset, so the
9874
9809
  // scan runs backwards and exits on the first hit.
9875
9810
  fn findNeighborStrokeId(uv: vec2<f32>, centerId: f32, texel: vec2<f32>) -> f32 {
9876
- let radius = i32(min(maskUniforms.uMaxStrokeWidth, ${MAX_INTERACTION_STROKE_RADIUS}.0));
9811
+ let radius = i32(min(maskUniforms.uMaxStrokeWidth, ${MAX_ID_MASK_STROKE_WIDTH}.0));
9877
9812
 
9878
9813
  for (var offsetY = radius; offsetY >= -radius; offsetY -= 1) {
9879
9814
  for (var offsetX = radius; offsetX >= -radius; offsetX -= 1) {
@@ -9970,7 +9905,7 @@ function createPixiIdMaskShaderRenderer(options) {
9970
9905
  autoGenerateMipmaps: false,
9971
9906
  dynamic: false,
9972
9907
  height: 1,
9973
- resource: createPlaceholderCanvas$1(),
9908
+ resource: createShaderPlaceholderCanvas(),
9974
9909
  scaleMode: "nearest",
9975
9910
  width: 1,
9976
9911
  });
@@ -9999,7 +9934,7 @@ function createPixiIdMaskShaderRenderer(options) {
9999
9934
  },
10000
9935
  destroy() {
10001
9936
  mesh.destroy();
10002
- shader.destroy();
9937
+ destroyShaderKeepingProgram(shader);
10003
9938
  geometry.destroy();
10004
9939
  placeholderSource.destroy();
10005
9940
  },
@@ -10060,27 +9995,11 @@ function createPixiIdMaskShaderRenderer(options) {
10060
9995
  });
10061
9996
  }
10062
9997
  function rebuildShader() {
10063
- try {
10064
- // Never destroy(true): the program cache is keyed by source and shared
10065
- // by every shader built from it; a destroyed entry poisons the rebuild.
10066
- shader.destroy();
10067
- }
10068
- catch {
10069
- // Pixi has already invalidated this shader's resource group.
10070
- }
9998
+ destroyShaderKeepingProgram(shader);
10071
9999
  shader = createShader();
10072
10000
  mesh.shader = shader;
10073
10001
  }
10074
10002
  }
10075
- function createPlaceholderCanvas$1() {
10076
- const canvas = document.createElement("canvas");
10077
- canvas.height = 1;
10078
- canvas.width = 1;
10079
- // WebGPU rejects a canvas that was never given a rendering context, and Pixi
10080
- // uploads this placeholder while it builds the shader's first bind group.
10081
- canvas.getContext("2d");
10082
- return canvas;
10083
- }
10084
10003
  const idMaskFragmentShader = `#version 300 es
10085
10004
  precision highp float;
10086
10005
  precision highp int;
@@ -10278,6 +10197,21 @@ fn mainFragment(
10278
10197
  `;
10279
10198
 
10280
10199
  const TEXTURE_ROW_ALIGNMENT_BYTES = 4;
10200
+ /**
10201
+ * Whether the frame on screen can answer for detection ids. Hover, selection
10202
+ * and focus highlighting all cut their shapes out of the id raster, so a frame
10203
+ * that reaches the screen without one draws its whole picture while those three
10204
+ * have nothing to cut from.
10205
+ */
10206
+ var PixiMaskLayerIdMaskStatus;
10207
+ (function (PixiMaskLayerIdMaskStatus) {
10208
+ /** A mask frame is on screen and carries no id raster. */
10209
+ PixiMaskLayerIdMaskStatus["Absent"] = "absent";
10210
+ /** No mask frame is on screen. */
10211
+ PixiMaskLayerIdMaskStatus["None"] = "none";
10212
+ /** The id raster is on screen. */
10213
+ PixiMaskLayerIdMaskStatus["Present"] = "present";
10214
+ })(PixiMaskLayerIdMaskStatus || (PixiMaskLayerIdMaskStatus = {}));
10281
10215
  /**
10282
10216
  * Cook-only style for consumers that read detection ids out of the raster
10283
10217
  * instead of drawing the fill. Ids come from the detection index, so the colour
@@ -10364,15 +10298,14 @@ function createPixiMaskLayer(options) {
10364
10298
  showMaskFrame(preparedFrame.maskFrame, preparedFrame.detectionFrame.mediaTime, presentedFrameId);
10365
10299
  return;
10366
10300
  }
10367
- if (!preparedFrame || !canHoldVisibleMaskOver(preparedFrame)) {
10368
- hideSprite();
10369
- }
10301
+ hideSprite();
10370
10302
  },
10371
10303
  getDrawnState() {
10372
10304
  return {
10373
10305
  drawnFrameTime: visibleMaskMediaTime,
10374
10306
  drawnFrameKey: visibleMaskFrameKey,
10375
10307
  drawnFrameId: visibleMaskFrameId,
10308
+ idMaskStatus: resolveIdMaskStatus(),
10376
10309
  };
10377
10310
  },
10378
10311
  prepareFrame(mediaTime) {
@@ -10384,8 +10317,14 @@ function createPixiMaskLayer(options) {
10384
10317
  isArtifactPrepared(mediaTime) {
10385
10318
  return preparedRenderWindow.isArtifactPrepared(mediaTime);
10386
10319
  },
10387
- waitForRenderPreparation(mediaTime, gateOptions) {
10388
- return preparedRenderWindow.waitForReady(mediaTime, gateOptions);
10320
+ getPreparationProgress() {
10321
+ return preparedRenderWindow.getPreparationProgress();
10322
+ },
10323
+ needsRenderPreparationWait(mediaTime, gateOptions) {
10324
+ return preparedRenderWindow.needsPlaybackGateWait(mediaTime, gateOptions);
10325
+ },
10326
+ waitForRenderPreparation(mediaTime, gateOptions, signal) {
10327
+ return preparedRenderWindow.waitForReady(mediaTime, gateOptions, signal);
10389
10328
  },
10390
10329
  pickDetectionAtPoint(point, mediaTime) {
10391
10330
  const preparedFrame = preparedRenderWindow.getFrame(mediaTime);
@@ -10488,6 +10427,14 @@ function createPixiMaskLayer(options) {
10488
10427
  }
10489
10428
  showRgbaMaskFrame(maskFrame);
10490
10429
  }
10430
+ function resolveIdMaskStatus() {
10431
+ if (activeIdMaskFrame) {
10432
+ return PixiMaskLayerIdMaskStatus.Present;
10433
+ }
10434
+ return activeRgbaMaskFrame
10435
+ ? PixiMaskLayerIdMaskStatus.Absent
10436
+ : PixiMaskLayerIdMaskStatus.None;
10437
+ }
10491
10438
  function getTexture(maskFrame) {
10492
10439
  const existingTexture = maskTextures.get(maskFrame.key);
10493
10440
  if (existingTexture) {
@@ -10588,26 +10535,6 @@ function createPixiMaskLayer(options) {
10588
10535
  }
10589
10536
  idMaskRenderer?.hide();
10590
10537
  }
10591
- /**
10592
- * Whether the raster on screen may stand over a frame whose own cook has not
10593
- * landed, which it may for exactly the frame beside its own.
10594
- *
10595
- * The bound is counted in frames of the timeline and never in seconds. A span
10596
- * of seconds only means one frame at one frame rate and one playback rate, so
10597
- * it has to be scaled by the rate to go on meaning it, and that scaling is
10598
- * what once left a raster twelve frames old on the picture after a fast run.
10599
- */
10600
- function canHoldVisibleMaskOver(preparedFrame) {
10601
- return (preparedFrame.maskStatus === PreparedRenderFrameMaskStatus.Pending &&
10602
- visibleMaskMediaTime !== null &&
10603
- isNeighbouringDetectionFrame(visibleMaskMediaTime, preparedFrame.detectionFrame.mediaTime));
10604
- }
10605
- function isNeighbouringDetectionFrame(mediaTime, otherMediaTime) {
10606
- const frames = getBufferedDetectionTimelineFrameSnapshot(options.detectionTimeline);
10607
- const index = indexOfDetectionFrameAt(frames, mediaTime);
10608
- const otherIndex = indexOfDetectionFrameAt(frames, otherMediaTime);
10609
- return (index !== -1 && otherIndex !== -1 && Math.abs(index - otherIndex) === 1);
10610
- }
10611
10538
  /**
10612
10539
  * Everything the layer hands other layers is a view of the raster on screen,
10613
10540
  * so a draw that puts none up has to drop them all in the same step.
@@ -10893,26 +10820,6 @@ function expandIdsToRgba(ids) {
10893
10820
  }
10894
10821
  return rgba;
10895
10822
  }
10896
- /** Where the frame sits in the timeline's buffer, which is held in media-time
10897
- * order, or -1 for a time no frame of detections sits on. */
10898
- function indexOfDetectionFrameAt(frames, mediaTime) {
10899
- let low = 0;
10900
- let high = frames.length - 1;
10901
- while (low <= high) {
10902
- const middle = (low + high) >>> 1;
10903
- const frameTime = frames[middle].mediaTime;
10904
- if (frameTime === mediaTime) {
10905
- return middle;
10906
- }
10907
- if (frameTime < mediaTime) {
10908
- low = middle + 1;
10909
- }
10910
- else {
10911
- high = middle - 1;
10912
- }
10913
- }
10914
- return -1;
10915
- }
10916
10823
 
10917
10824
  function createPixiMaskBrushPreview(options) {
10918
10825
  const { editor } = options.preview;
@@ -11014,6 +10921,7 @@ function createPixiPolygonLayer(options) {
11014
10921
  },
11015
10922
  clearFrame: rasterLayer.clearFrame,
11016
10923
  isArtifactPrepared: rasterLayer.isArtifactPrepared,
10924
+ getPreparationProgress: rasterLayer.getPreparationProgress,
11017
10925
  getVectorFallbackStyle() {
11018
10926
  return vectorFallbackStyle;
11019
10927
  },
@@ -11025,6 +10933,7 @@ function createPixiPolygonLayer(options) {
11025
10933
  styleVersion += 1;
11026
10934
  rasterLayer.setMaskStyle(nextPolygonStyle ? createArtifactStyle() : nextPolygonStyle);
11027
10935
  },
10936
+ needsRenderPreparationWait: rasterLayer.needsRenderPreparationWait,
11028
10937
  setTimelineContext: rasterLayer.setTimelineContext,
11029
10938
  waitForRenderPreparation: rasterLayer.waitForRenderPreparation,
11030
10939
  };
@@ -11168,7 +11077,7 @@ function createPixiRegionCoverageMask(options) {
11168
11077
  autoGenerateMipmaps: false,
11169
11078
  dynamic: false,
11170
11079
  height: 1,
11171
- resource: createPlaceholderCanvas(),
11080
+ resource: createShaderPlaceholderCanvas(),
11172
11081
  scaleMode: "nearest",
11173
11082
  width: 1,
11174
11083
  });
@@ -11186,9 +11095,7 @@ function createPixiRegionCoverageMask(options) {
11186
11095
  destroy() {
11187
11096
  effect.destroy();
11188
11097
  display.destroy();
11189
- // Never destroy(true): the program cache is keyed by source and shared
11190
- // by every shader built from it; a destroyed entry poisons the rebuild.
11191
- shader.destroy();
11098
+ destroyShaderKeepingProgram(shader);
11192
11099
  geometry.destroy();
11193
11100
  placeholderSource.destroy();
11194
11101
  },
@@ -11220,14 +11127,7 @@ function createPixiRegionCoverageMask(options) {
11220
11127
  shader.resources.uSampler = source.style;
11221
11128
  }
11222
11129
  catch {
11223
- try {
11224
- // Never destroy(true): the program cache is keyed by source and shared
11225
- // by every shader built from it; a destroyed entry poisons the rebuild.
11226
- shader.destroy();
11227
- }
11228
- catch {
11229
- // Pixi may already have invalidated this shader resource group.
11230
- }
11130
+ destroyShaderKeepingProgram(shader);
11231
11131
  shader = createShader();
11232
11132
  display.shader = shader;
11233
11133
  shader.resources.uTexture = source;
@@ -11258,18 +11158,6 @@ function createPixiRegionCoverageMask(options) {
11258
11158
  });
11259
11159
  }
11260
11160
  }
11261
- function createPlaceholderCanvas() {
11262
- if (typeof document === "undefined") {
11263
- return { height: 1, width: 1 };
11264
- }
11265
- const canvas = document.createElement("canvas");
11266
- canvas.height = 1;
11267
- canvas.width = 1;
11268
- // WebGPU builds this placeholder into the shader's first bind group, and a
11269
- // canvas that was never given a rendering context has nothing to bind.
11270
- canvas.getContext("2d");
11271
- return canvas;
11272
- }
11273
11161
  const regionCoverageMaskVertexShader = `#version 300 es
11274
11162
  precision highp float;
11275
11163
 
@@ -12601,12 +12489,8 @@ const REGION_COVERAGE_ONLY_MASK_STYLE = {
12601
12489
  * media time, so anything past a fade's length lands the overlay at once.
12602
12490
  */
12603
12491
  const STATIC_FOCUS_SETTLE_MS = 10_000;
12604
- /**
12605
- * Presentation fields a render answers to. The style half comes from the
12606
- * renderer registry, so a renderer kind added there joins it on its own.
12607
- */
12608
- const RENDERED_PRESENTATION_FIELDS = [
12609
- ...resolveAnnotationRendererStyleFields(annotationRendererKinds),
12492
+ /** The presentation fields a render answers to that no renderer kind owns. */
12493
+ const RENDERED_PRESENTATION_TAIL = [
12610
12494
  "annotationOverlayStyle",
12611
12495
  "backgroundColor",
12612
12496
  "focusStyle",
@@ -12614,6 +12498,14 @@ const RENDERED_PRESENTATION_FIELDS = [
12614
12498
  "renderers",
12615
12499
  "visibility",
12616
12500
  ];
12501
+ /**
12502
+ * Presentation fields a render answers to. The style half comes from the
12503
+ * renderer registry, so a renderer kind added there joins it on its own.
12504
+ */
12505
+ const RENDERED_PRESENTATION_FIELDS = [
12506
+ ...resolveAnnotationRendererStyleFields(annotationRendererKinds),
12507
+ ...RENDERED_PRESENTATION_TAIL,
12508
+ ];
12617
12509
  /** Several steps share a bucket: the public timings name fewer than the walk draws. */
12618
12510
  const FRAME_DRAW_TIMING_BUCKETS = {
12619
12511
  drawBox: "boxMs",
@@ -12706,7 +12598,7 @@ async function createPixiMediaScene(options) {
12706
12598
  let isPresenting = false;
12707
12599
  let isDestroyed = false;
12708
12600
  let displayFrameHandle = null;
12709
- let hasDeferredPresentRender = false;
12601
+ let deferredPresentedFrame = null;
12710
12602
  /**
12711
12603
  * The detection under an editing gesture. The base layers hide it while the
12712
12604
  * annotation overlay draws its live preview, so an edit never shows beside a
@@ -12855,6 +12747,8 @@ async function createPixiMediaScene(options) {
12855
12747
  autoDensity: true,
12856
12748
  autoStart: frameChannel === undefined,
12857
12749
  backgroundColor: options.backgroundColor ?? 0x111111,
12750
+ /* Only the video-engine media path supplies a presented-frame channel, so
12751
+ * which reader opens the clip is also what decides which backend draws. */
12858
12752
  preference: frameChannel ? "webgpu" : RENDER_ENGINE_PREFERENCE,
12859
12753
  resolution: resolvePixiResolution(options.maxDevicePixelRatio),
12860
12754
  });
@@ -13443,10 +13337,20 @@ async function createPixiMediaScene(options) {
13443
13337
  source: readPresentedSurface(),
13444
13338
  });
13445
13339
  },
13446
- waitForRenderPreparation(mediaTime, gateOptions) {
13340
+ getRenderPreparationProgress() {
13341
+ return ((maskLayer?.getPreparationProgress() ?? 0) +
13342
+ (polygonLayer?.getPreparationProgress() ?? 0));
13343
+ },
13344
+ needsRenderPreparationWait(mediaTime, gateOptions) {
13345
+ return (maskLayer?.needsRenderPreparationWait(mediaTime, gateOptions) ===
13346
+ true ||
13347
+ polygonLayer?.needsRenderPreparationWait(mediaTime, gateOptions) ===
13348
+ true);
13349
+ },
13350
+ waitForRenderPreparation(mediaTime, gateOptions, signal) {
13447
13351
  return Promise.all([
13448
- maskLayer?.waitForRenderPreparation(mediaTime, gateOptions),
13449
- polygonLayer?.waitForRenderPreparation(mediaTime, gateOptions),
13352
+ maskLayer?.waitForRenderPreparation(mediaTime, gateOptions, signal),
13353
+ polygonLayer?.waitForRenderPreparation(mediaTime, gateOptions, signal),
13450
13354
  ]).then(() => undefined);
13451
13355
  },
13452
13356
  setRenderQuality(maxDevicePixelRatio) {
@@ -13694,6 +13598,8 @@ async function createPixiMediaScene(options) {
13694
13598
  cancelDisplayFrame(displayFrameHandle);
13695
13599
  displayFrameHandle = null;
13696
13600
  }
13601
+ deferredPresentedFrame?.frame.close();
13602
+ deferredPresentedFrame = null;
13697
13603
  unsubscribeDetectionTimeline?.();
13698
13604
  disconnectContainerResizeObserver();
13699
13605
  app.cancelResize?.();
@@ -14132,7 +14038,7 @@ async function createPixiMediaScene(options) {
14132
14038
  return;
14133
14039
  }
14134
14040
  if (isFocusArtifactOwed(mediaTime)) {
14135
- drawFocusWithNothingToCut(mediaTime, maskLayer?.getDrawnState().drawnFrameTime ?? null);
14041
+ drawFocusWithNothingToCut(mediaTime, maskLayer?.getDrawnState().drawnFrameTime ?? null, true);
14136
14042
  return;
14137
14043
  }
14138
14044
  const frame = annotationDetectionTimeline.selectFrame(mediaTime);
@@ -14147,11 +14053,12 @@ async function createPixiMediaScene(options) {
14147
14053
  viewportScale,
14148
14054
  });
14149
14055
  }
14150
- function drawFocusWithNothingToCut(mediaTime, heldMaskFrameTime) {
14056
+ function drawFocusWithNothingToCut(mediaTime, heldMaskFrameTime, isMaskArtifactOwed = false) {
14151
14057
  focusLayer?.drawFrame({
14152
14058
  frame: undefined,
14153
14059
  heldMaskFrameTime,
14154
14060
  hoveredPick: null,
14061
+ isMaskArtifactOwed,
14155
14062
  mediaTime,
14156
14063
  selectedPick: null,
14157
14064
  });
@@ -14188,8 +14095,7 @@ async function createPixiMediaScene(options) {
14188
14095
  }
14189
14096
  redrawAnnotationsNow();
14190
14097
  }
14191
- /** Redraws the frame on screen whether or not its readiness moved, for a
14192
- * layer that went stale on wall clock rather than on new data. */
14098
+ /** Redraws the frame on screen whether or not its readiness moved. */
14193
14099
  function redrawAnnotationsNow() {
14194
14100
  if (isPresenting || isDestroyed) {
14195
14101
  return;
@@ -14271,10 +14177,21 @@ async function createPixiMediaScene(options) {
14271
14177
  return frameChannel ? currentMediaTime * MILLISECONDS_PER_SECOND : now();
14272
14178
  }
14273
14179
  function handlePresentedFrame(presented) {
14274
- if (!mediaCompositor) {
14180
+ if (!mediaCompositor || isDestroyed) {
14275
14181
  presented.frame.close();
14276
14182
  return;
14277
14183
  }
14184
+ // The producer may outrun the display refresh. Keep only its newest frame
14185
+ // for the next refresh, and never acknowledge a superseded frame as
14186
+ // presented: neither its pixels nor its annotations reached the canvas.
14187
+ if (displayFrameHandle !== null) {
14188
+ deferredPresentedFrame?.frame.close();
14189
+ deferredPresentedFrame = presented;
14190
+ return;
14191
+ }
14192
+ presentFrameNow(presented);
14193
+ }
14194
+ function presentFrameNow(presented) {
14278
14195
  // Scheduling a cook notifies, and a notification that drew or rendered
14279
14196
  // here would put a second render inside one present.
14280
14197
  isPresenting = true;
@@ -14307,26 +14224,26 @@ async function createPixiMediaScene(options) {
14307
14224
  /**
14308
14225
  * Renders a present at most once per display refresh. Frames arrive as
14309
14226
  * messages from the producer's own thread, so a main thread that falls
14310
- * behind takes a whole burst in one refresh and submits a scene per frame
14311
- * that only the last of can reach the screen. Deferring costs no latency:
14312
- * the skipped render is replaced before the refresh it would have made.
14227
+ * behind keeps only the latest frame for the next refresh. Coalescing before
14228
+ * the atomic present is important: a skipped frame must not upload pixels,
14229
+ * draw annotations, or advance the producer's presented playhead.
14313
14230
  */
14314
14231
  function renderPresent() {
14315
- if (displayFrameHandle !== null) {
14316
- hasDeferredPresentRender = true;
14317
- return;
14318
- }
14319
14232
  renderScene();
14320
- displayFrameHandle = requestDisplayFrame(flushDeferredPresentRender);
14233
+ displayFrameHandle = requestDisplayFrame(flushDeferredPresentedFrame);
14321
14234
  }
14322
- function flushDeferredPresentRender() {
14235
+ function flushDeferredPresentedFrame() {
14323
14236
  displayFrameHandle = null;
14324
- if (!hasDeferredPresentRender || isDestroyed) {
14237
+ const deferred = deferredPresentedFrame;
14238
+ deferredPresentedFrame = null;
14239
+ if (!deferred) {
14325
14240
  return;
14326
14241
  }
14327
- hasDeferredPresentRender = false;
14328
- renderScene();
14329
- displayFrameHandle = requestDisplayFrame(flushDeferredPresentRender);
14242
+ if (isDestroyed) {
14243
+ deferred.frame.close();
14244
+ return;
14245
+ }
14246
+ presentFrameNow(deferred);
14330
14247
  }
14331
14248
  function renderNow() {
14332
14249
  if (frameChannel)
@@ -14715,14 +14632,14 @@ const DEFAULT_FRAME_RATE = 30;
14715
14632
  * The refresh interval rebuilds a window that already covers the playhead. A gap
14716
14633
  * the window does not reach reloads immediately whatever this says, so this only
14717
14634
  * decides how often covered ground is derived again, and a file's detections do
14718
- * not change under it. Rebuilding a 10 second window every half second cost 15
14635
+ * not change under it. Rebuilding an 8 second window every half second cost 15
14719
14636
  * rebuilds a second at 8x playback, each re-deriving a window overlapping the
14720
14637
  * one it replaced by 95%. A stream keeps the short interval below, because there
14721
14638
  * the source really does gain data.
14722
14639
  */
14723
14640
  const FILE_DETECTION_BUFFER_DEFAULTS = {
14724
14641
  bufferAheadSeconds: 10,
14725
- bufferBehindSeconds: 0.5,
14642
+ bufferBehindSeconds: 5,
14726
14643
  refreshIntervalSeconds: 2.5,
14727
14644
  };
14728
14645
  const STREAM_DETECTION_BUFFER_DEFAULTS = {
@@ -14738,12 +14655,15 @@ const STREAM_DETECTION_BUFFER_DEFAULTS = {
14738
14655
  */
14739
14656
  const DETECTION_PLAYBACK_GATE_DEFAULTS = {
14740
14657
  enabled: true,
14658
+ maxWaitSeconds: 10,
14741
14659
  requiredAheadSeconds: 2,
14742
14660
  };
14743
14661
  const RENDER_PREPARATION_PLAYBACK_GATE_DEFAULTS = {
14744
14662
  enabled: true,
14745
- minimumAheadSeconds: 0.25,
14663
+ maxWaitSeconds: DEFAULT_RENDER_PREPARATION_GATE_MAX_WAIT_SECONDS,
14746
14664
  requiredAheadSeconds: 1,
14665
+ resumeMarginWallSeconds: DEFAULT_RENDER_PREPARATION_GATE_RESUME_MARGIN_WALL_SECONDS,
14666
+ stopBelowWallSeconds: DEFAULT_RENDER_PREPARATION_GATE_STOP_BELOW_WALL_SECONDS,
14747
14667
  };
14748
14668
  const MASK_FRAME_DEFAULTS = {
14749
14669
  maxPendingFrameCount: 24,
@@ -14784,6 +14704,11 @@ function resolveMediaSessionDefaults(options) {
14784
14704
  ...(detectionPlaybackGate
14785
14705
  ? {
14786
14706
  playbackGate: {
14707
+ // The detection timeline applies this bound even when a caller
14708
+ // enables only the specific gate and leaves the session switch
14709
+ // unset. Resolving it here makes the session snapshot describe the
14710
+ // runtime that will actually open.
14711
+ maxWaitSeconds: DETECTION_PLAYBACK_GATE_DEFAULTS.maxWaitSeconds,
14787
14712
  ...(inheritsGateLookahead
14788
14713
  ? DETECTION_PLAYBACK_GATE_DEFAULTS
14789
14714
  : undefined),
@@ -15171,13 +15096,24 @@ function createMediaSessionStateSnapshot({ errorMessage, media, normalization, r
15171
15096
  const stoppedForPlayback = renderer?.playbackState === MediaRendererPlaybackState.Buffering;
15172
15097
  const awaitingCoverage = renderer?.detectionBuffer.status === DetectionBufferStatus.AwaitingCoverage;
15173
15098
  const loadingDetections = renderer?.detectionBuffer.status === DetectionBufferStatus.Loading;
15099
+ const awaitingSourceRead = renderer?.source.awaitingRead === true;
15174
15100
  const preparingActiveFrame = (renderPreparation?.artifacts ?? []).some((artifact) => artifact.activeFrame?.status ===
15175
15101
  RenderPreparationArtifactFrameStatus.Pending ||
15176
15102
  Boolean(artifact.gateHold));
15103
+ if (awaitingSourceRead) {
15104
+ activities.push(createActivity({
15105
+ blockingPlayback: true,
15106
+ detail: "The video for this part has not arrived yet",
15107
+ kind: MediaSessionActivityKind.MediaSourceReading,
15108
+ label: "Loading the video",
15109
+ status: MediaSessionActivityStatus.Waiting,
15110
+ }));
15111
+ }
15177
15112
  // A picture stopped for its annotations is not stopped for its own bytes, and
15178
15113
  // a host shown both reads the vaguer one first and tells the viewer the wrong
15179
15114
  // thing. Only the reason nothing more specific claims is reported here.
15180
15115
  if (stoppedForPlayback &&
15116
+ !awaitingSourceRead &&
15181
15117
  !awaitingCoverage &&
15182
15118
  !loadingDetections &&
15183
15119
  !preparingActiveFrame) {
@@ -15219,42 +15155,44 @@ function createMediaSessionStateSnapshot({ errorMessage, media, normalization, r
15219
15155
  const totalCount = artifact.pendingCount + artifact.preparedCount;
15220
15156
  const activeFrameIsPending = artifact.activeFrame?.status ===
15221
15157
  RenderPreparationArtifactFrameStatus.Pending;
15222
- // A gate banking a lead in front of a finished frame leaves nothing else to
15223
- // report: the frame is prepared and the queue can be empty, so a loop that
15224
- // reads only those two says nothing and the wait is described upstream as a
15225
- // transfer that is not happening.
15226
- const holdingForLead = artifact.gateHold?.reason ===
15227
- RenderPreparationGateHoldReason.LeadBelowRequirement;
15158
+ const gateHold = artifact.gateHold ?? null;
15159
+ const leadHold = gateHold?.reason === RenderPreparationGateHoldReason.LeadBelowRequirement
15160
+ ? gateHold
15161
+ : null;
15162
+ // The gate holds the frame about to be presented, which is not the frame on
15163
+ // screen: a hold can name an unprepared frame ahead while the presented one
15164
+ // is prepared and the queue behind it is empty. Reading only those two says
15165
+ // nothing at all, and the picture stops with no activity to explain it.
15166
+ const waitingForFrame = activeFrameIsPending || (gateHold !== null && leadHold === null);
15228
15167
  if (artifact.pendingCount <= 0 &&
15229
15168
  !activeFrameIsPending &&
15230
- !holdingForLead) {
15169
+ gateHold === null) {
15231
15170
  continue;
15232
15171
  }
15233
- const holdingPlayback = (activeFrameIsPending || Boolean(artifact.gateHold)) &&
15234
- stoppedForPlayback;
15235
- const waiting = activeFrameIsPending || holdingForLead;
15172
+ const holdingPlayback = (activeFrameIsPending || gateHold !== null) && stoppedForPlayback;
15173
+ const waiting = activeFrameIsPending || gateHold !== null;
15236
15174
  activities.push(createActivity({
15237
15175
  artifactKind: artifact.kind,
15238
15176
  blockingPlayback: holdingPlayback,
15239
15177
  blockingPresentation: activeFrameIsPending,
15240
- detail: activeFrameIsPending
15241
- ? holdingPlayback
15178
+ detail: leadHold
15179
+ ? `Starting again at ${leadHold.requiredAheadSeconds.toFixed(1)}s of masks ready`
15180
+ : holdingPlayback && waitingForFrame
15242
15181
  ? "The masks for this frame are not drawn yet"
15243
- : `Active frame ${artifact.activeFrame.mediaTime.toFixed(3)}s is waiting for ${artifact.kind}`
15244
- : holdingForLead
15245
- ? "This frame is ready; the video starts once enough is drawn ahead of it"
15246
- : null,
15182
+ : activeFrameIsPending
15183
+ ? `Active frame ${artifact.activeFrame.mediaTime.toFixed(3)}s is waiting for ${artifact.kind}`
15184
+ : null,
15247
15185
  kind: MediaSessionActivityKind.RenderPreparing,
15248
- label: activeFrameIsPending
15249
- ? holdingPlayback
15250
- ? "Waiting for the masks"
15251
- : "Preparing active render artifact"
15252
- : holdingForLead
15253
- ? "Drawing ahead of the video"
15186
+ label: leadHold
15187
+ ? "Catching the masks up"
15188
+ : waitingForFrame
15189
+ ? holdingPlayback
15190
+ ? "Waiting for the masks"
15191
+ : "Preparing active render artifact"
15254
15192
  : "Preparing render artifacts",
15255
15193
  pendingCount: artifact.pendingCount,
15256
15194
  preparedCount: artifact.preparedCount,
15257
- progress: holdingForLead
15195
+ progress: leadHold
15258
15196
  ? leadProgress(artifact)
15259
15197
  : totalCount > 0
15260
15198
  ? artifact.preparedCount / totalCount
@@ -15264,6 +15202,14 @@ function createMediaSessionStateSnapshot({ errorMessage, media, normalization, r
15264
15202
  : MediaSessionActivityStatus.Running,
15265
15203
  }));
15266
15204
  }
15205
+ if (renderer?.renderPreparationGateAbandoned === true) {
15206
+ activities.push(createActivity({
15207
+ detail: "The video is playing without them",
15208
+ kind: MediaSessionActivityKind.RenderPreparationAbandoned,
15209
+ label: "Masks could not keep up",
15210
+ status: MediaSessionActivityStatus.Waiting,
15211
+ }));
15212
+ }
15267
15213
  if (renderer?.playbackState === MediaRendererPlaybackState.Error) {
15268
15214
  activities.push(createActivity({
15269
15215
  blockingPlayback: true,
@@ -15786,5 +15732,5 @@ function getErrorMessage(error, fallback) {
15786
15732
  return error instanceof Error ? error.message : fallback;
15787
15733
  }
15788
15734
 
15789
- export { ConversionFrameEffect, DEFAULT_NORMALIZATION_FRAME_RATE, DetectionPostProcessingMode, DetectionTimelineOrigin, MediaConditionCode, MediaConditionResponse, MediaConditionScope, MediaIndexPlacement, MediaNormalizationAudioCodec, MediaNormalizationContainer, MediaNormalizationFit, MediaNormalizationVideoCodec, MediaPreparationError, MediaProbeIssueCode, MediaProbeStatus, MediaSessionMediaBranch, MediaSourceError, RenderPreparationArtifactFrameStatus, RenderPreparationArtifactKind, RenderPreparationExecutionMode, RenderPreparationMode, RenderPreparationWorkerStatus, createBrowserColdDetectionFrameStore, createChunkedDetectionFrameSource, createDefaultDetectionPostProcessingWorkerFactory, createDetectionPostProcessingPipeline, createImageUrlMediaSource, createMediaRenderer, createMediaSession, createMediaStreamRendererSource, createStaticImageMediaSource, createVideoEngineMediaRendererSource, describeConversionFrameEffect, evaluateMediaConditions, getMediaErrorKind, isMediaSourceError, normalizeMedia, normalizeMediaProgressively, openVideoEngineMediaSource, prepareMedia, prepareMediaProgressively, probeMedia, probeMediaConditions, readIndexPlacement, resolveMediaSessionDefaults, summarizeTiming, toMediaSourceError };
15735
+ export { DEFAULT_NORMALIZATION_FRAME_RATE, DetectionPostProcessingMode, DetectionTimelineOrigin, MediaNormalizationAudioCodec, MediaNormalizationContainer, MediaNormalizationFit, MediaNormalizationVideoCodec, MediaPreparationError, MediaProbeIssueCode, MediaProbeStatus, MediaSessionMediaBranch, MediaSourceError, RenderPreparationArtifactFrameStatus, RenderPreparationArtifactKind, RenderPreparationExecutionMode, RenderPreparationMode, RenderPreparationWorkerStatus, createBrowserColdDetectionFrameStore, createChunkedDetectionFrameSource, createDefaultDetectionPostProcessingWorkerFactory, createDetectionPostProcessingPipeline, createImageUrlMediaSource, createMediaRenderer, createMediaSession, createMediaStreamRendererSource, createStaticImageMediaSource, normalizeMedia, normalizeMediaProgressively, prepareMedia, prepareMediaProgressively, probeMedia, resolveMediaSessionDefaults, toMediaSourceError };
15790
15736
  //# sourceMappingURL=index.js.map