supervision 0.2.0-next.1 → 0.2.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +557 -134
  4. package/dist/index.js.map +1 -1
  5. package/dist/mask-preparation.worker.js.map +1 -1
  6. package/dist/media/media-frame-clock.d.ts +4 -0
  7. package/dist/media/media-frame-clock.d.ts.map +1 -0
  8. package/dist/media/media-source.d.ts +8 -0
  9. package/dist/media/media-source.d.ts.map +1 -1
  10. package/dist/media/mediabunny-media-source.d.ts.map +1 -1
  11. package/dist/media/video-engine-media-source.d.ts.map +1 -1
  12. package/dist/media/video-engine-media-source.js +1 -1
  13. package/dist/render-preparation/prepared-window-timeline.d.ts +1 -0
  14. package/dist/render-preparation/prepared-window-timeline.d.ts.map +1 -1
  15. package/dist/renderers/media-frame-navigation.d.ts +17 -0
  16. package/dist/renderers/media-frame-navigation.d.ts.map +1 -0
  17. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  18. package/dist/renderers/media-renderer-scene.d.ts +1 -0
  19. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  20. package/dist/renderers/media-renderer-transport.d.ts +4 -1
  21. package/dist/renderers/media-renderer-transport.d.ts.map +1 -1
  22. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  23. package/dist/renderers/prepared-annotation-window.d.ts.map +1 -1
  24. package/dist/renderers/presented-frame-channel.d.ts +8 -6
  25. package/dist/renderers/presented-frame-channel.d.ts.map +1 -1
  26. package/dist/sessions/media-session-defaults.d.ts.map +1 -1
  27. package/dist/sessions/media-session-detections.d.ts.map +1 -1
  28. package/dist/sessions/media-session.d.ts.map +1 -1
  29. package/dist/tracking.worker.js.map +1 -1
  30. package/dist/types/media-frame-clock.d.ts +16 -0
  31. package/dist/types/media-frame-clock.d.ts.map +1 -0
  32. package/dist/types/media-frame-navigation.d.ts +28 -0
  33. package/dist/types/media-frame-navigation.d.ts.map +1 -0
  34. package/dist/types/media-renderer-display.d.ts +12 -0
  35. package/dist/types/media-renderer-display.d.ts.map +1 -0
  36. package/dist/types/media-renderer.d.ts +13 -0
  37. package/dist/types/media-renderer.d.ts.map +1 -1
  38. package/dist/types/media-session.d.ts +15 -5
  39. package/dist/types/media-session.d.ts.map +1 -1
  40. package/dist/types/render-preparation.d.ts +8 -5
  41. package/dist/types/render-preparation.d.ts.map +1 -1
  42. package/dist/{video-engine-media-source-CUXnOJOV.js → video-engine-media-source-DR-x1UfK.js} +35 -2
  43. package/dist/video-engine-media-source-DR-x1UfK.js.map +1 -0
  44. package/dist/web-video-engine/analysis.js +16 -0
  45. package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts +7 -2
  46. package/dist/web-video-engine/decode-scheduler.d.ts +9 -3
  47. package/dist/web-video-engine/decode-session.d.ts +5 -2
  48. package/dist/web-video-engine/decode-source.d.ts +2 -0
  49. package/dist/web-video-engine/engine-core.d.ts +10 -0
  50. package/dist/web-video-engine/engine.js +131 -15
  51. package/dist/web-video-engine/engine.worker.js +513 -36
  52. package/dist/web-video-engine/frame-cache.d.ts +2 -0
  53. package/dist/web-video-engine/pausable-deadline.d.ts +11 -0
  54. package/dist/web-video-engine/scrub-controller.d.ts +8 -4
  55. package/dist/web-video-engine/scrub-cursor.d.ts +6 -5
  56. package/dist/web-video-engine/video-engine.d.ts +16 -1
  57. package/dist/web-video-engine/worker-protocol.d.ts +9 -1
  58. package/node_modules/supervision-js-core/dist/detections/buffered-detection-timeline.d.ts.map +1 -1
  59. package/node_modules/supervision-js-core/dist/index.js +77 -41
  60. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  61. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +8 -3
  62. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
  63. package/package.json +1 -1
  64. package/dist/video-engine-media-source-CUXnOJOV.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
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
+ import { M as MediaSourceError, t as toMediaSourceError, r as resolveDisplayPixelRatio } from './video-engine-media-source-DR-x1UfK.js';
4
+ export { c as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-DR-x1UfK.js';
5
5
 
6
6
  const DEFAULT_DATABASE_NAME = "supervision-js-detection-frames";
7
7
  const DEFAULT_CHUNK_DURATION_SECONDS = 1;
@@ -1114,10 +1114,11 @@ async function openMediabunnyMediaSource(sourceInput) {
1114
1114
  })
1115
1115
  .catch(() => null)
1116
1116
  : Promise.resolve(null);
1117
- const [displayWidth, displayHeight, firstTimestamp, packetStats] = await Promise.all([
1117
+ const [displayWidth, displayHeight, firstTimestamp, timeResolution, packetStats,] = await Promise.all([
1118
1118
  primaryVideoTrack.getDisplayWidth(),
1119
1119
  primaryVideoTrack.getDisplayHeight(),
1120
1120
  primaryVideoTrack.getFirstTimestamp(),
1121
+ primaryVideoTrack.getTimeResolution(),
1121
1122
  packetStatsPromise,
1122
1123
  ]);
1123
1124
  const duration = isUrlSourceInput(sourceInput)
@@ -1145,6 +1146,7 @@ async function openMediabunnyMediaSource(sourceInput) {
1145
1146
  mimeType,
1146
1147
  primaryVideoHeight: displayHeight,
1147
1148
  primaryVideoWidth: displayWidth,
1149
+ timeResolution,
1148
1150
  trackCount: tracks.length,
1149
1151
  videoTrackCount: videoTracks.length,
1150
1152
  },
@@ -2341,7 +2343,7 @@ function createMediaPlaybackController(options) {
2341
2343
  ((now - playbackOriginNow) / 1000) * playbackRate;
2342
2344
  const playableEnd = options.duration === null
2343
2345
  ? null
2344
- : options.firstTimestamp + Math.max(options.duration, 0);
2346
+ : Math.max(options.firstTimestamp, options.duration);
2345
2347
  let shouldPresentLoopStartSample = false;
2346
2348
  if (playableEnd !== null && requestedMediaTime >= playableEnd) {
2347
2349
  if (!options.loop) {
@@ -2959,6 +2961,178 @@ function createMediaRenderErrorSourcePatch(error) {
2959
2961
  };
2960
2962
  }
2961
2963
 
2964
+ function createMediaFrameNavigation(options) {
2965
+ let activeOperation = null;
2966
+ let displayed = null;
2967
+ let destroyed = false;
2968
+ let destroyError;
2969
+ const controller = {
2970
+ api: {
2971
+ moveToFrame(index) {
2972
+ return startMove(() => landingAt(index));
2973
+ },
2974
+ moveToTime(mediaTime) {
2975
+ return startMove(() => landingAt(options.clock.indexAtOrBefore(mediaTime)));
2976
+ },
2977
+ scrubToFrame(index) {
2978
+ const target = landingAt(index);
2979
+ return startScrub(target);
2980
+ },
2981
+ scrubToTime(mediaTime) {
2982
+ const target = landingAt(options.clock.indexAtOrBefore(mediaTime));
2983
+ return startScrub(target);
2984
+ },
2985
+ },
2986
+ presented(frame) {
2987
+ displayed = frame;
2988
+ const operation = activeOperation;
2989
+ if (!operation ||
2990
+ frame.index !== operation.target.index ||
2991
+ frame.mediaTime !== operation.target.mediaTime) {
2992
+ return;
2993
+ }
2994
+ if (operation.kind === "scrub") {
2995
+ activeOperation = null;
2996
+ operation.resolve({ frame: operation.target, status: "landed" });
2997
+ return;
2998
+ }
2999
+ operation.presented = true;
3000
+ },
3001
+ cancel() {
3002
+ supersedeActive(createAbortError("Media frame navigation was superseded."));
3003
+ },
3004
+ destroy(error) {
3005
+ if (destroyed) {
3006
+ return;
3007
+ }
3008
+ destroyed = true;
3009
+ destroyError =
3010
+ error ?? createAbortError("Media frame navigation was destroyed.");
3011
+ supersedeActive(destroyError);
3012
+ },
3013
+ };
3014
+ return controller;
3015
+ function landingAt(index) {
3016
+ return {
3017
+ duration: options.clock.durationAt(index),
3018
+ index,
3019
+ mediaTime: options.clock.timeAt(index),
3020
+ };
3021
+ }
3022
+ function startMove(getTarget) {
3023
+ if (destroyed) {
3024
+ return own(Promise.reject(destroyError));
3025
+ }
3026
+ let target;
3027
+ try {
3028
+ target = getTarget();
3029
+ }
3030
+ catch (error) {
3031
+ return own(Promise.reject(error));
3032
+ }
3033
+ supersedeActive(createAbortError("Media frame navigation was superseded."));
3034
+ let resolve;
3035
+ let reject;
3036
+ const promise = new Promise((resolvePromise, rejectPromise) => {
3037
+ resolve = resolvePromise;
3038
+ reject = rejectPromise;
3039
+ });
3040
+ const operation = {
3041
+ kind: "move",
3042
+ presented: false,
3043
+ reject,
3044
+ resolve,
3045
+ target,
3046
+ };
3047
+ activeOperation = operation;
3048
+ let command;
3049
+ try {
3050
+ command = options.seek(target.mediaTime);
3051
+ }
3052
+ catch (error) {
3053
+ failMove(operation, error);
3054
+ return own(promise);
3055
+ }
3056
+ void Promise.resolve(command).then(() => {
3057
+ if (activeOperation !== operation) {
3058
+ return;
3059
+ }
3060
+ if (!operation.presented && !isDisplayed(operation.target)) {
3061
+ failMove(operation, new Error("Media seek completed without presenting the requested exact frame."));
3062
+ return;
3063
+ }
3064
+ activeOperation = null;
3065
+ operation.resolve(operation.target);
3066
+ }, (error) => {
3067
+ failMove(operation, error);
3068
+ });
3069
+ return own(promise);
3070
+ }
3071
+ function startScrub(target) {
3072
+ if (destroyed) {
3073
+ throw destroyError;
3074
+ }
3075
+ supersedeActive(createAbortError("Media frame navigation was superseded."));
3076
+ let resolve;
3077
+ const settled = new Promise((resolvePromise) => {
3078
+ resolve = resolvePromise;
3079
+ });
3080
+ const operation = {
3081
+ kind: "scrub",
3082
+ resolve,
3083
+ target,
3084
+ };
3085
+ activeOperation = operation;
3086
+ own(settled);
3087
+ try {
3088
+ options.scrub(target.mediaTime);
3089
+ if (activeOperation === operation && isDisplayed(target)) {
3090
+ activeOperation = null;
3091
+ operation.resolve({ frame: target, status: "landed" });
3092
+ }
3093
+ }
3094
+ catch (error) {
3095
+ if (activeOperation === operation) {
3096
+ activeOperation = null;
3097
+ operation.resolve({ status: "superseded" });
3098
+ }
3099
+ throw error;
3100
+ }
3101
+ return { settled, target };
3102
+ }
3103
+ function isDisplayed(target) {
3104
+ return (displayed?.index === target.index &&
3105
+ displayed.mediaTime === target.mediaTime);
3106
+ }
3107
+ function failMove(operation, error) {
3108
+ if (activeOperation !== operation) {
3109
+ return;
3110
+ }
3111
+ activeOperation = null;
3112
+ operation.reject(error);
3113
+ }
3114
+ function supersedeActive(error) {
3115
+ const operation = activeOperation;
3116
+ if (!operation) {
3117
+ return;
3118
+ }
3119
+ activeOperation = null;
3120
+ if (operation.kind === "move") {
3121
+ operation.reject(error);
3122
+ }
3123
+ else {
3124
+ operation.resolve({ status: "superseded" });
3125
+ }
3126
+ }
3127
+ }
3128
+ function own(promise) {
3129
+ void promise.catch(() => undefined);
3130
+ return promise;
3131
+ }
3132
+ function createAbortError(message) {
3133
+ return new DOMException(message, "AbortError");
3134
+ }
3135
+
2962
3136
  const MILLISECONDS_PER_SECOND$2 = 1000;
2963
3137
  /**
2964
3138
  * The renderer's playback surface over a producer that owns the playhead.
@@ -2979,12 +3153,14 @@ function createMediaRendererTransport(options) {
2979
3153
  // play the hold is waiting for, so a pause taken during it is not undone by
2980
3154
  // the readiness that arrives afterwards.
2981
3155
  let playbackIntent = 0;
3156
+ let stepIntent = null;
2982
3157
  /** Intent a readiness wait belongs to, or null while none is running. */
2983
3158
  let readinessHoldIntent = null;
2984
3159
  /** Whether a readiness hold still owes the producer its release. */
2985
3160
  let readinessFreezeHeld = false;
2986
3161
  let activeReadinessWait;
2987
3162
  let presentationWait = 0;
3163
+ let displayResize = 0;
2988
3164
  let presentationHold = null;
2989
3165
  const isHoldingForReadiness = () => readinessHoldIntent === playbackIntent ||
2990
3166
  presentationHold?.intent === playbackIntent;
@@ -2996,8 +3172,15 @@ function createMediaRendererTransport(options) {
2996
3172
  const beginPlaybackIntent = () => {
2997
3173
  activeReadinessWait?.abort();
2998
3174
  activeReadinessWait = undefined;
3175
+ stepIntent = null;
2999
3176
  return ++playbackIntent;
3000
3177
  };
3178
+ const beginStepIntent = () => {
3179
+ if (stepIntent !== null)
3180
+ return stepIntent;
3181
+ stepIntent = beginPlaybackIntent();
3182
+ return stepIntent;
3183
+ };
3001
3184
  const publishSeekSignals = () => {
3002
3185
  options.onScrubbing(gestureInFlight);
3003
3186
  options.onSeeking(isSettling(channel.getStatus(), channel.getSeeking()));
@@ -3143,6 +3326,10 @@ function createMediaRendererTransport(options) {
3143
3326
  await options.waitForReadiness(channel.getPlayhead().mediaTimeS, readinessWait.signal);
3144
3327
  readinessLanded = true;
3145
3328
  }
3329
+ catch (error) {
3330
+ if (intent === playbackIntent)
3331
+ throw error;
3332
+ }
3146
3333
  finally {
3147
3334
  if (activeReadinessWait === readinessWait) {
3148
3335
  activeReadinessWait = undefined;
@@ -3176,6 +3363,11 @@ function createMediaRendererTransport(options) {
3176
3363
  // A pause ends the producer's mechanical hold, so it lands ahead of the
3177
3364
  // releases a readiness hold or an open gesture still owe.
3178
3365
  channel.pause();
3366
+ // A gate may already have mechanically paused the producer, so its
3367
+ // no-op pause emits no state signal to retire Buffering.
3368
+ if (channel.getStatus() === "PAUSED") {
3369
+ publishPlaybackState();
3370
+ }
3179
3371
  void releaseReadinessFreeze();
3180
3372
  void releaseGesture();
3181
3373
  },
@@ -3211,7 +3403,7 @@ function createMediaRendererTransport(options) {
3211
3403
  options.invalidatePresentedFrame?.();
3212
3404
  const navigationPresentation = ++presentationWait;
3213
3405
  presentationHold = null;
3214
- beginPlaybackIntent();
3406
+ const intent = beginPlaybackIntent();
3215
3407
  // Releasing first is what keeps a drag from freezing the picture on
3216
3408
  // release: the producer resumes on its own terms, and the landing decode
3217
3409
  // for a cold region no longer sits between the pointer coming up and
@@ -3229,13 +3421,48 @@ function createMediaRendererTransport(options) {
3229
3421
  landingRelease = null;
3230
3422
  }
3231
3423
  }
3424
+ if (intent !== playbackIntent)
3425
+ return;
3232
3426
  const presentation = options.beginPresentedFrameNavigation?.();
3233
3427
  try {
3428
+ const before = channel.getPlayhead().frame;
3234
3429
  await channel.commit(mediaTime * MILLISECONDS_PER_SECOND$2);
3235
- await presentation?.waitFor(channel.getPlayhead().frame);
3430
+ if (intent !== playbackIntent) {
3431
+ presentation?.cancel();
3432
+ return;
3433
+ }
3434
+ const after = channel.getPlayhead().frame;
3435
+ await presentation?.waitFor(after, before.index === after.index && before.ticks === after.ticks);
3436
+ }
3437
+ catch (error) {
3438
+ presentation?.cancel();
3439
+ throw error;
3440
+ }
3441
+ },
3442
+ async resizeOutput(resize) {
3443
+ const intent = playbackIntent;
3444
+ const request = ++displayResize;
3445
+ const assertCurrent = () => {
3446
+ if (intent !== playbackIntent || request !== displayResize)
3447
+ throw new DOMException("Display resize was superseded.", "AbortError");
3448
+ };
3449
+ // Geometry does not change the viewer's play/pause or annotation gate.
3450
+ // A replacement enters through the same guard as every other frame.
3451
+ const presentation = options.beginPresentedFrameNavigation?.(true);
3452
+ try {
3453
+ const changed = await resize();
3454
+ assertCurrent();
3455
+ if (changed) {
3456
+ await presentation?.waitFor(channel.getPlayhead().frame);
3457
+ assertCurrent();
3458
+ }
3459
+ else {
3460
+ presentation?.cancel();
3461
+ }
3236
3462
  }
3237
3463
  catch (error) {
3238
3464
  presentation?.cancel();
3465
+ assertCurrent();
3239
3466
  throw error;
3240
3467
  }
3241
3468
  },
@@ -3243,15 +3470,23 @@ function createMediaRendererTransport(options) {
3243
3470
  options.invalidatePresentedFrame?.();
3244
3471
  const navigationPresentation = ++presentationWait;
3245
3472
  presentationHold = null;
3246
- beginPlaybackIntent();
3473
+ const intent = beginStepIntent();
3247
3474
  await releaseGesture();
3475
+ if (intent !== playbackIntent)
3476
+ return;
3248
3477
  if (navigationPresentation === presentationWait) {
3249
3478
  await releaseReadinessFreeze();
3250
3479
  }
3480
+ if (intent !== playbackIntent)
3481
+ return;
3251
3482
  const presentation = options.beginPresentedFrameNavigation?.();
3252
3483
  const before = channel.getPlayhead().frame;
3253
3484
  try {
3254
3485
  await channel.step(direction);
3486
+ if (intent !== playbackIntent) {
3487
+ presentation?.cancel();
3488
+ return;
3489
+ }
3255
3490
  const after = channel.getPlayhead().frame;
3256
3491
  if (before.index === after.index && before.ticks === after.ticks) {
3257
3492
  // A boundary step intentionally emits no replacement frame. The
@@ -3277,15 +3512,17 @@ function createMediaRendererTransport(options) {
3277
3512
  const guarded = options.waitForPresentationReadiness(mediaTime, signal);
3278
3513
  if (!guarded) {
3279
3514
  if (presentationHold) {
3280
- presentationWait += 1;
3281
- presentationHold = null;
3282
- void releaseReadinessFreeze().then(publishPlaybackState);
3515
+ presentationHold = {
3516
+ intent: playbackIntent,
3517
+ ready: true,
3518
+ wait: ++presentationWait,
3519
+ };
3283
3520
  }
3284
3521
  return null;
3285
3522
  }
3286
3523
  const wait = ++presentationWait;
3287
3524
  const intent = playbackIntent;
3288
- presentationHold = { intent, wait };
3525
+ presentationHold = { intent, ready: false, wait };
3289
3526
  readinessFreezeHeld = true;
3290
3527
  channel.beginInteractiveSeek();
3291
3528
  publishPlaybackState();
@@ -3298,18 +3535,22 @@ function createMediaRendererTransport(options) {
3298
3535
  // provider's diagnostics own the error while the frame stays visible.
3299
3536
  }
3300
3537
  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
3538
  if (wait === presentationWait && !signal.aborted) {
3307
- await releaseReadinessFreeze();
3308
- publishPlaybackState();
3539
+ presentationHold = { intent, ready: true, wait };
3309
3540
  }
3310
3541
  }
3311
3542
  })();
3312
3543
  },
3544
+ didPresentFrame() {
3545
+ const hold = presentationHold;
3546
+ if (!hold?.ready ||
3547
+ hold.intent !== playbackIntent ||
3548
+ hold.wait !== presentationWait) {
3549
+ return;
3550
+ }
3551
+ presentationHold = null;
3552
+ return releaseReadinessFreeze().then(publishPlaybackState);
3553
+ },
3313
3554
  destroy() {
3314
3555
  presentationWait += 1;
3315
3556
  presentationHold = null;
@@ -3363,13 +3604,14 @@ function isSettling(status, seeking) {
3363
3604
  * VideoFrame, so stale readiness can never authorize presentation after a new
3364
3605
  * seek. The downstream handler owns a frame only after the guard succeeds.
3365
3606
  */
3366
- function createProtectedPresentedFrameSource(upstream, onPresentationError = () => undefined) {
3607
+ function createProtectedPresentedFrameSource(upstream, onPresentationError = () => undefined, onAccepted, onPresented) {
3367
3608
  let downstream = null;
3368
3609
  let protect = null;
3369
3610
  let pending = null;
3370
3611
  let active = null;
3371
- let navigation = null;
3612
+ const navigations = new Set();
3372
3613
  let generation = 0;
3614
+ let lastAcceptedKey = null;
3373
3615
  let destroyed = false;
3374
3616
  let firstPresented = false;
3375
3617
  let resolveFirstPresentation;
@@ -3392,27 +3634,27 @@ function createProtectedPresentedFrameSource(upstream, onPresentationError = ()
3392
3634
  if (ticket.settled)
3393
3635
  return;
3394
3636
  ticket.settled = true;
3395
- if (navigation === ticket)
3396
- navigation = null;
3637
+ navigations.delete(ticket);
3397
3638
  if (error === undefined)
3398
3639
  ticket.resolve();
3399
3640
  else
3400
3641
  ticket.reject(error);
3401
3642
  };
3402
- const acceptNavigationFrame = (frameId) => {
3403
- const ticket = navigation;
3404
- if (!ticket || ticket.settled)
3405
- return;
3643
+ const acceptNavigationFrame = (frameId, frameGeneration) => {
3406
3644
  const accepted = frameKey(frameId);
3407
- ticket.accepted.add(accepted);
3408
- if (ticket.target === accepted)
3409
- settleNavigation(ticket);
3645
+ for (const ticket of navigations) {
3646
+ if (frameGeneration <= ticket.afterGeneration)
3647
+ continue;
3648
+ ticket.accepted.add(accepted);
3649
+ if (ticket.target === accepted)
3650
+ settleNavigation(ticket);
3651
+ }
3410
3652
  };
3411
3653
  const failPresentation = (error) => {
3412
3654
  if (!firstPresented)
3413
3655
  rejectFirstPresentation(error);
3414
- if (navigation)
3415
- settleNavigation(navigation, error);
3656
+ for (const ticket of navigations)
3657
+ settleNavigation(ticket, error);
3416
3658
  if (firstPresented)
3417
3659
  onPresentationError(error);
3418
3660
  };
@@ -3450,29 +3692,64 @@ function createProtectedPresentedFrameSource(upstream, onPresentationError = ()
3450
3692
  run.handedOff = true;
3451
3693
  try {
3452
3694
  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;
3695
+ let acknowledged = false;
3696
+ downstream({
3697
+ ...frame,
3698
+ acknowledgePresentation: () => {
3699
+ if (acknowledged ||
3700
+ destroyed ||
3701
+ controller.signal.aborted ||
3702
+ run.generation !== generation) {
3703
+ return;
3704
+ }
3705
+ acknowledged = true;
3706
+ try {
3707
+ acknowledge?.();
3708
+ }
3709
+ catch (error) {
3710
+ failPresentation(error);
3711
+ return;
3712
+ }
3713
+ if (destroyed ||
3714
+ controller.signal.aborted ||
3715
+ run.generation !== generation) {
3716
+ return;
3717
+ }
3718
+ try {
3719
+ const release = onPresented?.();
3720
+ if (release) {
3721
+ void release.catch((error) => {
3722
+ if (!destroyed &&
3723
+ !controller.signal.aborted &&
3724
+ run.generation === generation) {
3725
+ failPresentation(error);
3726
+ }
3727
+ });
3461
3728
  }
3462
- acknowledge();
3463
- },
3464
- }
3465
- : frame);
3729
+ }
3730
+ catch (error) {
3731
+ failPresentation(error);
3732
+ return;
3733
+ }
3734
+ if (destroyed ||
3735
+ controller.signal.aborted ||
3736
+ run.generation !== generation) {
3737
+ return;
3738
+ }
3739
+ lastAcceptedKey = frameKey(frame.frameId);
3740
+ onAccepted?.(frame.frameId, frame.mediaTimeS);
3741
+ acceptNavigationFrame(frame.frameId, run.generation);
3742
+ if (!firstPresented) {
3743
+ firstPresented = true;
3744
+ resolveFirstPresentation();
3745
+ }
3746
+ },
3747
+ });
3466
3748
  }
3467
3749
  catch (error) {
3468
3750
  failPresentation(error);
3469
3751
  return;
3470
3752
  }
3471
- acceptNavigationFrame(frame.frameId);
3472
- if (!firstPresented) {
3473
- firstPresented = true;
3474
- resolveFirstPresentation();
3475
- }
3476
3753
  };
3477
3754
  let guarded;
3478
3755
  try {
@@ -3532,16 +3809,18 @@ function createProtectedPresentedFrameSource(upstream, onPresentationError = ()
3532
3809
  cancelActive();
3533
3810
  pending?.frame.close();
3534
3811
  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);
3812
+ for (const ticket of navigations)
3813
+ settleNavigation(ticket);
3814
+ },
3815
+ beginNavigation(preserveCurrent = false) {
3816
+ if (!preserveCurrent) {
3817
+ generation += 1;
3818
+ cancelActive();
3819
+ pending?.frame.close();
3820
+ pending = null;
3821
+ for (const ticket of navigations)
3822
+ settleNavigation(ticket);
3823
+ }
3545
3824
  let resolve;
3546
3825
  let reject;
3547
3826
  const promise = new Promise((onResolve, onReject) => {
@@ -3553,20 +3832,28 @@ function createProtectedPresentedFrameSource(upstream, onPresentationError = ()
3553
3832
  void promise.catch(() => undefined);
3554
3833
  const ticket = {
3555
3834
  accepted: new Set(),
3835
+ afterGeneration: generation,
3556
3836
  promise,
3557
3837
  reject,
3558
3838
  resolve,
3559
3839
  settled: false,
3560
3840
  target: null,
3561
3841
  };
3562
- navigation = ticket;
3842
+ navigations.add(ticket);
3563
3843
  return {
3564
- waitFor(frameId) {
3844
+ waitFor(frameId, allowCurrent = false) {
3565
3845
  if (ticket.settled)
3566
3846
  return ticket.promise;
3567
3847
  ticket.target = frameKey(frameId);
3568
3848
  if (ticket.accepted.has(ticket.target))
3569
3849
  settleNavigation(ticket);
3850
+ if (allowCurrent &&
3851
+ lastAcceptedKey === ticket.target &&
3852
+ generation === ticket.afterGeneration &&
3853
+ pending === null &&
3854
+ active === null) {
3855
+ settleNavigation(ticket);
3856
+ }
3570
3857
  return ticket.promise;
3571
3858
  },
3572
3859
  cancel() {
@@ -3583,8 +3870,8 @@ function createProtectedPresentedFrameSource(upstream, onPresentationError = ()
3583
3870
  cancelActive();
3584
3871
  pending?.frame.close();
3585
3872
  pending = null;
3586
- if (navigation)
3587
- settleNavigation(navigation);
3873
+ for (const ticket of navigations)
3874
+ settleNavigation(ticket);
3588
3875
  upstream.onPresentedFrame((presented) => presented.frame.close());
3589
3876
  if (!firstPresented)
3590
3877
  resolveFirstPresentation();
@@ -3723,6 +4010,8 @@ async function createMediaRendererCore(options, providers) {
3723
4010
  options.onState?.(state);
3724
4011
  },
3725
4012
  });
4013
+ let frameClock = null;
4014
+ let frameNavigation;
3726
4015
  let activeSampleIterator;
3727
4016
  let mediaInput;
3728
4017
  let playbackController;
@@ -3731,6 +4020,7 @@ async function createMediaRendererCore(options, providers) {
3731
4020
  let pushPresentationReady = false;
3732
4021
  let sampleSink;
3733
4022
  let firstTimestamp = 0;
4023
+ let sampleTimeResolution = null;
3734
4024
  let navigationVersion = 0;
3735
4025
  let outstandingSourceReads = 0;
3736
4026
  let pendingPullScrubTime = null;
@@ -3789,6 +4079,7 @@ async function createMediaRendererCore(options, providers) {
3789
4079
  runtimeState.setLoading();
3790
4080
  break;
3791
4081
  case MediaRendererPlaybackState.Error:
4082
+ frameNavigation?.cancel();
3792
4083
  runtimeState.setRenderError(new Error("Media playback failed."));
3793
4084
  break;
3794
4085
  }
@@ -4104,8 +4395,53 @@ async function createMediaRendererCore(options, providers) {
4104
4395
  });
4105
4396
  });
4106
4397
  };
4398
+ const seekMediaTime = async (mediaTime) => {
4399
+ if (runtimeState.isDestroyed()) {
4400
+ throw new Error("Media renderer has been destroyed.");
4401
+ }
4402
+ if (runtimeState.isError()) {
4403
+ throw new Error(runtimeState.errorMessage() ?? "Media renderer is in error state.");
4404
+ }
4405
+ const targetTime = clampSeekTime({
4406
+ duration: runtimeState.duration(),
4407
+ firstTimestamp,
4408
+ mediaTime,
4409
+ });
4410
+ endSeekGesture();
4411
+ if (transport) {
4412
+ await transport.commit(targetTime);
4413
+ return;
4414
+ }
4415
+ await seekPullSample(targetTime);
4416
+ };
4417
+ const scrubMediaTime = (mediaTime) => {
4418
+ if (runtimeState.isDestroyed() || runtimeState.isError())
4419
+ return;
4420
+ const targetTime = clampSeekTime({
4421
+ duration: runtimeState.duration(),
4422
+ firstTimestamp,
4423
+ mediaTime,
4424
+ });
4425
+ isSeekGestureInFlight = true;
4426
+ publishPlaybackActivity();
4427
+ if (transport) {
4428
+ transport.scrub(targetTime);
4429
+ return;
4430
+ }
4431
+ pendingPullScrubTime = targetTime;
4432
+ if (pullScrubReadInFlight)
4433
+ navigationVersion += 1;
4434
+ schedulePullScrubPreview();
4435
+ };
4107
4436
  const renderer = {
4437
+ get frameClock() {
4438
+ return frameClock;
4439
+ },
4440
+ get frameNavigation() {
4441
+ return frameNavigation?.api ?? null;
4442
+ },
4108
4443
  async play() {
4444
+ frameNavigation?.cancel();
4109
4445
  if (runtimeState.isDestroyed()) {
4110
4446
  throw new Error("Media renderer has been destroyed.");
4111
4447
  }
@@ -4134,6 +4470,7 @@ async function createMediaRendererCore(options, providers) {
4134
4470
  playbackController.play();
4135
4471
  },
4136
4472
  pause() {
4473
+ frameNavigation?.cancel();
4137
4474
  if (runtimeState.isDestroyed()) {
4138
4475
  return;
4139
4476
  }
@@ -4149,6 +4486,7 @@ async function createMediaRendererCore(options, providers) {
4149
4486
  playbackController?.pause();
4150
4487
  },
4151
4488
  async togglePlayback() {
4489
+ frameNavigation?.cancel();
4152
4490
  if (runtimeState.isDestroyed()) {
4153
4491
  return;
4154
4492
  }
@@ -4164,49 +4502,19 @@ async function createMediaRendererCore(options, providers) {
4164
4502
  await renderer.play();
4165
4503
  },
4166
4504
  async seek(mediaTime) {
4167
- if (runtimeState.isDestroyed()) {
4168
- throw new Error("Media renderer has been destroyed.");
4169
- }
4170
- if (runtimeState.isError()) {
4171
- throw new Error(runtimeState.errorMessage() ?? "Media renderer is in error state.");
4172
- }
4173
- const targetTime = clampSeekTime({
4174
- duration: runtimeState.duration(),
4175
- firstTimestamp,
4176
- mediaTime,
4177
- });
4178
- endSeekGesture();
4179
- if (transport) {
4180
- await transport.commit(targetTime);
4181
- return;
4182
- }
4183
- await seekPullSample(targetTime);
4505
+ frameNavigation?.cancel();
4506
+ await seekMediaTime(mediaTime);
4184
4507
  },
4185
4508
  scrub(mediaTime) {
4186
- if (runtimeState.isDestroyed() || runtimeState.isError()) {
4187
- return;
4188
- }
4189
- const targetTime = clampSeekTime({
4190
- duration: runtimeState.duration(),
4191
- firstTimestamp,
4192
- mediaTime,
4193
- });
4194
- isSeekGestureInFlight = true;
4195
- publishPlaybackActivity();
4196
- if (transport) {
4197
- transport.scrub(targetTime);
4198
- return;
4199
- }
4200
- pendingPullScrubTime = targetTime;
4201
- if (pullScrubReadInFlight) {
4202
- navigationVersion += 1;
4203
- }
4204
- schedulePullScrubPreview();
4509
+ frameNavigation?.cancel();
4510
+ scrubMediaTime(mediaTime);
4205
4511
  },
4206
4512
  async stepForward() {
4513
+ frameNavigation?.cancel();
4207
4514
  await stepToAdjacentSample("forward");
4208
4515
  },
4209
4516
  async stepBackward() {
4517
+ frameNavigation?.cancel();
4210
4518
  await stepToAdjacentSample("backward");
4211
4519
  },
4212
4520
  setPlaybackRate(playbackRate) {
@@ -4246,7 +4554,7 @@ async function createMediaRendererCore(options, providers) {
4246
4554
  if (requestVersion !== navigationVersion || runtimeState.isDestroyed()) {
4247
4555
  return;
4248
4556
  }
4249
- const presentedSample = mediaScene.setPresentation(currentPresentation, mediaTime);
4557
+ const presentedSample = mediaScene.setPresentation(currentPresentation, runtimeState.presentedTime() ?? runtimeState.currentTime());
4250
4558
  if (presentedSample) {
4251
4559
  runtimeState.recordPresentationUpdate(presentedSample);
4252
4560
  }
@@ -4337,6 +4645,7 @@ async function createMediaRendererCore(options, providers) {
4337
4645
  return;
4338
4646
  }
4339
4647
  cancelPullScrubPreview();
4648
+ frameNavigation?.destroy();
4340
4649
  runtimeState.markDestroyed();
4341
4650
  protectedPresentedFrames?.destroy();
4342
4651
  transport?.destroy();
@@ -4355,6 +4664,7 @@ async function createMediaRendererCore(options, providers) {
4355
4664
  throw new Error("Provide either detectionFrames or detectionSource, not both.");
4356
4665
  }
4357
4666
  const mediaSource = await openRendererMediaSource(options, providers);
4667
+ frameClock = mediaSource.frameClock ?? null;
4358
4668
  mediaInput = mediaSource.input;
4359
4669
  sampleSink = mediaSource.sampleSink;
4360
4670
  if (runtimeState.isDestroyed()) {
@@ -4363,6 +4673,12 @@ async function createMediaRendererCore(options, providers) {
4363
4673
  }
4364
4674
  const { metadata } = mediaSource;
4365
4675
  firstTimestamp = metadata.firstTimestamp;
4676
+ sampleTimeResolution =
4677
+ metadata.timeResolution !== undefined &&
4678
+ Number.isFinite(metadata.timeResolution) &&
4679
+ metadata.timeResolution > 0
4680
+ ? metadata.timeResolution
4681
+ : null;
4366
4682
  // One central projection step for every detection input this renderer can
4367
4683
  // receive: static frames, a caller-owned source, or a composite source.
4368
4684
  // Media dimensions are known here, so a producer can declare its own
@@ -4388,11 +4704,12 @@ async function createMediaRendererCore(options, providers) {
4388
4704
  // the producer keeps running. Cut off future frames and state
4389
4705
  // signals before publishing the rendering failure.
4390
4706
  protectedPresentedFrames?.destroy();
4707
+ frameNavigation?.destroy(error);
4391
4708
  transport?.destroy();
4392
4709
  presentedFrameChannel.pause();
4393
4710
  if (!runtimeState.isDestroyed())
4394
4711
  runtimeState.setRenderError(error);
4395
- })
4712
+ }, (id, mediaTime) => frameNavigation?.presented({ index: id.index, mediaTime }), () => transport?.didPresentFrame())
4396
4713
  : undefined;
4397
4714
  const mediaDimensions = runtimeState.recordMediaMetadata(metadata);
4398
4715
  mediaScene = await providers.createScene({
@@ -4437,14 +4754,13 @@ async function createMediaRendererCore(options, providers) {
4437
4754
  });
4438
4755
  publishPlaybackActivity();
4439
4756
  runtimeState.setRendererBackend(mediaScene.rendererBackend);
4440
- detectionTimeline.setTimelineContext?.({
4441
- duration: metadata.duration,
4442
- loop: options.loop !== false,
4443
- });
4444
- mediaScene.setTimelineContext?.({
4757
+ const timelineContext = {
4445
4758
  duration: metadata.duration,
4759
+ firstTimestamp: metadata.firstTimestamp,
4446
4760
  loop: options.loop !== false,
4447
- });
4761
+ };
4762
+ detectionTimeline.setTimelineContext?.(timelineContext);
4763
+ mediaScene.setTimelineContext?.(timelineContext);
4448
4764
  mediaScene.initializeMedia(mediaDimensions);
4449
4765
  runtimeState.setSourceReady(metadata);
4450
4766
  if (presentedFrameChannel) {
@@ -4474,9 +4790,35 @@ async function createMediaRendererCore(options, providers) {
4474
4790
  ? holdForPresentationReadiness
4475
4791
  : undefined,
4476
4792
  invalidatePresentedFrame: () => protectedPresentedFrames?.invalidate(),
4477
- beginPresentedFrameNavigation: () => protectedPresentedFrames.beginNavigation(),
4793
+ beginPresentedFrameNavigation: (preserveCurrent) => protectedPresentedFrames.beginNavigation(preserveCurrent),
4478
4794
  });
4479
- protectedPresentedFrames?.activate((presented, signal) => transport.protectPresentation(presented.mediaTimeS, signal));
4795
+ const setDisplay = mediaSource.setDisplay;
4796
+ if (setDisplay) {
4797
+ renderer.setDisplay = async (display) => {
4798
+ if (runtimeState.isDestroyed())
4799
+ throw new Error("Media renderer has been destroyed.");
4800
+ await transport.resizeOutput(() => setDisplay(display));
4801
+ };
4802
+ }
4803
+ if (frameClock) {
4804
+ frameNavigation = createMediaFrameNavigation({
4805
+ clock: frameClock,
4806
+ seek: seekMediaTime,
4807
+ scrub(mediaTime) {
4808
+ if (runtimeState.isDestroyed()) {
4809
+ throw new Error("Media renderer has been destroyed.");
4810
+ }
4811
+ if (runtimeState.isError()) {
4812
+ throw new Error(runtimeState.errorMessage() ??
4813
+ "Media renderer is in error state.");
4814
+ }
4815
+ scrubMediaTime(mediaTime);
4816
+ },
4817
+ });
4818
+ }
4819
+ protectedPresentedFrames?.activate((presented, signal) => pushPresentationReady
4820
+ ? transport.protectPresentation(presented.mediaTimeS, signal)
4821
+ : null);
4480
4822
  if (initialPlaybackRate !== 1) {
4481
4823
  transport.setPlaybackRate(initialPlaybackRate);
4482
4824
  }
@@ -4498,7 +4840,11 @@ async function createMediaRendererCore(options, providers) {
4498
4840
  pushPresentationReady = true;
4499
4841
  runtimeState.setReady();
4500
4842
  if (options.autoPlay ?? true) {
4501
- await renderer.play();
4843
+ // Opening exposes the writer even when autoplay awaits future detections.
4844
+ void renderer.play().catch((error) => {
4845
+ if (!runtimeState.isDestroyed())
4846
+ runtimeState.setRenderError(error);
4847
+ });
4502
4848
  }
4503
4849
  return renderer;
4504
4850
  }
@@ -4580,21 +4926,38 @@ async function createMediaRendererCore(options, providers) {
4580
4926
  playbackController.pause();
4581
4927
  runtimeState.setPaused();
4582
4928
  const currentTime = runtimeState.currentTime();
4583
- const epsilon = 1e-6;
4584
4929
  let sample = null;
4585
4930
  try {
4586
4931
  if (direction === "backward") {
4587
- sample = await trackSourceRead(sampleSink.getSample(Math.max(firstTimestamp, currentTime - epsilon), {
4932
+ if (currentTime <= firstTimestamp) {
4933
+ return;
4934
+ }
4935
+ const stepSeconds = sampleTimeResolution === null ? 1e-6 : 1 / sampleTimeResolution;
4936
+ sample = await trackSourceRead(sampleSink.getSample(Math.max(firstTimestamp, currentTime - stepSeconds), {
4588
4937
  skipLiveWait: true,
4589
4938
  }));
4590
4939
  }
4591
4940
  else {
4592
- const iterator = sampleSink.samples(currentTime + epsilon, undefined, {
4941
+ const iterator = sampleSink.samples(currentTime, undefined, {
4593
4942
  skipLiveWait: true,
4594
4943
  });
4595
4944
  try {
4596
- const result = await trackSourceRead(iterator.next());
4597
- sample = result.done ? null : result.value;
4945
+ while (true) {
4946
+ const result = await trackSourceRead(iterator.next());
4947
+ if (result.done) {
4948
+ break;
4949
+ }
4950
+ if (requestVersion !== navigationVersion ||
4951
+ runtimeState.isDestroyed()) {
4952
+ result.value.close();
4953
+ return;
4954
+ }
4955
+ if (result.value.timestamp > currentTime) {
4956
+ sample = result.value;
4957
+ break;
4958
+ }
4959
+ result.value.close();
4960
+ }
4598
4961
  }
4599
4962
  finally {
4600
4963
  await iterator.return?.();
@@ -4662,7 +5025,7 @@ function clampSeekTime(options) {
4662
5025
  const startTime = options.firstTimestamp;
4663
5026
  const endTime = options.duration === null
4664
5027
  ? null
4665
- : options.firstTimestamp + Math.max(options.duration, 0);
5028
+ : Math.max(options.firstTimestamp, options.duration);
4666
5029
  return Math.min(Math.max(mediaTime, startTime), endTime ?? mediaTime);
4667
5030
  }
4668
5031
 
@@ -5559,9 +5922,10 @@ function createPreparedWindowTimeline() {
5559
5922
  },
5560
5923
  };
5561
5924
  function isLoopingTimeline() {
5925
+ const firstTimestamp = timelineContext.firstTimestamp ?? 0;
5562
5926
  return (timelineContext.loop &&
5563
5927
  timelineContext.duration !== null &&
5564
- timelineContext.duration > 0);
5928
+ timelineContext.duration > firstTimestamp);
5565
5929
  }
5566
5930
  /**
5567
5931
  * Travel forward from `mediaTime` to `frameTime`, wrapping at the media end,
@@ -5571,11 +5935,12 @@ function createPreparedWindowTimeline() {
5571
5935
  if (!isLoopingTimeline() || timelineContext.duration === null) {
5572
5936
  return Math.max(0, frameTime - mediaTime);
5573
5937
  }
5574
- const duration = timelineContext.duration;
5575
- const normalizedFrameTime = modulo(frameTime, duration);
5576
- const normalizedMediaTime = modulo(mediaTime, duration);
5938
+ const firstTimestamp = timelineContext.firstTimestamp ?? 0;
5939
+ const loopSpan = timelineContext.duration - firstTimestamp;
5940
+ const normalizedFrameTime = firstTimestamp + modulo(frameTime - firstTimestamp, loopSpan);
5941
+ const normalizedMediaTime = firstTimestamp + modulo(mediaTime - firstTimestamp, loopSpan);
5577
5942
  const rawDistance = normalizedFrameTime - normalizedMediaTime;
5578
- return rawDistance >= 0 ? rawDistance : rawDistance + duration;
5943
+ return rawDistance >= 0 ? rawDistance : rawDistance + loopSpan;
5579
5944
  }
5580
5945
  /**
5581
5946
  * The buffer's end time is the far edge of what the playhead reaches next, and
@@ -6642,17 +7007,23 @@ function createPreparedAnnotationWindow(options) {
6642
7007
  const spanFrameCount = resolvePreparedWindowFrameCount({
6643
7008
  renderPreparation: options.renderPreparation,
6644
7009
  });
7010
+ const frameRevisions = new WeakMap();
7011
+ let nextFrameRevision = 0;
6645
7012
  const getPreparedFrame = (mediaTime) => options.detectionTimeline.selectFrame(mediaTime) ?? null;
6646
7013
  const isFrameArtifactPrepared = (mediaTime) => options.getLayers().every((layer) => layer.isArtifactPrepared(mediaTime));
6647
7014
  return {
6648
7015
  getPreparedFrame,
6649
7016
  getReadinessToken(mediaTime) {
6650
7017
  const detectionFrame = options.detectionTimeline.selectFrame(mediaTime);
7018
+ if (detectionFrame && !frameRevisions.has(detectionFrame)) {
7019
+ frameRevisions.set(detectionFrame, ++nextFrameRevision);
7020
+ }
7021
+ const revision = detectionFrame ? frameRevisions.get(detectionFrame) : 0;
6651
7022
  const cooks = options
6652
7023
  .getLayers()
6653
7024
  .map((layer) => (layer.isArtifactPrepared(mediaTime) ? "1" : "0"))
6654
7025
  .join("");
6655
- return `${detectionFrame?.frameIndex ?? "time"}:${detectionFrame?.mediaTime ?? "none"}:${cooks}`;
7026
+ return `${detectionFrame?.frameIndex ?? "time"}:${detectionFrame?.mediaTime ?? "none"}:${revision}:${cooks}`;
6656
7027
  },
6657
7028
  getSnapshot() {
6658
7029
  const playheadMediaTime = options.getPlayheadMediaTime();
@@ -13143,13 +13514,14 @@ async function createPixiMediaScene(options) {
13143
13514
  });
13144
13515
  },
13145
13516
  };
13146
- // The focus fade and the overlay are advanced by a clock: the presented media
13147
- // time in a push scene, the ticker in a pull one. Drawing them from a walk
13148
- // that clock did not drive would move them out of turn.
13517
+ // A content redraw updates editing geometry at the same media time without
13518
+ // advancing the focus animation.
13149
13519
  const annotationDrawLayers = {
13150
13520
  ...presentLayers,
13151
13521
  advanceFocus: () => undefined,
13152
- drawAnnotationOverlay: () => undefined,
13522
+ drawAnnotationOverlay: frameChannel
13523
+ ? presentLayers.drawAnnotationOverlay
13524
+ : () => undefined,
13153
13525
  };
13154
13526
  // A viewport-style redraw covers the layers whose geometry is a function of
13155
13527
  // the viewport scale. Neither the mask raster nor the interaction hit map
@@ -13593,6 +13965,8 @@ async function createPixiMediaScene(options) {
13593
13965
  return pick;
13594
13966
  },
13595
13967
  destroy() {
13968
+ if (isDestroyed)
13969
+ return;
13596
13970
  isDestroyed = true;
13597
13971
  if (displayFrameHandle !== null) {
13598
13972
  cancelDisplayFrame(displayFrameHandle);
@@ -13634,6 +14008,7 @@ async function createPixiMediaScene(options) {
13634
14008
  texture: true,
13635
14009
  textureSource: true,
13636
14010
  });
14011
+ backdrop.remove();
13637
14012
  },
13638
14013
  };
13639
14014
  /**
@@ -14648,10 +15023,11 @@ const STREAM_DETECTION_BUFFER_DEFAULTS = {
14648
15023
  refreshIntervalSeconds: 0.25,
14649
15024
  };
14650
15025
  /**
14651
- * Playback waits for annotations unless a host says otherwise, so a viewer sees
14652
- * a frame and the marks that belong to it together rather than a picture that
14653
- * fills in afterwards. `playbackGate: false` on the session, or either gate's
14654
- * own `enabled`, turns it off.
15026
+ * Once a session is open, playback waits for annotations unless a host says
15027
+ * otherwise, so a viewer sees a frame and the marks that belong to it together.
15028
+ * Opening itself may show a bare initial frame while future annotations arrive.
15029
+ * `playbackGate: false` on the session, or either gate's own `enabled`, turns
15030
+ * it off.
14655
15031
  */
14656
15032
  const DETECTION_PLAYBACK_GATE_DEFAULTS = {
14657
15033
  enabled: true,
@@ -14844,10 +15220,16 @@ async function prepareSessionDetections(options) {
14844
15220
  sourcePresentations: [],
14845
15221
  };
14846
15222
  }
15223
+ const writableSource = isWritableSource(detections.source)
15224
+ ? detections.source
15225
+ : undefined;
14847
15226
  return {
14848
- appendableSources: new Map(),
15227
+ appendableSource: writableSource,
15228
+ appendableSources: new Map(writableSource ? [[writableSource.datasetId, writableSource]] : []),
14849
15229
  detectionFrames: detections.frames,
14850
- detectionSource: detections.source,
15230
+ detectionSource: detections.source
15231
+ ? borrowDetectionSource(detections.source)
15232
+ : undefined,
14851
15233
  sourcePresentations: [],
14852
15234
  };
14853
15235
  }
@@ -14866,6 +15248,7 @@ async function prepareMultiSourceSessionDetections(options) {
14866
15248
  throw new Error("detections.sources must include at least one source.");
14867
15249
  }
14868
15250
  const appendableSources = new Map();
15251
+ const ownedSources = [];
14869
15252
  const compositeEntries = [];
14870
15253
  validateMediaSessionDetectionSources(detections.sources);
14871
15254
  try {
@@ -14875,7 +15258,12 @@ async function prepareMultiSourceSessionDetections(options) {
14875
15258
  appendable: sourceOptions.appendable,
14876
15259
  mode: options.mode,
14877
15260
  })
14878
- : undefined;
15261
+ : isWritableSource(sourceOptions.source)
15262
+ ? sourceOptions.source
15263
+ : undefined;
15264
+ if (sourceOptions.appendable && appendableSource) {
15265
+ ownedSources.push(appendableSource);
15266
+ }
14879
15267
  if (appendableSource) {
14880
15268
  appendableSources.set(sourceOptions.id, appendableSource);
14881
15269
  }
@@ -14884,13 +15272,15 @@ async function prepareMultiSourceSessionDetections(options) {
14884
15272
  id: sourceOptions.id,
14885
15273
  order: sourceOptions.order,
14886
15274
  requiredForCoverage: sourceOptions.requiredForCoverage,
14887
- source: sourceOptions.source ?? appendableSource,
15275
+ source: sourceOptions.source
15276
+ ? borrowDetectionSource(sourceOptions.source)
15277
+ : appendableSource,
14888
15278
  sync: sourceOptions.sync,
14889
15279
  });
14890
15280
  }
14891
15281
  }
14892
15282
  catch (error) {
14893
- for (const source of appendableSources.values()) {
15283
+ for (const source of ownedSources) {
14894
15284
  source.destroy?.();
14895
15285
  }
14896
15286
  throw error;
@@ -14907,6 +15297,36 @@ async function prepareMultiSourceSessionDetections(options) {
14907
15297
  })),
14908
15298
  };
14909
15299
  }
15300
+ function isWritableSource(source) {
15301
+ const candidate = source;
15302
+ return (typeof candidate?.datasetId === "string" &&
15303
+ [
15304
+ "appendFrames",
15305
+ "replaceFrames",
15306
+ "clear",
15307
+ "getVersion",
15308
+ "getSummary",
15309
+ "waitForRange",
15310
+ "getAvailableRanges",
15311
+ ].every((method) => typeof candidate[method] === "function"));
15312
+ }
15313
+ function borrowDetectionSource(source) {
15314
+ return {
15315
+ loadFrames: source.loadFrames.bind(source),
15316
+ ...(source.getVersion
15317
+ ? { getVersion: source.getVersion.bind(source) }
15318
+ : {}),
15319
+ ...(source.getChangesSince
15320
+ ? { getChangesSince: source.getChangesSince.bind(source) }
15321
+ : {}),
15322
+ ...(source.getAvailableRanges
15323
+ ? { getAvailableRanges: source.getAvailableRanges.bind(source) }
15324
+ : {}),
15325
+ ...(source.waitForRange
15326
+ ? { waitForRange: source.waitForRange.bind(source) }
15327
+ : {}),
15328
+ };
15329
+ }
14910
15330
  function validateMediaSessionDetectionSources(sources) {
14911
15331
  const sourceIds = new Set();
14912
15332
  for (const source of sources) {
@@ -15529,6 +15949,9 @@ async function createMediaSession(options) {
15529
15949
  };
15530
15950
  };
15531
15951
  return {
15952
+ frameClock: renderer.frameClock ?? null,
15953
+ frameNavigation: renderer.frameNavigation ?? null,
15954
+ setDisplay: renderer.setDisplay?.bind(renderer),
15532
15955
  detectionSource: sessionDetections.detectionSource,
15533
15956
  media: sessionMedia.state,
15534
15957
  renderer,