supervision 0.2.0-next.1 → 0.2.0-next.2

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 (63) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +508 -127
  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 +6 -0
  9. package/dist/media/media-source.d.ts.map +1 -1
  10. package/dist/media/video-engine-media-source.d.ts.map +1 -1
  11. package/dist/media/video-engine-media-source.js +1 -1
  12. package/dist/render-preparation/prepared-window-timeline.d.ts +1 -0
  13. package/dist/render-preparation/prepared-window-timeline.d.ts.map +1 -1
  14. package/dist/renderers/media-frame-navigation.d.ts +17 -0
  15. package/dist/renderers/media-frame-navigation.d.ts.map +1 -0
  16. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  17. package/dist/renderers/media-renderer-scene.d.ts +1 -0
  18. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  19. package/dist/renderers/media-renderer-transport.d.ts +4 -1
  20. package/dist/renderers/media-renderer-transport.d.ts.map +1 -1
  21. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  22. package/dist/renderers/prepared-annotation-window.d.ts.map +1 -1
  23. package/dist/renderers/presented-frame-channel.d.ts +8 -6
  24. package/dist/renderers/presented-frame-channel.d.ts.map +1 -1
  25. package/dist/sessions/media-session-defaults.d.ts.map +1 -1
  26. package/dist/sessions/media-session-detections.d.ts.map +1 -1
  27. package/dist/sessions/media-session.d.ts.map +1 -1
  28. package/dist/tracking.worker.js.map +1 -1
  29. package/dist/types/media-frame-clock.d.ts +16 -0
  30. package/dist/types/media-frame-clock.d.ts.map +1 -0
  31. package/dist/types/media-frame-navigation.d.ts +28 -0
  32. package/dist/types/media-frame-navigation.d.ts.map +1 -0
  33. package/dist/types/media-renderer-display.d.ts +12 -0
  34. package/dist/types/media-renderer-display.d.ts.map +1 -0
  35. package/dist/types/media-renderer.d.ts +13 -0
  36. package/dist/types/media-renderer.d.ts.map +1 -1
  37. package/dist/types/media-session.d.ts +15 -5
  38. package/dist/types/media-session.d.ts.map +1 -1
  39. package/dist/types/render-preparation.d.ts +8 -5
  40. package/dist/types/render-preparation.d.ts.map +1 -1
  41. package/dist/{video-engine-media-source-CUXnOJOV.js → video-engine-media-source-DR-x1UfK.js} +35 -2
  42. package/dist/video-engine-media-source-DR-x1UfK.js.map +1 -0
  43. package/dist/web-video-engine/analysis.js +16 -0
  44. package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts +6 -1
  45. package/dist/web-video-engine/decode-scheduler.d.ts +8 -2
  46. package/dist/web-video-engine/decode-session.d.ts +5 -2
  47. package/dist/web-video-engine/decode-source.d.ts +2 -0
  48. package/dist/web-video-engine/engine-core.d.ts +9 -0
  49. package/dist/web-video-engine/engine.js +103 -11
  50. package/dist/web-video-engine/engine.worker.js +492 -31
  51. package/dist/web-video-engine/frame-cache.d.ts +2 -0
  52. package/dist/web-video-engine/pausable-deadline.d.ts +11 -0
  53. package/dist/web-video-engine/scrub-controller.d.ts +8 -4
  54. package/dist/web-video-engine/scrub-cursor.d.ts +3 -0
  55. package/dist/web-video-engine/video-engine.d.ts +12 -1
  56. package/dist/web-video-engine/worker-protocol.d.ts +9 -1
  57. package/node_modules/supervision-js-core/dist/detections/buffered-detection-timeline.d.ts.map +1 -1
  58. package/node_modules/supervision-js-core/dist/index.js +77 -41
  59. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  60. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +8 -3
  61. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
  62. package/package.json +1 -1
  63. 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;
@@ -2341,7 +2341,7 @@ function createMediaPlaybackController(options) {
2341
2341
  ((now - playbackOriginNow) / 1000) * playbackRate;
2342
2342
  const playableEnd = options.duration === null
2343
2343
  ? null
2344
- : options.firstTimestamp + Math.max(options.duration, 0);
2344
+ : Math.max(options.firstTimestamp, options.duration);
2345
2345
  let shouldPresentLoopStartSample = false;
2346
2346
  if (playableEnd !== null && requestedMediaTime >= playableEnd) {
2347
2347
  if (!options.loop) {
@@ -2959,6 +2959,178 @@ function createMediaRenderErrorSourcePatch(error) {
2959
2959
  };
2960
2960
  }
2961
2961
 
2962
+ function createMediaFrameNavigation(options) {
2963
+ let activeOperation = null;
2964
+ let displayed = null;
2965
+ let destroyed = false;
2966
+ let destroyError;
2967
+ const controller = {
2968
+ api: {
2969
+ moveToFrame(index) {
2970
+ return startMove(() => landingAt(index));
2971
+ },
2972
+ moveToTime(mediaTime) {
2973
+ return startMove(() => landingAt(options.clock.indexAtOrBefore(mediaTime)));
2974
+ },
2975
+ scrubToFrame(index) {
2976
+ const target = landingAt(index);
2977
+ return startScrub(target);
2978
+ },
2979
+ scrubToTime(mediaTime) {
2980
+ const target = landingAt(options.clock.indexAtOrBefore(mediaTime));
2981
+ return startScrub(target);
2982
+ },
2983
+ },
2984
+ presented(frame) {
2985
+ displayed = frame;
2986
+ const operation = activeOperation;
2987
+ if (!operation ||
2988
+ frame.index !== operation.target.index ||
2989
+ frame.mediaTime !== operation.target.mediaTime) {
2990
+ return;
2991
+ }
2992
+ if (operation.kind === "scrub") {
2993
+ activeOperation = null;
2994
+ operation.resolve({ frame: operation.target, status: "landed" });
2995
+ return;
2996
+ }
2997
+ operation.presented = true;
2998
+ },
2999
+ cancel() {
3000
+ supersedeActive(createAbortError("Media frame navigation was superseded."));
3001
+ },
3002
+ destroy(error) {
3003
+ if (destroyed) {
3004
+ return;
3005
+ }
3006
+ destroyed = true;
3007
+ destroyError =
3008
+ error ?? createAbortError("Media frame navigation was destroyed.");
3009
+ supersedeActive(destroyError);
3010
+ },
3011
+ };
3012
+ return controller;
3013
+ function landingAt(index) {
3014
+ return {
3015
+ duration: options.clock.durationAt(index),
3016
+ index,
3017
+ mediaTime: options.clock.timeAt(index),
3018
+ };
3019
+ }
3020
+ function startMove(getTarget) {
3021
+ if (destroyed) {
3022
+ return own(Promise.reject(destroyError));
3023
+ }
3024
+ let target;
3025
+ try {
3026
+ target = getTarget();
3027
+ }
3028
+ catch (error) {
3029
+ return own(Promise.reject(error));
3030
+ }
3031
+ supersedeActive(createAbortError("Media frame navigation was superseded."));
3032
+ let resolve;
3033
+ let reject;
3034
+ const promise = new Promise((resolvePromise, rejectPromise) => {
3035
+ resolve = resolvePromise;
3036
+ reject = rejectPromise;
3037
+ });
3038
+ const operation = {
3039
+ kind: "move",
3040
+ presented: false,
3041
+ reject,
3042
+ resolve,
3043
+ target,
3044
+ };
3045
+ activeOperation = operation;
3046
+ let command;
3047
+ try {
3048
+ command = options.seek(target.mediaTime);
3049
+ }
3050
+ catch (error) {
3051
+ failMove(operation, error);
3052
+ return own(promise);
3053
+ }
3054
+ void Promise.resolve(command).then(() => {
3055
+ if (activeOperation !== operation) {
3056
+ return;
3057
+ }
3058
+ if (!operation.presented && !isDisplayed(operation.target)) {
3059
+ failMove(operation, new Error("Media seek completed without presenting the requested exact frame."));
3060
+ return;
3061
+ }
3062
+ activeOperation = null;
3063
+ operation.resolve(operation.target);
3064
+ }, (error) => {
3065
+ failMove(operation, error);
3066
+ });
3067
+ return own(promise);
3068
+ }
3069
+ function startScrub(target) {
3070
+ if (destroyed) {
3071
+ throw destroyError;
3072
+ }
3073
+ supersedeActive(createAbortError("Media frame navigation was superseded."));
3074
+ let resolve;
3075
+ const settled = new Promise((resolvePromise) => {
3076
+ resolve = resolvePromise;
3077
+ });
3078
+ const operation = {
3079
+ kind: "scrub",
3080
+ resolve,
3081
+ target,
3082
+ };
3083
+ activeOperation = operation;
3084
+ own(settled);
3085
+ try {
3086
+ options.scrub(target.mediaTime);
3087
+ if (activeOperation === operation && isDisplayed(target)) {
3088
+ activeOperation = null;
3089
+ operation.resolve({ frame: target, status: "landed" });
3090
+ }
3091
+ }
3092
+ catch (error) {
3093
+ if (activeOperation === operation) {
3094
+ activeOperation = null;
3095
+ operation.resolve({ status: "superseded" });
3096
+ }
3097
+ throw error;
3098
+ }
3099
+ return { settled, target };
3100
+ }
3101
+ function isDisplayed(target) {
3102
+ return (displayed?.index === target.index &&
3103
+ displayed.mediaTime === target.mediaTime);
3104
+ }
3105
+ function failMove(operation, error) {
3106
+ if (activeOperation !== operation) {
3107
+ return;
3108
+ }
3109
+ activeOperation = null;
3110
+ operation.reject(error);
3111
+ }
3112
+ function supersedeActive(error) {
3113
+ const operation = activeOperation;
3114
+ if (!operation) {
3115
+ return;
3116
+ }
3117
+ activeOperation = null;
3118
+ if (operation.kind === "move") {
3119
+ operation.reject(error);
3120
+ }
3121
+ else {
3122
+ operation.resolve({ status: "superseded" });
3123
+ }
3124
+ }
3125
+ }
3126
+ function own(promise) {
3127
+ void promise.catch(() => undefined);
3128
+ return promise;
3129
+ }
3130
+ function createAbortError(message) {
3131
+ return new DOMException(message, "AbortError");
3132
+ }
3133
+
2962
3134
  const MILLISECONDS_PER_SECOND$2 = 1000;
2963
3135
  /**
2964
3136
  * The renderer's playback surface over a producer that owns the playhead.
@@ -2985,6 +3157,7 @@ function createMediaRendererTransport(options) {
2985
3157
  let readinessFreezeHeld = false;
2986
3158
  let activeReadinessWait;
2987
3159
  let presentationWait = 0;
3160
+ let displayResize = 0;
2988
3161
  let presentationHold = null;
2989
3162
  const isHoldingForReadiness = () => readinessHoldIntent === playbackIntent ||
2990
3163
  presentationHold?.intent === playbackIntent;
@@ -3143,6 +3316,10 @@ function createMediaRendererTransport(options) {
3143
3316
  await options.waitForReadiness(channel.getPlayhead().mediaTimeS, readinessWait.signal);
3144
3317
  readinessLanded = true;
3145
3318
  }
3319
+ catch (error) {
3320
+ if (intent === playbackIntent)
3321
+ throw error;
3322
+ }
3146
3323
  finally {
3147
3324
  if (activeReadinessWait === readinessWait) {
3148
3325
  activeReadinessWait = undefined;
@@ -3176,6 +3353,11 @@ function createMediaRendererTransport(options) {
3176
3353
  // A pause ends the producer's mechanical hold, so it lands ahead of the
3177
3354
  // releases a readiness hold or an open gesture still owe.
3178
3355
  channel.pause();
3356
+ // A gate may already have mechanically paused the producer, so its
3357
+ // no-op pause emits no state signal to retire Buffering.
3358
+ if (channel.getStatus() === "PAUSED") {
3359
+ publishPlaybackState();
3360
+ }
3179
3361
  void releaseReadinessFreeze();
3180
3362
  void releaseGesture();
3181
3363
  },
@@ -3211,7 +3393,7 @@ function createMediaRendererTransport(options) {
3211
3393
  options.invalidatePresentedFrame?.();
3212
3394
  const navigationPresentation = ++presentationWait;
3213
3395
  presentationHold = null;
3214
- beginPlaybackIntent();
3396
+ const intent = beginPlaybackIntent();
3215
3397
  // Releasing first is what keeps a drag from freezing the picture on
3216
3398
  // release: the producer resumes on its own terms, and the landing decode
3217
3399
  // for a cold region no longer sits between the pointer coming up and
@@ -3229,16 +3411,51 @@ function createMediaRendererTransport(options) {
3229
3411
  landingRelease = null;
3230
3412
  }
3231
3413
  }
3414
+ if (intent !== playbackIntent)
3415
+ return;
3232
3416
  const presentation = options.beginPresentedFrameNavigation?.();
3233
3417
  try {
3418
+ const before = channel.getPlayhead().frame;
3234
3419
  await channel.commit(mediaTime * MILLISECONDS_PER_SECOND$2);
3235
- await presentation?.waitFor(channel.getPlayhead().frame);
3420
+ if (intent !== playbackIntent) {
3421
+ presentation?.cancel();
3422
+ return;
3423
+ }
3424
+ const after = channel.getPlayhead().frame;
3425
+ await presentation?.waitFor(after, before.index === after.index && before.ticks === after.ticks);
3236
3426
  }
3237
3427
  catch (error) {
3238
3428
  presentation?.cancel();
3239
3429
  throw error;
3240
3430
  }
3241
3431
  },
3432
+ async resizeOutput(resize) {
3433
+ const intent = playbackIntent;
3434
+ const request = ++displayResize;
3435
+ const assertCurrent = () => {
3436
+ if (intent !== playbackIntent || request !== displayResize)
3437
+ throw new DOMException("Display resize was superseded.", "AbortError");
3438
+ };
3439
+ // Geometry does not change the viewer's play/pause or annotation gate.
3440
+ // A replacement enters through the same guard as every other frame.
3441
+ const presentation = options.beginPresentedFrameNavigation?.(true);
3442
+ try {
3443
+ const changed = await resize();
3444
+ assertCurrent();
3445
+ if (changed) {
3446
+ await presentation?.waitFor(channel.getPlayhead().frame);
3447
+ assertCurrent();
3448
+ }
3449
+ else {
3450
+ presentation?.cancel();
3451
+ }
3452
+ }
3453
+ catch (error) {
3454
+ presentation?.cancel();
3455
+ assertCurrent();
3456
+ throw error;
3457
+ }
3458
+ },
3242
3459
  async step(direction) {
3243
3460
  options.invalidatePresentedFrame?.();
3244
3461
  const navigationPresentation = ++presentationWait;
@@ -3277,15 +3494,17 @@ function createMediaRendererTransport(options) {
3277
3494
  const guarded = options.waitForPresentationReadiness(mediaTime, signal);
3278
3495
  if (!guarded) {
3279
3496
  if (presentationHold) {
3280
- presentationWait += 1;
3281
- presentationHold = null;
3282
- void releaseReadinessFreeze().then(publishPlaybackState);
3497
+ presentationHold = {
3498
+ intent: playbackIntent,
3499
+ ready: true,
3500
+ wait: ++presentationWait,
3501
+ };
3283
3502
  }
3284
3503
  return null;
3285
3504
  }
3286
3505
  const wait = ++presentationWait;
3287
3506
  const intent = playbackIntent;
3288
- presentationHold = { intent, wait };
3507
+ presentationHold = { intent, ready: false, wait };
3289
3508
  readinessFreezeHeld = true;
3290
3509
  channel.beginInteractiveSeek();
3291
3510
  publishPlaybackState();
@@ -3298,18 +3517,22 @@ function createMediaRendererTransport(options) {
3298
3517
  // provider's diagnostics own the error while the frame stays visible.
3299
3518
  }
3300
3519
  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
3520
  if (wait === presentationWait && !signal.aborted) {
3307
- await releaseReadinessFreeze();
3308
- publishPlaybackState();
3521
+ presentationHold = { intent, ready: true, wait };
3309
3522
  }
3310
3523
  }
3311
3524
  })();
3312
3525
  },
3526
+ didPresentFrame() {
3527
+ const hold = presentationHold;
3528
+ if (!hold?.ready ||
3529
+ hold.intent !== playbackIntent ||
3530
+ hold.wait !== presentationWait) {
3531
+ return;
3532
+ }
3533
+ presentationHold = null;
3534
+ return releaseReadinessFreeze().then(publishPlaybackState);
3535
+ },
3313
3536
  destroy() {
3314
3537
  presentationWait += 1;
3315
3538
  presentationHold = null;
@@ -3363,13 +3586,14 @@ function isSettling(status, seeking) {
3363
3586
  * VideoFrame, so stale readiness can never authorize presentation after a new
3364
3587
  * seek. The downstream handler owns a frame only after the guard succeeds.
3365
3588
  */
3366
- function createProtectedPresentedFrameSource(upstream, onPresentationError = () => undefined) {
3589
+ function createProtectedPresentedFrameSource(upstream, onPresentationError = () => undefined, onAccepted, onPresented) {
3367
3590
  let downstream = null;
3368
3591
  let protect = null;
3369
3592
  let pending = null;
3370
3593
  let active = null;
3371
- let navigation = null;
3594
+ const navigations = new Set();
3372
3595
  let generation = 0;
3596
+ let lastAcceptedKey = null;
3373
3597
  let destroyed = false;
3374
3598
  let firstPresented = false;
3375
3599
  let resolveFirstPresentation;
@@ -3392,27 +3616,27 @@ function createProtectedPresentedFrameSource(upstream, onPresentationError = ()
3392
3616
  if (ticket.settled)
3393
3617
  return;
3394
3618
  ticket.settled = true;
3395
- if (navigation === ticket)
3396
- navigation = null;
3619
+ navigations.delete(ticket);
3397
3620
  if (error === undefined)
3398
3621
  ticket.resolve();
3399
3622
  else
3400
3623
  ticket.reject(error);
3401
3624
  };
3402
- const acceptNavigationFrame = (frameId) => {
3403
- const ticket = navigation;
3404
- if (!ticket || ticket.settled)
3405
- return;
3625
+ const acceptNavigationFrame = (frameId, frameGeneration) => {
3406
3626
  const accepted = frameKey(frameId);
3407
- ticket.accepted.add(accepted);
3408
- if (ticket.target === accepted)
3409
- settleNavigation(ticket);
3627
+ for (const ticket of navigations) {
3628
+ if (frameGeneration <= ticket.afterGeneration)
3629
+ continue;
3630
+ ticket.accepted.add(accepted);
3631
+ if (ticket.target === accepted)
3632
+ settleNavigation(ticket);
3633
+ }
3410
3634
  };
3411
3635
  const failPresentation = (error) => {
3412
3636
  if (!firstPresented)
3413
3637
  rejectFirstPresentation(error);
3414
- if (navigation)
3415
- settleNavigation(navigation, error);
3638
+ for (const ticket of navigations)
3639
+ settleNavigation(ticket, error);
3416
3640
  if (firstPresented)
3417
3641
  onPresentationError(error);
3418
3642
  };
@@ -3450,29 +3674,64 @@ function createProtectedPresentedFrameSource(upstream, onPresentationError = ()
3450
3674
  run.handedOff = true;
3451
3675
  try {
3452
3676
  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;
3677
+ let acknowledged = false;
3678
+ downstream({
3679
+ ...frame,
3680
+ acknowledgePresentation: () => {
3681
+ if (acknowledged ||
3682
+ destroyed ||
3683
+ controller.signal.aborted ||
3684
+ run.generation !== generation) {
3685
+ return;
3686
+ }
3687
+ acknowledged = true;
3688
+ try {
3689
+ acknowledge?.();
3690
+ }
3691
+ catch (error) {
3692
+ failPresentation(error);
3693
+ return;
3694
+ }
3695
+ if (destroyed ||
3696
+ controller.signal.aborted ||
3697
+ run.generation !== generation) {
3698
+ return;
3699
+ }
3700
+ try {
3701
+ const release = onPresented?.();
3702
+ if (release) {
3703
+ void release.catch((error) => {
3704
+ if (!destroyed &&
3705
+ !controller.signal.aborted &&
3706
+ run.generation === generation) {
3707
+ failPresentation(error);
3708
+ }
3709
+ });
3461
3710
  }
3462
- acknowledge();
3463
- },
3464
- }
3465
- : frame);
3711
+ }
3712
+ catch (error) {
3713
+ failPresentation(error);
3714
+ return;
3715
+ }
3716
+ if (destroyed ||
3717
+ controller.signal.aborted ||
3718
+ run.generation !== generation) {
3719
+ return;
3720
+ }
3721
+ lastAcceptedKey = frameKey(frame.frameId);
3722
+ onAccepted?.(frame.frameId, frame.mediaTimeS);
3723
+ acceptNavigationFrame(frame.frameId, run.generation);
3724
+ if (!firstPresented) {
3725
+ firstPresented = true;
3726
+ resolveFirstPresentation();
3727
+ }
3728
+ },
3729
+ });
3466
3730
  }
3467
3731
  catch (error) {
3468
3732
  failPresentation(error);
3469
3733
  return;
3470
3734
  }
3471
- acceptNavigationFrame(frame.frameId);
3472
- if (!firstPresented) {
3473
- firstPresented = true;
3474
- resolveFirstPresentation();
3475
- }
3476
3735
  };
3477
3736
  let guarded;
3478
3737
  try {
@@ -3532,16 +3791,18 @@ function createProtectedPresentedFrameSource(upstream, onPresentationError = ()
3532
3791
  cancelActive();
3533
3792
  pending?.frame.close();
3534
3793
  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);
3794
+ for (const ticket of navigations)
3795
+ settleNavigation(ticket);
3796
+ },
3797
+ beginNavigation(preserveCurrent = false) {
3798
+ if (!preserveCurrent) {
3799
+ generation += 1;
3800
+ cancelActive();
3801
+ pending?.frame.close();
3802
+ pending = null;
3803
+ for (const ticket of navigations)
3804
+ settleNavigation(ticket);
3805
+ }
3545
3806
  let resolve;
3546
3807
  let reject;
3547
3808
  const promise = new Promise((onResolve, onReject) => {
@@ -3553,20 +3814,28 @@ function createProtectedPresentedFrameSource(upstream, onPresentationError = ()
3553
3814
  void promise.catch(() => undefined);
3554
3815
  const ticket = {
3555
3816
  accepted: new Set(),
3817
+ afterGeneration: generation,
3556
3818
  promise,
3557
3819
  reject,
3558
3820
  resolve,
3559
3821
  settled: false,
3560
3822
  target: null,
3561
3823
  };
3562
- navigation = ticket;
3824
+ navigations.add(ticket);
3563
3825
  return {
3564
- waitFor(frameId) {
3826
+ waitFor(frameId, allowCurrent = false) {
3565
3827
  if (ticket.settled)
3566
3828
  return ticket.promise;
3567
3829
  ticket.target = frameKey(frameId);
3568
3830
  if (ticket.accepted.has(ticket.target))
3569
3831
  settleNavigation(ticket);
3832
+ if (allowCurrent &&
3833
+ lastAcceptedKey === ticket.target &&
3834
+ generation === ticket.afterGeneration &&
3835
+ pending === null &&
3836
+ active === null) {
3837
+ settleNavigation(ticket);
3838
+ }
3570
3839
  return ticket.promise;
3571
3840
  },
3572
3841
  cancel() {
@@ -3583,8 +3852,8 @@ function createProtectedPresentedFrameSource(upstream, onPresentationError = ()
3583
3852
  cancelActive();
3584
3853
  pending?.frame.close();
3585
3854
  pending = null;
3586
- if (navigation)
3587
- settleNavigation(navigation);
3855
+ for (const ticket of navigations)
3856
+ settleNavigation(ticket);
3588
3857
  upstream.onPresentedFrame((presented) => presented.frame.close());
3589
3858
  if (!firstPresented)
3590
3859
  resolveFirstPresentation();
@@ -3723,6 +3992,8 @@ async function createMediaRendererCore(options, providers) {
3723
3992
  options.onState?.(state);
3724
3993
  },
3725
3994
  });
3995
+ let frameClock = null;
3996
+ let frameNavigation;
3726
3997
  let activeSampleIterator;
3727
3998
  let mediaInput;
3728
3999
  let playbackController;
@@ -3789,6 +4060,7 @@ async function createMediaRendererCore(options, providers) {
3789
4060
  runtimeState.setLoading();
3790
4061
  break;
3791
4062
  case MediaRendererPlaybackState.Error:
4063
+ frameNavigation?.cancel();
3792
4064
  runtimeState.setRenderError(new Error("Media playback failed."));
3793
4065
  break;
3794
4066
  }
@@ -4104,8 +4376,53 @@ async function createMediaRendererCore(options, providers) {
4104
4376
  });
4105
4377
  });
4106
4378
  };
4379
+ const seekMediaTime = async (mediaTime) => {
4380
+ if (runtimeState.isDestroyed()) {
4381
+ throw new Error("Media renderer has been destroyed.");
4382
+ }
4383
+ if (runtimeState.isError()) {
4384
+ throw new Error(runtimeState.errorMessage() ?? "Media renderer is in error state.");
4385
+ }
4386
+ const targetTime = clampSeekTime({
4387
+ duration: runtimeState.duration(),
4388
+ firstTimestamp,
4389
+ mediaTime,
4390
+ });
4391
+ endSeekGesture();
4392
+ if (transport) {
4393
+ await transport.commit(targetTime);
4394
+ return;
4395
+ }
4396
+ await seekPullSample(targetTime);
4397
+ };
4398
+ const scrubMediaTime = (mediaTime) => {
4399
+ if (runtimeState.isDestroyed() || runtimeState.isError())
4400
+ return;
4401
+ const targetTime = clampSeekTime({
4402
+ duration: runtimeState.duration(),
4403
+ firstTimestamp,
4404
+ mediaTime,
4405
+ });
4406
+ isSeekGestureInFlight = true;
4407
+ publishPlaybackActivity();
4408
+ if (transport) {
4409
+ transport.scrub(targetTime);
4410
+ return;
4411
+ }
4412
+ pendingPullScrubTime = targetTime;
4413
+ if (pullScrubReadInFlight)
4414
+ navigationVersion += 1;
4415
+ schedulePullScrubPreview();
4416
+ };
4107
4417
  const renderer = {
4418
+ get frameClock() {
4419
+ return frameClock;
4420
+ },
4421
+ get frameNavigation() {
4422
+ return frameNavigation?.api ?? null;
4423
+ },
4108
4424
  async play() {
4425
+ frameNavigation?.cancel();
4109
4426
  if (runtimeState.isDestroyed()) {
4110
4427
  throw new Error("Media renderer has been destroyed.");
4111
4428
  }
@@ -4134,6 +4451,7 @@ async function createMediaRendererCore(options, providers) {
4134
4451
  playbackController.play();
4135
4452
  },
4136
4453
  pause() {
4454
+ frameNavigation?.cancel();
4137
4455
  if (runtimeState.isDestroyed()) {
4138
4456
  return;
4139
4457
  }
@@ -4149,6 +4467,7 @@ async function createMediaRendererCore(options, providers) {
4149
4467
  playbackController?.pause();
4150
4468
  },
4151
4469
  async togglePlayback() {
4470
+ frameNavigation?.cancel();
4152
4471
  if (runtimeState.isDestroyed()) {
4153
4472
  return;
4154
4473
  }
@@ -4164,49 +4483,19 @@ async function createMediaRendererCore(options, providers) {
4164
4483
  await renderer.play();
4165
4484
  },
4166
4485
  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);
4486
+ frameNavigation?.cancel();
4487
+ await seekMediaTime(mediaTime);
4184
4488
  },
4185
4489
  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();
4490
+ frameNavigation?.cancel();
4491
+ scrubMediaTime(mediaTime);
4205
4492
  },
4206
4493
  async stepForward() {
4494
+ frameNavigation?.cancel();
4207
4495
  await stepToAdjacentSample("forward");
4208
4496
  },
4209
4497
  async stepBackward() {
4498
+ frameNavigation?.cancel();
4210
4499
  await stepToAdjacentSample("backward");
4211
4500
  },
4212
4501
  setPlaybackRate(playbackRate) {
@@ -4246,7 +4535,7 @@ async function createMediaRendererCore(options, providers) {
4246
4535
  if (requestVersion !== navigationVersion || runtimeState.isDestroyed()) {
4247
4536
  return;
4248
4537
  }
4249
- const presentedSample = mediaScene.setPresentation(currentPresentation, mediaTime);
4538
+ const presentedSample = mediaScene.setPresentation(currentPresentation, runtimeState.presentedTime() ?? runtimeState.currentTime());
4250
4539
  if (presentedSample) {
4251
4540
  runtimeState.recordPresentationUpdate(presentedSample);
4252
4541
  }
@@ -4337,6 +4626,7 @@ async function createMediaRendererCore(options, providers) {
4337
4626
  return;
4338
4627
  }
4339
4628
  cancelPullScrubPreview();
4629
+ frameNavigation?.destroy();
4340
4630
  runtimeState.markDestroyed();
4341
4631
  protectedPresentedFrames?.destroy();
4342
4632
  transport?.destroy();
@@ -4355,6 +4645,7 @@ async function createMediaRendererCore(options, providers) {
4355
4645
  throw new Error("Provide either detectionFrames or detectionSource, not both.");
4356
4646
  }
4357
4647
  const mediaSource = await openRendererMediaSource(options, providers);
4648
+ frameClock = mediaSource.frameClock ?? null;
4358
4649
  mediaInput = mediaSource.input;
4359
4650
  sampleSink = mediaSource.sampleSink;
4360
4651
  if (runtimeState.isDestroyed()) {
@@ -4388,11 +4679,12 @@ async function createMediaRendererCore(options, providers) {
4388
4679
  // the producer keeps running. Cut off future frames and state
4389
4680
  // signals before publishing the rendering failure.
4390
4681
  protectedPresentedFrames?.destroy();
4682
+ frameNavigation?.destroy(error);
4391
4683
  transport?.destroy();
4392
4684
  presentedFrameChannel.pause();
4393
4685
  if (!runtimeState.isDestroyed())
4394
4686
  runtimeState.setRenderError(error);
4395
- })
4687
+ }, (id, mediaTime) => frameNavigation?.presented({ index: id.index, mediaTime }), () => transport?.didPresentFrame())
4396
4688
  : undefined;
4397
4689
  const mediaDimensions = runtimeState.recordMediaMetadata(metadata);
4398
4690
  mediaScene = await providers.createScene({
@@ -4437,14 +4729,13 @@ async function createMediaRendererCore(options, providers) {
4437
4729
  });
4438
4730
  publishPlaybackActivity();
4439
4731
  runtimeState.setRendererBackend(mediaScene.rendererBackend);
4440
- detectionTimeline.setTimelineContext?.({
4441
- duration: metadata.duration,
4442
- loop: options.loop !== false,
4443
- });
4444
- mediaScene.setTimelineContext?.({
4732
+ const timelineContext = {
4445
4733
  duration: metadata.duration,
4734
+ firstTimestamp: metadata.firstTimestamp,
4446
4735
  loop: options.loop !== false,
4447
- });
4736
+ };
4737
+ detectionTimeline.setTimelineContext?.(timelineContext);
4738
+ mediaScene.setTimelineContext?.(timelineContext);
4448
4739
  mediaScene.initializeMedia(mediaDimensions);
4449
4740
  runtimeState.setSourceReady(metadata);
4450
4741
  if (presentedFrameChannel) {
@@ -4474,9 +4765,35 @@ async function createMediaRendererCore(options, providers) {
4474
4765
  ? holdForPresentationReadiness
4475
4766
  : undefined,
4476
4767
  invalidatePresentedFrame: () => protectedPresentedFrames?.invalidate(),
4477
- beginPresentedFrameNavigation: () => protectedPresentedFrames.beginNavigation(),
4768
+ beginPresentedFrameNavigation: (preserveCurrent) => protectedPresentedFrames.beginNavigation(preserveCurrent),
4478
4769
  });
4479
- protectedPresentedFrames?.activate((presented, signal) => transport.protectPresentation(presented.mediaTimeS, signal));
4770
+ const setDisplay = mediaSource.setDisplay;
4771
+ if (setDisplay) {
4772
+ renderer.setDisplay = async (display) => {
4773
+ if (runtimeState.isDestroyed())
4774
+ throw new Error("Media renderer has been destroyed.");
4775
+ await transport.resizeOutput(() => setDisplay(display));
4776
+ };
4777
+ }
4778
+ if (frameClock) {
4779
+ frameNavigation = createMediaFrameNavigation({
4780
+ clock: frameClock,
4781
+ seek: seekMediaTime,
4782
+ scrub(mediaTime) {
4783
+ if (runtimeState.isDestroyed()) {
4784
+ throw new Error("Media renderer has been destroyed.");
4785
+ }
4786
+ if (runtimeState.isError()) {
4787
+ throw new Error(runtimeState.errorMessage() ??
4788
+ "Media renderer is in error state.");
4789
+ }
4790
+ scrubMediaTime(mediaTime);
4791
+ },
4792
+ });
4793
+ }
4794
+ protectedPresentedFrames?.activate((presented, signal) => pushPresentationReady
4795
+ ? transport.protectPresentation(presented.mediaTimeS, signal)
4796
+ : null);
4480
4797
  if (initialPlaybackRate !== 1) {
4481
4798
  transport.setPlaybackRate(initialPlaybackRate);
4482
4799
  }
@@ -4498,7 +4815,11 @@ async function createMediaRendererCore(options, providers) {
4498
4815
  pushPresentationReady = true;
4499
4816
  runtimeState.setReady();
4500
4817
  if (options.autoPlay ?? true) {
4501
- await renderer.play();
4818
+ // Opening exposes the writer even when autoplay awaits future detections.
4819
+ void renderer.play().catch((error) => {
4820
+ if (!runtimeState.isDestroyed())
4821
+ runtimeState.setRenderError(error);
4822
+ });
4502
4823
  }
4503
4824
  return renderer;
4504
4825
  }
@@ -4662,7 +4983,7 @@ function clampSeekTime(options) {
4662
4983
  const startTime = options.firstTimestamp;
4663
4984
  const endTime = options.duration === null
4664
4985
  ? null
4665
- : options.firstTimestamp + Math.max(options.duration, 0);
4986
+ : Math.max(options.firstTimestamp, options.duration);
4666
4987
  return Math.min(Math.max(mediaTime, startTime), endTime ?? mediaTime);
4667
4988
  }
4668
4989
 
@@ -5559,9 +5880,10 @@ function createPreparedWindowTimeline() {
5559
5880
  },
5560
5881
  };
5561
5882
  function isLoopingTimeline() {
5883
+ const firstTimestamp = timelineContext.firstTimestamp ?? 0;
5562
5884
  return (timelineContext.loop &&
5563
5885
  timelineContext.duration !== null &&
5564
- timelineContext.duration > 0);
5886
+ timelineContext.duration > firstTimestamp);
5565
5887
  }
5566
5888
  /**
5567
5889
  * Travel forward from `mediaTime` to `frameTime`, wrapping at the media end,
@@ -5571,11 +5893,12 @@ function createPreparedWindowTimeline() {
5571
5893
  if (!isLoopingTimeline() || timelineContext.duration === null) {
5572
5894
  return Math.max(0, frameTime - mediaTime);
5573
5895
  }
5574
- const duration = timelineContext.duration;
5575
- const normalizedFrameTime = modulo(frameTime, duration);
5576
- const normalizedMediaTime = modulo(mediaTime, duration);
5896
+ const firstTimestamp = timelineContext.firstTimestamp ?? 0;
5897
+ const loopSpan = timelineContext.duration - firstTimestamp;
5898
+ const normalizedFrameTime = firstTimestamp + modulo(frameTime - firstTimestamp, loopSpan);
5899
+ const normalizedMediaTime = firstTimestamp + modulo(mediaTime - firstTimestamp, loopSpan);
5577
5900
  const rawDistance = normalizedFrameTime - normalizedMediaTime;
5578
- return rawDistance >= 0 ? rawDistance : rawDistance + duration;
5901
+ return rawDistance >= 0 ? rawDistance : rawDistance + loopSpan;
5579
5902
  }
5580
5903
  /**
5581
5904
  * The buffer's end time is the far edge of what the playhead reaches next, and
@@ -6642,17 +6965,23 @@ function createPreparedAnnotationWindow(options) {
6642
6965
  const spanFrameCount = resolvePreparedWindowFrameCount({
6643
6966
  renderPreparation: options.renderPreparation,
6644
6967
  });
6968
+ const frameRevisions = new WeakMap();
6969
+ let nextFrameRevision = 0;
6645
6970
  const getPreparedFrame = (mediaTime) => options.detectionTimeline.selectFrame(mediaTime) ?? null;
6646
6971
  const isFrameArtifactPrepared = (mediaTime) => options.getLayers().every((layer) => layer.isArtifactPrepared(mediaTime));
6647
6972
  return {
6648
6973
  getPreparedFrame,
6649
6974
  getReadinessToken(mediaTime) {
6650
6975
  const detectionFrame = options.detectionTimeline.selectFrame(mediaTime);
6976
+ if (detectionFrame && !frameRevisions.has(detectionFrame)) {
6977
+ frameRevisions.set(detectionFrame, ++nextFrameRevision);
6978
+ }
6979
+ const revision = detectionFrame ? frameRevisions.get(detectionFrame) : 0;
6651
6980
  const cooks = options
6652
6981
  .getLayers()
6653
6982
  .map((layer) => (layer.isArtifactPrepared(mediaTime) ? "1" : "0"))
6654
6983
  .join("");
6655
- return `${detectionFrame?.frameIndex ?? "time"}:${detectionFrame?.mediaTime ?? "none"}:${cooks}`;
6984
+ return `${detectionFrame?.frameIndex ?? "time"}:${detectionFrame?.mediaTime ?? "none"}:${revision}:${cooks}`;
6656
6985
  },
6657
6986
  getSnapshot() {
6658
6987
  const playheadMediaTime = options.getPlayheadMediaTime();
@@ -13143,13 +13472,14 @@ async function createPixiMediaScene(options) {
13143
13472
  });
13144
13473
  },
13145
13474
  };
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.
13475
+ // A content redraw updates editing geometry at the same media time without
13476
+ // advancing the focus animation.
13149
13477
  const annotationDrawLayers = {
13150
13478
  ...presentLayers,
13151
13479
  advanceFocus: () => undefined,
13152
- drawAnnotationOverlay: () => undefined,
13480
+ drawAnnotationOverlay: frameChannel
13481
+ ? presentLayers.drawAnnotationOverlay
13482
+ : () => undefined,
13153
13483
  };
13154
13484
  // A viewport-style redraw covers the layers whose geometry is a function of
13155
13485
  // the viewport scale. Neither the mask raster nor the interaction hit map
@@ -13593,6 +13923,8 @@ async function createPixiMediaScene(options) {
13593
13923
  return pick;
13594
13924
  },
13595
13925
  destroy() {
13926
+ if (isDestroyed)
13927
+ return;
13596
13928
  isDestroyed = true;
13597
13929
  if (displayFrameHandle !== null) {
13598
13930
  cancelDisplayFrame(displayFrameHandle);
@@ -13634,6 +13966,7 @@ async function createPixiMediaScene(options) {
13634
13966
  texture: true,
13635
13967
  textureSource: true,
13636
13968
  });
13969
+ backdrop.remove();
13637
13970
  },
13638
13971
  };
13639
13972
  /**
@@ -14648,10 +14981,11 @@ const STREAM_DETECTION_BUFFER_DEFAULTS = {
14648
14981
  refreshIntervalSeconds: 0.25,
14649
14982
  };
14650
14983
  /**
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.
14984
+ * Once a session is open, playback waits for annotations unless a host says
14985
+ * otherwise, so a viewer sees a frame and the marks that belong to it together.
14986
+ * Opening itself may show a bare initial frame while future annotations arrive.
14987
+ * `playbackGate: false` on the session, or either gate's own `enabled`, turns
14988
+ * it off.
14655
14989
  */
14656
14990
  const DETECTION_PLAYBACK_GATE_DEFAULTS = {
14657
14991
  enabled: true,
@@ -14844,10 +15178,16 @@ async function prepareSessionDetections(options) {
14844
15178
  sourcePresentations: [],
14845
15179
  };
14846
15180
  }
15181
+ const writableSource = isWritableSource(detections.source)
15182
+ ? detections.source
15183
+ : undefined;
14847
15184
  return {
14848
- appendableSources: new Map(),
15185
+ appendableSource: writableSource,
15186
+ appendableSources: new Map(writableSource ? [[writableSource.datasetId, writableSource]] : []),
14849
15187
  detectionFrames: detections.frames,
14850
- detectionSource: detections.source,
15188
+ detectionSource: detections.source
15189
+ ? borrowDetectionSource(detections.source)
15190
+ : undefined,
14851
15191
  sourcePresentations: [],
14852
15192
  };
14853
15193
  }
@@ -14866,6 +15206,7 @@ async function prepareMultiSourceSessionDetections(options) {
14866
15206
  throw new Error("detections.sources must include at least one source.");
14867
15207
  }
14868
15208
  const appendableSources = new Map();
15209
+ const ownedSources = [];
14869
15210
  const compositeEntries = [];
14870
15211
  validateMediaSessionDetectionSources(detections.sources);
14871
15212
  try {
@@ -14875,7 +15216,12 @@ async function prepareMultiSourceSessionDetections(options) {
14875
15216
  appendable: sourceOptions.appendable,
14876
15217
  mode: options.mode,
14877
15218
  })
14878
- : undefined;
15219
+ : isWritableSource(sourceOptions.source)
15220
+ ? sourceOptions.source
15221
+ : undefined;
15222
+ if (sourceOptions.appendable && appendableSource) {
15223
+ ownedSources.push(appendableSource);
15224
+ }
14879
15225
  if (appendableSource) {
14880
15226
  appendableSources.set(sourceOptions.id, appendableSource);
14881
15227
  }
@@ -14884,13 +15230,15 @@ async function prepareMultiSourceSessionDetections(options) {
14884
15230
  id: sourceOptions.id,
14885
15231
  order: sourceOptions.order,
14886
15232
  requiredForCoverage: sourceOptions.requiredForCoverage,
14887
- source: sourceOptions.source ?? appendableSource,
15233
+ source: sourceOptions.source
15234
+ ? borrowDetectionSource(sourceOptions.source)
15235
+ : appendableSource,
14888
15236
  sync: sourceOptions.sync,
14889
15237
  });
14890
15238
  }
14891
15239
  }
14892
15240
  catch (error) {
14893
- for (const source of appendableSources.values()) {
15241
+ for (const source of ownedSources) {
14894
15242
  source.destroy?.();
14895
15243
  }
14896
15244
  throw error;
@@ -14907,6 +15255,36 @@ async function prepareMultiSourceSessionDetections(options) {
14907
15255
  })),
14908
15256
  };
14909
15257
  }
15258
+ function isWritableSource(source) {
15259
+ const candidate = source;
15260
+ return (typeof candidate?.datasetId === "string" &&
15261
+ [
15262
+ "appendFrames",
15263
+ "replaceFrames",
15264
+ "clear",
15265
+ "getVersion",
15266
+ "getSummary",
15267
+ "waitForRange",
15268
+ "getAvailableRanges",
15269
+ ].every((method) => typeof candidate[method] === "function"));
15270
+ }
15271
+ function borrowDetectionSource(source) {
15272
+ return {
15273
+ loadFrames: source.loadFrames.bind(source),
15274
+ ...(source.getVersion
15275
+ ? { getVersion: source.getVersion.bind(source) }
15276
+ : {}),
15277
+ ...(source.getChangesSince
15278
+ ? { getChangesSince: source.getChangesSince.bind(source) }
15279
+ : {}),
15280
+ ...(source.getAvailableRanges
15281
+ ? { getAvailableRanges: source.getAvailableRanges.bind(source) }
15282
+ : {}),
15283
+ ...(source.waitForRange
15284
+ ? { waitForRange: source.waitForRange.bind(source) }
15285
+ : {}),
15286
+ };
15287
+ }
14910
15288
  function validateMediaSessionDetectionSources(sources) {
14911
15289
  const sourceIds = new Set();
14912
15290
  for (const source of sources) {
@@ -15529,6 +15907,9 @@ async function createMediaSession(options) {
15529
15907
  };
15530
15908
  };
15531
15909
  return {
15910
+ frameClock: renderer.frameClock ?? null,
15911
+ frameNavigation: renderer.frameNavigation ?? null,
15912
+ setDisplay: renderer.setDisplay?.bind(renderer),
15532
15913
  detectionSource: sessionDetections.detectionSource,
15533
15914
  media: sessionMedia.state,
15534
15915
  renderer,