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.
- package/dist/constants/media-renderer.d.ts +3 -0
- package/dist/constants/media-renderer.d.ts.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1246 -1300
- package/dist/index.js.map +1 -1
- package/dist/mask-preparation.worker.js +109 -31
- package/dist/mask-preparation.worker.js.map +1 -1
- package/dist/media/media-source-state.d.ts.map +1 -1
- package/dist/media/video-engine-media-source.d.ts +17 -7
- package/dist/media/video-engine-media-source.d.ts.map +1 -1
- package/dist/media/video-engine-media-source.js +3 -0
- package/dist/media/video-engine-media-source.js.map +1 -0
- package/dist/playback/media-playback-controller.d.ts +7 -1
- package/dist/playback/media-playback-controller.d.ts.map +1 -1
- package/dist/render-preparation/mask-frame-artifact.d.ts +2 -0
- package/dist/render-preparation/mask-frame-artifact.d.ts.map +1 -1
- package/dist/render-preparation/mask-frame-compositor.d.ts +7 -0
- package/dist/render-preparation/mask-frame-compositor.d.ts.map +1 -1
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts +1 -0
- package/dist/render-preparation/mask-preparation-worker-protocol.d.ts.map +1 -1
- package/dist/render-preparation/prepared-render-window.d.ts +21 -2
- package/dist/render-preparation/prepared-render-window.d.ts.map +1 -1
- package/dist/renderers/injected-pixi.d.ts +38 -0
- package/dist/renderers/injected-pixi.d.ts.map +1 -0
- package/dist/renderers/media-renderer-core.d.ts +9 -0
- package/dist/renderers/media-renderer-core.d.ts.map +1 -1
- package/dist/renderers/media-renderer-scene.d.ts +16 -5
- package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
- package/dist/renderers/media-renderer-state.d.ts +9 -0
- package/dist/renderers/media-renderer-state.d.ts.map +1 -1
- package/dist/renderers/media-renderer-transport.d.ts +16 -3
- package/dist/renderers/media-renderer-transport.d.ts.map +1 -1
- package/dist/renderers/pixi-focus-layer.d.ts +1 -0
- package/dist/renderers/pixi-focus-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-frame-present.d.ts +1 -1
- package/dist/renderers/pixi-frame-present.d.ts.map +1 -1
- package/dist/renderers/pixi-id-mask-shader.d.ts +4 -33
- package/dist/renderers/pixi-id-mask-shader.d.ts.map +1 -1
- package/dist/renderers/pixi-interaction-presentation-layer.d.ts +0 -1
- package/dist/renderers/pixi-interaction-presentation-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-halo.d.ts +4 -33
- package/dist/renderers/pixi-mask-halo.d.ts.map +1 -1
- package/dist/renderers/pixi-mask-layer.d.ts +28 -29
- package/dist/renderers/pixi-mask-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
- package/dist/renderers/pixi-polygon-layer.d.ts +2 -0
- package/dist/renderers/pixi-polygon-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-region-coverage-mask.d.ts +4 -33
- package/dist/renderers/pixi-region-coverage-mask.d.ts.map +1 -1
- package/dist/renderers/pixi-region-layer.d.ts +6 -31
- package/dist/renderers/pixi-region-layer.d.ts.map +1 -1
- package/dist/renderers/pixi-shader-lifecycle.d.ts +14 -0
- package/dist/renderers/pixi-shader-lifecycle.d.ts.map +1 -0
- package/dist/renderers/presented-frame-channel.d.ts +32 -1
- package/dist/renderers/presented-frame-channel.d.ts.map +1 -1
- package/dist/sessions/media-session-defaults.d.ts.map +1 -1
- package/dist/sessions/media-session-state.d.ts.map +1 -1
- package/dist/tracking.worker.js +19 -5
- package/dist/tracking.worker.js.map +1 -1
- package/dist/types/media-session.d.ts +20 -27
- package/dist/types/media-session.d.ts.map +1 -1
- package/dist/types/render-preparation.d.ts +70 -23
- package/dist/types/render-preparation.d.ts.map +1 -1
- package/dist/video-engine-media-source-CUXnOJOV.js +450 -0
- package/dist/video-engine-media-source-CUXnOJOV.js.map +1 -0
- package/dist/web-video-engine/analysis-session.d.ts +0 -1
- package/dist/web-video-engine/analysis.d.ts +0 -1
- package/dist/web-video-engine/analysis.js +195 -23
- package/dist/web-video-engine/cache-budget.d.ts +0 -1
- package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts +0 -1
- package/dist/web-video-engine/clock.d.ts +0 -1
- package/dist/web-video-engine/constants.d.ts +9 -1
- package/dist/web-video-engine/create-scrub-cursor.d.ts +6 -3
- package/dist/web-video-engine/decode-resolution.d.ts +0 -1
- package/dist/web-video-engine/decode-scheduler.d.ts +18 -3
- package/dist/web-video-engine/decode-session.d.ts +8 -1
- package/dist/web-video-engine/decode-source.d.ts +9 -1
- package/dist/web-video-engine/diagnostics-store.d.ts +0 -1
- package/dist/web-video-engine/diagnostics.d.ts +19 -19
- package/dist/web-video-engine/embedded-engine-worker.d.ts +0 -1
- package/dist/web-video-engine/engine-core.d.ts +22 -11
- package/dist/web-video-engine/engine.d.ts +3 -4
- package/dist/web-video-engine/engine.js +76 -37
- package/dist/web-video-engine/engine.worker.d.ts +0 -1
- package/dist/web-video-engine/engine.worker.js +5501 -3197
- package/dist/web-video-engine/frame-cache.d.ts +119 -12
- package/dist/web-video-engine/frame-extractor.d.ts +0 -1
- package/dist/web-video-engine/{frame-timeline-GINI8Gum.js → frame-timeline-DreEwsRY.js} +83 -27
- package/dist/web-video-engine/frame-timeline.d.ts +10 -1
- package/dist/web-video-engine/frame-walker.d.ts +0 -1
- package/dist/web-video-engine/index.d.ts +12 -2
- package/dist/web-video-engine/index.d.ts.map +1 -1
- package/dist/web-video-engine/index.js +2 -2
- package/dist/web-video-engine/key-packet.d.ts +0 -1
- package/dist/web-video-engine/keyframe-index.d.ts +0 -1
- package/dist/web-video-engine/mirror-store.d.ts +0 -1
- package/dist/web-video-engine/renderer.d.ts +0 -1
- package/dist/web-video-engine/rotation.d.ts +0 -1
- package/dist/web-video-engine/scrub-controller.d.ts +0 -1
- package/dist/web-video-engine/scrub-cursor.d.ts +12 -7
- package/dist/web-video-engine/scrub-trajectory.d.ts +0 -1
- package/dist/web-video-engine/source-residency.d.ts +35 -1
- package/dist/web-video-engine/trace-recorder.d.ts +0 -1
- package/dist/web-video-engine/types.d.ts +18 -22
- package/dist/web-video-engine/video-engine.d.ts +27 -16
- package/dist/web-video-engine/webgpu-renderer.d.ts +0 -1
- package/dist/web-video-engine/worker-bridge.d.ts +0 -1
- package/dist/web-video-engine/worker-dispatch.d.ts +0 -1
- package/dist/web-video-engine/worker-protocol.d.ts +29 -14
- package/node_modules/supervision-js-core/dist/detections/buffered-detection-timeline.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.d.ts +2 -2
- package/node_modules/supervision-js-core/dist/index.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.js +107 -29
- package/node_modules/supervision-js-core/dist/index.js.map +1 -1
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +12 -0
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts +28 -26
- package/node_modules/supervision-js-core/dist/types/media-rendering.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/types/session-lifecycle.d.ts +16 -0
- package/node_modules/supervision-js-core/dist/types/session-lifecycle.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/detection-masks.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts +19 -0
- package/node_modules/supervision-js-core/dist/utils/id-mask-frame.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/media/media-condition-probe.d.ts +0 -51
- package/dist/media/media-condition-probe.d.ts.map +0 -1
- package/dist/media/media-conditions.d.ts +0 -28
- package/dist/media/media-conditions.d.ts.map +0 -1
- package/dist/types/media-conditions.d.ts +0 -149
- package/dist/types/media-conditions.d.ts.map +0 -1
- package/dist/web-video-engine/analysis-session.d.ts.map +0 -1
- package/dist/web-video-engine/analysis.d.ts.map +0 -1
- package/dist/web-video-engine/analysis.js.map +0 -1
- package/dist/web-video-engine/cache-budget.d.ts.map +0 -1
- package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts.map +0 -1
- package/dist/web-video-engine/clock.d.ts.map +0 -1
- package/dist/web-video-engine/constants.d.ts.map +0 -1
- package/dist/web-video-engine/create-scrub-cursor.d.ts.map +0 -1
- package/dist/web-video-engine/decode-resolution.d.ts.map +0 -1
- package/dist/web-video-engine/decode-scheduler.d.ts.map +0 -1
- package/dist/web-video-engine/decode-session.d.ts.map +0 -1
- package/dist/web-video-engine/decode-source.d.ts.map +0 -1
- package/dist/web-video-engine/diagnostics-store.d.ts.map +0 -1
- package/dist/web-video-engine/diagnostics.d.ts.map +0 -1
- package/dist/web-video-engine/embedded-engine-worker.d.ts.map +0 -1
- package/dist/web-video-engine/engine-core.d.ts.map +0 -1
- package/dist/web-video-engine/engine.d.ts.map +0 -1
- package/dist/web-video-engine/engine.js.map +0 -1
- package/dist/web-video-engine/engine.worker.d.ts.map +0 -1
- package/dist/web-video-engine/engine.worker.js.map +0 -1
- package/dist/web-video-engine/frame-cache.d.ts.map +0 -1
- package/dist/web-video-engine/frame-extractor.d.ts.map +0 -1
- package/dist/web-video-engine/frame-timeline-GINI8Gum.js.map +0 -1
- package/dist/web-video-engine/frame-timeline.d.ts.map +0 -1
- package/dist/web-video-engine/frame-walker.d.ts.map +0 -1
- package/dist/web-video-engine/key-packet.d.ts.map +0 -1
- package/dist/web-video-engine/keyframe-index.d.ts.map +0 -1
- package/dist/web-video-engine/mirror-store.d.ts.map +0 -1
- package/dist/web-video-engine/renderer.d.ts.map +0 -1
- package/dist/web-video-engine/rotation.d.ts.map +0 -1
- package/dist/web-video-engine/scrub-controller.d.ts.map +0 -1
- package/dist/web-video-engine/scrub-cursor.d.ts.map +0 -1
- package/dist/web-video-engine/scrub-trajectory.d.ts.map +0 -1
- package/dist/web-video-engine/source-residency.d.ts.map +0 -1
- package/dist/web-video-engine/trace-recorder.d.ts.map +0 -1
- package/dist/web-video-engine/types.d.ts.map +0 -1
- package/dist/web-video-engine/video-engine.d.ts.map +0 -1
- package/dist/web-video-engine/webgpu-renderer.d.ts.map +0 -1
- package/dist/web-video-engine/worker-bridge.d.ts.map +0 -1
- package/dist/web-video-engine/worker-dispatch.d.ts.map +0 -1
- 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,
|
|
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
|
|
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
|
|
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$
|
|
2374
|
-
const TIMESTAMP_EPSILON_SECONDS
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
3902
|
-
|
|
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
|
|
3102
|
+
const readinessWait = new AbortController();
|
|
3103
|
+
const wait = options.holdForReadiness(channel.getPlayhead().mediaTimeS, readinessWait.signal);
|
|
3916
3104
|
if (wait) {
|
|
3917
|
-
|
|
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 =
|
|
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
|
-
|
|
3175
|
+
beginPlaybackIntent();
|
|
3961
3176
|
// A pause ends the producer's mechanical hold, so it lands ahead of the
|
|
3962
|
-
//
|
|
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
|
-
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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
|
-
|
|
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
|
|
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
|
|
4200
|
-
duration: runtimeState.duration(),
|
|
4201
|
-
firstTimestamp,
|
|
4202
|
-
gatePlayback: true,
|
|
4203
|
-
});
|
|
3904
|
+
await waitForDetectionCoverage(mediaTime);
|
|
4204
3905
|
}
|
|
4205
3906
|
if (shouldGateRenderPreparationPlayback) {
|
|
4206
|
-
|
|
3907
|
+
armRenderPreparationGate();
|
|
3908
|
+
await waitForRenderPreparationWithin(mediaTime, signal);
|
|
4207
3909
|
}
|
|
4208
3910
|
};
|
|
4209
|
-
/**
|
|
4210
|
-
*
|
|
4211
|
-
*
|
|
4212
|
-
*
|
|
4213
|
-
*
|
|
4214
|
-
*/
|
|
4215
|
-
const
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
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
|
|
4225
|
-
|
|
4226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
4652
|
-
?
|
|
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) =>
|
|
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), {
|
|
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
|
|
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,
|
|
5092
|
-
if (instruction.stroke) {
|
|
5093
|
-
compositeMaskStroke(rgba, canvasWidth,
|
|
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
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
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 =
|
|
5131
|
-
for (let x =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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.
|
|
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
|
|
6484
|
-
return Math.max(waitOptions.
|
|
6435
|
+
function getResumeAtSeconds(waitOptions) {
|
|
6436
|
+
return Math.max(waitOptions.resumeAtSeconds, 0);
|
|
6485
6437
|
}
|
|
6486
|
-
function
|
|
6487
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
9652
|
-
|
|
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
|
-
|
|
9625
|
+
writeIdMaskPaletteEntry(fillPalette, maskId, instruction.color, instruction.alpha);
|
|
9656
9626
|
if (instruction.stroke && instruction.stroke.width > 0) {
|
|
9657
|
-
|
|
9658
|
-
strokeWidths[maskId] =
|
|
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 =
|
|
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
|
-
|
|
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(${
|
|
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, ${
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10388
|
-
return preparedRenderWindow.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
14311
|
-
*
|
|
14312
|
-
*
|
|
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(
|
|
14233
|
+
displayFrameHandle = requestDisplayFrame(flushDeferredPresentedFrame);
|
|
14321
14234
|
}
|
|
14322
|
-
function
|
|
14235
|
+
function flushDeferredPresentedFrame() {
|
|
14323
14236
|
displayFrameHandle = null;
|
|
14324
|
-
|
|
14237
|
+
const deferred = deferredPresentedFrame;
|
|
14238
|
+
deferredPresentedFrame = null;
|
|
14239
|
+
if (!deferred) {
|
|
14325
14240
|
return;
|
|
14326
14241
|
}
|
|
14327
|
-
|
|
14328
|
-
|
|
14329
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
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
|
-
|
|
15169
|
+
gateHold === null) {
|
|
15231
15170
|
continue;
|
|
15232
15171
|
}
|
|
15233
|
-
const holdingPlayback = (activeFrameIsPending ||
|
|
15234
|
-
|
|
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:
|
|
15241
|
-
?
|
|
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
|
-
:
|
|
15244
|
-
|
|
15245
|
-
|
|
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:
|
|
15249
|
-
?
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
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:
|
|
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 {
|
|
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
|