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.
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +557 -134
- package/dist/index.js.map +1 -1
- package/dist/mask-preparation.worker.js.map +1 -1
- package/dist/media/media-frame-clock.d.ts +4 -0
- package/dist/media/media-frame-clock.d.ts.map +1 -0
- package/dist/media/media-source.d.ts +8 -0
- package/dist/media/media-source.d.ts.map +1 -1
- package/dist/media/mediabunny-media-source.d.ts.map +1 -1
- package/dist/media/video-engine-media-source.d.ts.map +1 -1
- package/dist/media/video-engine-media-source.js +1 -1
- package/dist/render-preparation/prepared-window-timeline.d.ts +1 -0
- package/dist/render-preparation/prepared-window-timeline.d.ts.map +1 -1
- package/dist/renderers/media-frame-navigation.d.ts +17 -0
- package/dist/renderers/media-frame-navigation.d.ts.map +1 -0
- package/dist/renderers/media-renderer-core.d.ts.map +1 -1
- package/dist/renderers/media-renderer-scene.d.ts +1 -0
- package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
- package/dist/renderers/media-renderer-transport.d.ts +4 -1
- package/dist/renderers/media-renderer-transport.d.ts.map +1 -1
- package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
- package/dist/renderers/prepared-annotation-window.d.ts.map +1 -1
- package/dist/renderers/presented-frame-channel.d.ts +8 -6
- package/dist/renderers/presented-frame-channel.d.ts.map +1 -1
- package/dist/sessions/media-session-defaults.d.ts.map +1 -1
- package/dist/sessions/media-session-detections.d.ts.map +1 -1
- package/dist/sessions/media-session.d.ts.map +1 -1
- package/dist/tracking.worker.js.map +1 -1
- package/dist/types/media-frame-clock.d.ts +16 -0
- package/dist/types/media-frame-clock.d.ts.map +1 -0
- package/dist/types/media-frame-navigation.d.ts +28 -0
- package/dist/types/media-frame-navigation.d.ts.map +1 -0
- package/dist/types/media-renderer-display.d.ts +12 -0
- package/dist/types/media-renderer-display.d.ts.map +1 -0
- package/dist/types/media-renderer.d.ts +13 -0
- package/dist/types/media-renderer.d.ts.map +1 -1
- package/dist/types/media-session.d.ts +15 -5
- package/dist/types/media-session.d.ts.map +1 -1
- package/dist/types/render-preparation.d.ts +8 -5
- package/dist/types/render-preparation.d.ts.map +1 -1
- package/dist/{video-engine-media-source-CUXnOJOV.js → video-engine-media-source-DR-x1UfK.js} +35 -2
- package/dist/video-engine-media-source-DR-x1UfK.js.map +1 -0
- package/dist/web-video-engine/analysis.js +16 -0
- package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts +7 -2
- package/dist/web-video-engine/decode-scheduler.d.ts +9 -3
- package/dist/web-video-engine/decode-session.d.ts +5 -2
- package/dist/web-video-engine/decode-source.d.ts +2 -0
- package/dist/web-video-engine/engine-core.d.ts +10 -0
- package/dist/web-video-engine/engine.js +131 -15
- package/dist/web-video-engine/engine.worker.js +513 -36
- package/dist/web-video-engine/frame-cache.d.ts +2 -0
- package/dist/web-video-engine/pausable-deadline.d.ts +11 -0
- package/dist/web-video-engine/scrub-controller.d.ts +8 -4
- package/dist/web-video-engine/scrub-cursor.d.ts +6 -5
- package/dist/web-video-engine/video-engine.d.ts +16 -1
- package/dist/web-video-engine/worker-protocol.d.ts +9 -1
- package/node_modules/supervision-js-core/dist/detections/buffered-detection-timeline.d.ts.map +1 -1
- package/node_modules/supervision-js-core/dist/index.js +77 -41
- package/node_modules/supervision-js-core/dist/index.js.map +1 -1
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +8 -3
- package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
- package/package.json +1 -1
- 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-
|
|
4
|
-
export { c as createWebVideoEngineMediaRendererSource, g as getMediaErrorKind, i as isMediaSourceError, o as openWebVideoEngineMediaSource } from './video-engine-media-source-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3408
|
-
|
|
3409
|
-
|
|
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
|
-
|
|
3415
|
-
settleNavigation(
|
|
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
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
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
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
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
|
-
|
|
3536
|
-
settleNavigation(
|
|
3537
|
-
},
|
|
3538
|
-
beginNavigation() {
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3587
|
-
settleNavigation(
|
|
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
|
-
|
|
4168
|
-
|
|
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
|
-
|
|
4187
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4941
|
+
const iterator = sampleSink.samples(currentTime, undefined, {
|
|
4593
4942
|
skipLiveWait: true,
|
|
4594
4943
|
});
|
|
4595
4944
|
try {
|
|
4596
|
-
|
|
4597
|
-
|
|
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
|
-
:
|
|
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 >
|
|
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
|
|
5575
|
-
const
|
|
5576
|
-
const
|
|
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 +
|
|
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
|
-
//
|
|
13147
|
-
//
|
|
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:
|
|
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
|
-
*
|
|
14652
|
-
* a frame and the marks that belong to it together
|
|
14653
|
-
*
|
|
14654
|
-
* own `enabled`, turns
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
|
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
|
|
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,
|