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.
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +508 -127
- 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 +6 -0
- package/dist/media/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 +6 -1
- package/dist/web-video-engine/decode-scheduler.d.ts +8 -2
- 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 +9 -0
- package/dist/web-video-engine/engine.js +103 -11
- package/dist/web-video-engine/engine.worker.js +492 -31
- 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 +3 -0
- package/dist/web-video-engine/video-engine.d.ts +12 -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
|
@@ -26172,6 +26172,19 @@
|
|
|
26172
26172
|
return queued.timestampS;
|
|
26173
26173
|
return this.reachable;
|
|
26174
26174
|
}
|
|
26175
|
+
async resizeOutput({ width, height }) {
|
|
26176
|
+
await this.exclusive(async () => {
|
|
26177
|
+
if (this.closed || this.outputWidth === undefined)
|
|
26178
|
+
return;
|
|
26179
|
+
if (this.outputWidth === width && this.outputHeight === height)
|
|
26180
|
+
return;
|
|
26181
|
+
this.quiesce();
|
|
26182
|
+
this.outputWidth = width;
|
|
26183
|
+
this.outputHeight = height;
|
|
26184
|
+
this.exhausted = true;
|
|
26185
|
+
this.epoch++;
|
|
26186
|
+
});
|
|
26187
|
+
}
|
|
26175
26188
|
/** The frame at or before `targetS`, or null when nothing precedes it. */
|
|
26176
26189
|
async frameAt(targetS) {
|
|
26177
26190
|
const landed = await this.exclusive(() => this.land(targetS));
|
|
@@ -26701,6 +26714,25 @@
|
|
|
26701
26714
|
function nativeResolution() {
|
|
26702
26715
|
return { kind: "native" };
|
|
26703
26716
|
}
|
|
26717
|
+
/**
|
|
26718
|
+
* Decode at the size the frame will actually occupy inside a box the consumer
|
|
26719
|
+
* describes: native aspect fitted into boxWidth x boxHeight, times the clamped
|
|
26720
|
+
* device pixel ratio. Letterboxing is why the box is two numbers and not one,
|
|
26721
|
+
* since a portrait source in a landscape box paints far narrower than the box.
|
|
26722
|
+
*
|
|
26723
|
+
* This is the strategy for presentation "frames". There the consumer owns the
|
|
26724
|
+
* compositor, so it is the only side that knows the box, and viewportResolution
|
|
26725
|
+
* has nothing to read.
|
|
26726
|
+
*/
|
|
26727
|
+
function displayBoxResolution(options) {
|
|
26728
|
+
return {
|
|
26729
|
+
kind: "displayBox",
|
|
26730
|
+
boxWidth: options.boxWidth,
|
|
26731
|
+
boxHeight: options.boxHeight,
|
|
26732
|
+
devicePixelRatio: options.devicePixelRatio,
|
|
26733
|
+
maxDevicePixelRatio: options.maxDevicePixelRatio ?? 2,
|
|
26734
|
+
};
|
|
26735
|
+
}
|
|
26704
26736
|
/** Runs the descriptor against a context to produce a target decode width. */
|
|
26705
26737
|
function strategyWidth(strategy, ctx) {
|
|
26706
26738
|
switch (strategy.kind) {
|
|
@@ -27120,6 +27152,9 @@
|
|
|
27120
27152
|
? sampleAtPresentationTime(sample, timeline.fromSourceTime(sample.timestamp))
|
|
27121
27153
|
: null;
|
|
27122
27154
|
return {
|
|
27155
|
+
...(source.resizeOutput
|
|
27156
|
+
? { resizeOutput: source.resizeOutput.bind(source) }
|
|
27157
|
+
: {}),
|
|
27123
27158
|
async frameAt(targetS) {
|
|
27124
27159
|
return wrap(await source.frameAt(timeline.toSourceTime(targetS)));
|
|
27125
27160
|
},
|
|
@@ -27172,6 +27207,7 @@
|
|
|
27172
27207
|
};
|
|
27173
27208
|
return {
|
|
27174
27209
|
decodePath: "sample",
|
|
27210
|
+
resizeOutput: async () => undefined,
|
|
27175
27211
|
// Pool-bound: every outstanding frame is a slot the sink cannot refill.
|
|
27176
27212
|
playReadAhead: 1,
|
|
27177
27213
|
track: handle.track,
|
|
@@ -27207,6 +27243,9 @@
|
|
|
27207
27243
|
function sessionProvider(handle) {
|
|
27208
27244
|
return {
|
|
27209
27245
|
decodePath: "session",
|
|
27246
|
+
...(handle.session.resizeOutput
|
|
27247
|
+
? { resizeOutput: handle.session.resizeOutput.bind(handle.session) }
|
|
27248
|
+
: {}),
|
|
27210
27249
|
playReadAhead: PLAYBACK.READ_AHEAD_CANVAS,
|
|
27211
27250
|
track: handle.track,
|
|
27212
27251
|
keyframeProbe: handle.keyframeProbe,
|
|
@@ -27630,6 +27669,8 @@
|
|
|
27630
27669
|
class CanvasSinkScrubCursor {
|
|
27631
27670
|
provider;
|
|
27632
27671
|
trackInfo;
|
|
27672
|
+
outputGeneration = 0;
|
|
27673
|
+
outputReads = new Set();
|
|
27633
27674
|
disposeSource;
|
|
27634
27675
|
/**
|
|
27635
27676
|
* Forward-playback iterator. Lives only between attachPlay and detachPlay so
|
|
@@ -27669,6 +27710,41 @@
|
|
|
27669
27710
|
this.pendingSeekTargetS === null &&
|
|
27670
27711
|
!this.seekDraining);
|
|
27671
27712
|
}
|
|
27713
|
+
async resizeOutput({ width, height }) {
|
|
27714
|
+
if (!this.provider.resizeOutput) {
|
|
27715
|
+
throw new WebVideoEngineError(WebVideoEngineErrorCode.PresentationMismatch, "this decode source cannot resize frame output in place");
|
|
27716
|
+
}
|
|
27717
|
+
if (this.closed ||
|
|
27718
|
+
(width === this.trackInfo.decodeWidth &&
|
|
27719
|
+
height === this.trackInfo.decodeHeight))
|
|
27720
|
+
return false;
|
|
27721
|
+
this.outputGeneration++;
|
|
27722
|
+
this.pendingSeekTargetS = null;
|
|
27723
|
+
const returning = this.iterator?.return();
|
|
27724
|
+
this.iterator = null;
|
|
27725
|
+
this.nextPending = 0;
|
|
27726
|
+
this.lastEmittedFrame = null;
|
|
27727
|
+
await Promise.allSettled([
|
|
27728
|
+
...this.outputReads,
|
|
27729
|
+
...(returning ? [returning] : []),
|
|
27730
|
+
]);
|
|
27731
|
+
if (this.closed)
|
|
27732
|
+
return false;
|
|
27733
|
+
await this.provider.resizeOutput({ width, height });
|
|
27734
|
+
if (this.closed)
|
|
27735
|
+
return false;
|
|
27736
|
+
this.trackInfo = {
|
|
27737
|
+
...this.trackInfo,
|
|
27738
|
+
decodeWidth: width,
|
|
27739
|
+
decodeHeight: height,
|
|
27740
|
+
};
|
|
27741
|
+
return true;
|
|
27742
|
+
}
|
|
27743
|
+
trackOutputRead(read) {
|
|
27744
|
+
this.outputReads.add(read);
|
|
27745
|
+
void read.then(() => this.outputReads.delete(read), () => this.outputReads.delete(read));
|
|
27746
|
+
return read;
|
|
27747
|
+
}
|
|
27672
27748
|
async open() {
|
|
27673
27749
|
// Seed the first paint via the same random-access path used for every
|
|
27674
27750
|
// seek. Cheaper than a one-shot iterator just for the seed. Seed at the
|
|
@@ -27697,9 +27773,13 @@
|
|
|
27697
27773
|
async seekToFrame(frame) {
|
|
27698
27774
|
if (this.closed)
|
|
27699
27775
|
return null;
|
|
27700
|
-
const
|
|
27701
|
-
|
|
27776
|
+
const outputGeneration = this.outputGeneration;
|
|
27777
|
+
const decoded = await this.trackOutputRead(this.provider.getFrame(this.trackInfo.timeline.timeAt(frame.index)));
|
|
27778
|
+
if (this.closed || outputGeneration !== this.outputGeneration || !decoded) {
|
|
27779
|
+
if (decoded?.kind === "sample")
|
|
27780
|
+
decoded.sample.close();
|
|
27702
27781
|
return null;
|
|
27782
|
+
}
|
|
27703
27783
|
this.emitFrame(decoded);
|
|
27704
27784
|
return this.lastEmittedFrame;
|
|
27705
27785
|
}
|
|
@@ -27727,11 +27807,13 @@
|
|
|
27727
27807
|
attachPlay(startS) {
|
|
27728
27808
|
if (this.closed)
|
|
27729
27809
|
return;
|
|
27730
|
-
|
|
27810
|
+
if (this.iterator)
|
|
27811
|
+
void this.trackOutputRead(this.iterator.return());
|
|
27731
27812
|
this.iterator = this.provider.frames(startS);
|
|
27732
27813
|
}
|
|
27733
27814
|
detachPlay() {
|
|
27734
|
-
|
|
27815
|
+
if (this.iterator)
|
|
27816
|
+
void this.trackOutputRead(this.iterator.return());
|
|
27735
27817
|
this.iterator = null;
|
|
27736
27818
|
}
|
|
27737
27819
|
next() {
|
|
@@ -27776,6 +27858,7 @@
|
|
|
27776
27858
|
}
|
|
27777
27859
|
async close() {
|
|
27778
27860
|
this.closed = true;
|
|
27861
|
+
this.outputReads.clear();
|
|
27779
27862
|
this.pendingSeekTargetS = null;
|
|
27780
27863
|
this.currentState = ScrubCursorState.Closed;
|
|
27781
27864
|
this.listeners.clear();
|
|
@@ -27809,11 +27892,14 @@
|
|
|
27809
27892
|
async runSeek(t, keyOnly) {
|
|
27810
27893
|
if (this.closed)
|
|
27811
27894
|
return;
|
|
27895
|
+
const outputGeneration = this.outputGeneration;
|
|
27812
27896
|
this.currentState = ScrubCursorState.Seeking;
|
|
27813
|
-
const frame = await this.provider.getFrame(t);
|
|
27814
|
-
if (this.closed)
|
|
27815
|
-
|
|
27816
|
-
|
|
27897
|
+
const frame = await this.trackOutputRead(this.provider.getFrame(t));
|
|
27898
|
+
if (this.closed || outputGeneration !== this.outputGeneration) {
|
|
27899
|
+
if (frame?.kind === "sample")
|
|
27900
|
+
frame.sample.close();
|
|
27901
|
+
}
|
|
27902
|
+
else if (frame)
|
|
27817
27903
|
this.emitFrame(frame, keyOnly);
|
|
27818
27904
|
if (!this.closed)
|
|
27819
27905
|
this.currentState = ScrubCursorState.Idle;
|
|
@@ -27826,7 +27912,7 @@
|
|
|
27826
27912
|
// otherwise we would race the iterator state.
|
|
27827
27913
|
if (this.seekDraining)
|
|
27828
27914
|
return;
|
|
27829
|
-
const next = (await iterator.next()).value ?? null;
|
|
27915
|
+
const next = (await this.trackOutputRead(iterator.next())).value ?? null;
|
|
27830
27916
|
if (this.closed || iterator !== this.iterator || !next) {
|
|
27831
27917
|
if (next?.kind === "sample")
|
|
27832
27918
|
next.sample.close();
|
|
@@ -27910,6 +27996,7 @@
|
|
|
27910
27996
|
FrameTier["Preview"] = "preview";
|
|
27911
27997
|
})(FrameTier || (FrameTier = {}));
|
|
27912
27998
|
class FrameCache {
|
|
27999
|
+
options;
|
|
27913
28000
|
exact;
|
|
27914
28001
|
preview;
|
|
27915
28002
|
exactHits = 0;
|
|
@@ -27918,6 +28005,7 @@
|
|
|
27918
28005
|
bucketMs;
|
|
27919
28006
|
exactBudgetBytes;
|
|
27920
28007
|
constructor(options) {
|
|
28008
|
+
this.options = options;
|
|
27921
28009
|
const exactWidth = Math.max(1, Math.round(options.exactWidth));
|
|
27922
28010
|
const exactHeight = Math.max(1, Math.round(options.exactHeight));
|
|
27923
28011
|
const aspect = exactWidth / exactHeight;
|
|
@@ -27937,6 +28025,13 @@
|
|
|
27937
28025
|
this.exact = new TierStore(exactCapacity, exactWidth, exactHeight, FRAME_IDENTITY_KEY_GRID_MS);
|
|
27938
28026
|
this.preview = new TierStore(Math.max(0, Math.floor(options.previewCapacity)), previewWidth, previewHeight, PREVIEW_KEY_GRID_MS);
|
|
27939
28027
|
}
|
|
28028
|
+
resized(width, height) {
|
|
28029
|
+
return new FrameCache({
|
|
28030
|
+
...this.options,
|
|
28031
|
+
exactWidth: width,
|
|
28032
|
+
exactHeight: height,
|
|
28033
|
+
});
|
|
28034
|
+
}
|
|
27940
28035
|
/**
|
|
27941
28036
|
* Store a crisp full-resolution frame for the exact (scrub) tier under
|
|
27942
28037
|
* `frame`, the timeline's name for it. `timestampMs` is the decoded
|
|
@@ -28663,6 +28758,9 @@
|
|
|
28663
28758
|
cache;
|
|
28664
28759
|
keyframeIndex;
|
|
28665
28760
|
trackInfo;
|
|
28761
|
+
outputGeneration = 0;
|
|
28762
|
+
resizingOutput = false;
|
|
28763
|
+
outputReads = new Set();
|
|
28666
28764
|
exactTolMs;
|
|
28667
28765
|
previewTolMs;
|
|
28668
28766
|
now;
|
|
@@ -28777,6 +28875,47 @@
|
|
|
28777
28875
|
this.pendingSeekTargetS === null &&
|
|
28778
28876
|
!this.seekDraining);
|
|
28779
28877
|
}
|
|
28878
|
+
async resizeOutput({ width, height }) {
|
|
28879
|
+
if (!this.provider.resizeOutput) {
|
|
28880
|
+
throw new WebVideoEngineError(WebVideoEngineErrorCode.PresentationMismatch, "this decode source cannot resize frame output in place");
|
|
28881
|
+
}
|
|
28882
|
+
if (this.closed ||
|
|
28883
|
+
(width === this.trackInfo.decodeWidth &&
|
|
28884
|
+
height === this.trackInfo.decodeHeight))
|
|
28885
|
+
return false;
|
|
28886
|
+
this.outputGeneration++;
|
|
28887
|
+
this.resizingOutput = true;
|
|
28888
|
+
this.pendingSeekTargetS = null;
|
|
28889
|
+
const returning = this.iterator?.return();
|
|
28890
|
+
this.iterator = null;
|
|
28891
|
+
this.nextPending = 0;
|
|
28892
|
+
this.abandonPrefetch();
|
|
28893
|
+
this.lastEmittedFrame = null;
|
|
28894
|
+
try {
|
|
28895
|
+
await this.prefetchTask?.catch(() => undefined);
|
|
28896
|
+
await Promise.allSettled([
|
|
28897
|
+
...this.outputReads,
|
|
28898
|
+
...(returning ? [returning] : []),
|
|
28899
|
+
]);
|
|
28900
|
+
if (this.closed)
|
|
28901
|
+
return false;
|
|
28902
|
+
await this.provider.resizeOutput({ width, height });
|
|
28903
|
+
if (this.closed)
|
|
28904
|
+
return false;
|
|
28905
|
+
this.trackInfo = {
|
|
28906
|
+
...this.trackInfo,
|
|
28907
|
+
decodeWidth: width,
|
|
28908
|
+
decodeHeight: height,
|
|
28909
|
+
};
|
|
28910
|
+
const oldCache = this.cache;
|
|
28911
|
+
this.cache = oldCache.resized(width, height);
|
|
28912
|
+
oldCache.clear();
|
|
28913
|
+
return true;
|
|
28914
|
+
}
|
|
28915
|
+
finally {
|
|
28916
|
+
this.resizingOutput = false;
|
|
28917
|
+
}
|
|
28918
|
+
}
|
|
28780
28919
|
async open() {
|
|
28781
28920
|
// Seed at the track's real first timestamp, not a hardcoded 0: a trimmed
|
|
28782
28921
|
// or offset clip's first frame is not at the origin, so getFrame(0) would
|
|
@@ -28996,7 +29135,9 @@
|
|
|
28996
29135
|
this.enterMode(AccessMode.Playing);
|
|
28997
29136
|
// Signal any running sweep to bail; the first pull awaits its teardown.
|
|
28998
29137
|
this.abandonPrefetch();
|
|
28999
|
-
|
|
29138
|
+
if (this.iterator) {
|
|
29139
|
+
void this.trackOutputRead(this.iterator.return());
|
|
29140
|
+
}
|
|
29000
29141
|
this.iterator = this.provider.frames(startS);
|
|
29001
29142
|
// Reset the in-flight latch for the new session. An old-iterator pull may
|
|
29002
29143
|
// still be settling; its finally is iterator-identity-aware (see next()),
|
|
@@ -29008,7 +29149,9 @@
|
|
|
29008
29149
|
this.nextInFlight = Promise.resolve();
|
|
29009
29150
|
}
|
|
29010
29151
|
detachPlay() {
|
|
29011
|
-
|
|
29152
|
+
if (this.iterator) {
|
|
29153
|
+
void this.trackOutputRead(this.iterator.return());
|
|
29154
|
+
}
|
|
29012
29155
|
this.iterator = null;
|
|
29013
29156
|
// A pull in flight at detach never decrements, since its finally only
|
|
29014
29157
|
// fires for the iterator it was issued against. Left stranded, the count
|
|
@@ -29056,8 +29199,9 @@
|
|
|
29056
29199
|
return null;
|
|
29057
29200
|
this.enterMode(AccessMode.Stepping);
|
|
29058
29201
|
this.abandonPrefetch();
|
|
29202
|
+
const outputGeneration = this.outputGeneration;
|
|
29059
29203
|
const decoded = await this.withDecodeWatchdog(this.provider.getFrame(this.trackInfo.timeline.timeAt(frame.index)));
|
|
29060
|
-
if (this.closed || !decoded) {
|
|
29204
|
+
if (this.closed || outputGeneration !== this.outputGeneration || !decoded) {
|
|
29061
29205
|
if (decoded?.kind === "sample")
|
|
29062
29206
|
decoded.sample.close();
|
|
29063
29207
|
return null;
|
|
@@ -29090,6 +29234,7 @@
|
|
|
29090
29234
|
}
|
|
29091
29235
|
async close() {
|
|
29092
29236
|
this.closed = true;
|
|
29237
|
+
this.outputReads.clear();
|
|
29093
29238
|
this.abandonPrefetch();
|
|
29094
29239
|
this.pendingSeekTargetS = null;
|
|
29095
29240
|
this.currentState = ScrubCursorState.Closed;
|
|
@@ -29168,6 +29313,7 @@
|
|
|
29168
29313
|
}
|
|
29169
29314
|
}
|
|
29170
29315
|
async runExactSeek(t) {
|
|
29316
|
+
const outputGeneration = this.outputGeneration;
|
|
29171
29317
|
this.exactSeeks += 1;
|
|
29172
29318
|
const ms = t * 1000;
|
|
29173
29319
|
const cached = this.cache.getForFrame(this.frameAtTime(t), ms, this.previewTolMs);
|
|
@@ -29188,7 +29334,7 @@
|
|
|
29188
29334
|
const previewPaintedAt = painted && cached.tier === FrameTier.Preview ? this.now() : null;
|
|
29189
29335
|
const startedAt = this.now();
|
|
29190
29336
|
const frame = await this.withDecodeWatchdog(this.provider.getFrame(t));
|
|
29191
|
-
if (this.closed || !frame) {
|
|
29337
|
+
if (this.closed || outputGeneration !== this.outputGeneration || !frame) {
|
|
29192
29338
|
if (frame?.kind === "sample")
|
|
29193
29339
|
frame.sample.close();
|
|
29194
29340
|
return;
|
|
@@ -29255,13 +29401,14 @@
|
|
|
29255
29401
|
: null, targetMs);
|
|
29256
29402
|
}
|
|
29257
29403
|
async runKeySeek(t) {
|
|
29404
|
+
const outputGeneration = this.outputGeneration;
|
|
29258
29405
|
this.keySeeks += 1;
|
|
29259
29406
|
// getFrame already anchors on the enclosing keyframe, so re-anchoring the
|
|
29260
29407
|
// request collapses every target within a GOP onto that one frame. A
|
|
29261
29408
|
// source carrying a single keyframe answers its whole timeline with the
|
|
29262
29409
|
// first frame.
|
|
29263
29410
|
const frame = await this.withDecodeWatchdog(this.provider.getFrame(t));
|
|
29264
|
-
if (this.closed || !frame) {
|
|
29411
|
+
if (this.closed || outputGeneration !== this.outputGeneration || !frame) {
|
|
29265
29412
|
if (frame?.kind === "sample")
|
|
29266
29413
|
frame.sample.close();
|
|
29267
29414
|
return;
|
|
@@ -29319,7 +29466,12 @@
|
|
|
29319
29466
|
* decoder having a bad moment from one that will never answer again.
|
|
29320
29467
|
*/
|
|
29321
29468
|
withDecodeWatchdog(decode) {
|
|
29322
|
-
return this.watchdogged(decode, true);
|
|
29469
|
+
return this.watchdogged(this.trackOutputRead(decode), true);
|
|
29470
|
+
}
|
|
29471
|
+
trackOutputRead(read) {
|
|
29472
|
+
this.outputReads.add(read);
|
|
29473
|
+
void read.then(() => this.outputReads.delete(read), () => this.outputReads.delete(read));
|
|
29474
|
+
return read;
|
|
29323
29475
|
}
|
|
29324
29476
|
/**
|
|
29325
29477
|
* The same watchdog over a container metadata read. Deliberately unbooked:
|
|
@@ -29447,6 +29599,11 @@
|
|
|
29447
29599
|
return;
|
|
29448
29600
|
}
|
|
29449
29601
|
this.provider = openFrameProvider(handle);
|
|
29602
|
+
this.outputReads.clear();
|
|
29603
|
+
await this.provider.resizeOutput?.({
|
|
29604
|
+
width: this.trackInfo.decodeWidth,
|
|
29605
|
+
height: this.trackInfo.decodeHeight,
|
|
29606
|
+
});
|
|
29450
29607
|
// Disposing the old input invalidated the old probe, so the index
|
|
29451
29608
|
// has to be rebuilt against the new one. It starts empty, and
|
|
29452
29609
|
// nothing else re-walks it: the source's keyframes are exactly
|
|
@@ -29527,6 +29684,8 @@
|
|
|
29527
29684
|
* is never used twice at once, then runs under a captured generation token.
|
|
29528
29685
|
*/
|
|
29529
29686
|
schedulePrefetch(aroundS) {
|
|
29687
|
+
if (this.resizingOutput)
|
|
29688
|
+
return;
|
|
29530
29689
|
if (this.mode === AccessMode.Playing)
|
|
29531
29690
|
return;
|
|
29532
29691
|
this.abandonPrefetch();
|
|
@@ -29639,7 +29798,7 @@
|
|
|
29639
29798
|
}
|
|
29640
29799
|
}
|
|
29641
29800
|
finally {
|
|
29642
|
-
void iter.return();
|
|
29801
|
+
void this.trackOutputRead(iter.return());
|
|
29643
29802
|
}
|
|
29644
29803
|
}
|
|
29645
29804
|
/**
|
|
@@ -31373,6 +31532,17 @@ ${VERTEX_STAGE}
|
|
|
31373
31532
|
* crosses the duration.
|
|
31374
31533
|
*/
|
|
31375
31534
|
beginPlay(startS) {
|
|
31535
|
+
const { timeline } = this.deps.cursor.track;
|
|
31536
|
+
if (this.playSessionLive &&
|
|
31537
|
+
this.deps.clock.playing &&
|
|
31538
|
+
this.lastDeliveryS !== null &&
|
|
31539
|
+
!this.paintedSincePlay &&
|
|
31540
|
+
timeline.indexAtOrBefore(this.playAnchorS) ===
|
|
31541
|
+
timeline.indexAtOrBefore(startS)) {
|
|
31542
|
+
return;
|
|
31543
|
+
}
|
|
31544
|
+
if (this.lastDeliveryS !== null)
|
|
31545
|
+
this.endPlay();
|
|
31376
31546
|
this.endedFired = false;
|
|
31377
31547
|
this.playAnchorS = startS;
|
|
31378
31548
|
// The realtime needles are per-session rates, not lifetime totals: a
|
|
@@ -31401,6 +31571,7 @@ ${VERTEX_STAGE}
|
|
|
31401
31571
|
this.lastPresentTickS = null;
|
|
31402
31572
|
this.resetCadenceTracking();
|
|
31403
31573
|
this.clearPlayQueue();
|
|
31574
|
+
this.clearStash();
|
|
31404
31575
|
this.deps.cursor.attachPlay(startS);
|
|
31405
31576
|
this.deps.cursor.next();
|
|
31406
31577
|
}
|
|
@@ -31420,6 +31591,17 @@ ${VERTEX_STAGE}
|
|
|
31420
31591
|
this.resetCadenceTracking();
|
|
31421
31592
|
this.clearPlayQueue();
|
|
31422
31593
|
}
|
|
31594
|
+
holdOutput() {
|
|
31595
|
+
this.stop();
|
|
31596
|
+
this.clearStash();
|
|
31597
|
+
this.clearPlayQueue();
|
|
31598
|
+
this.deps.cursor.detachPlay();
|
|
31599
|
+
this.lastDeliveryS = null;
|
|
31600
|
+
}
|
|
31601
|
+
releaseOutput() {
|
|
31602
|
+
if (!this.disposed && this.sink)
|
|
31603
|
+
this.start();
|
|
31604
|
+
}
|
|
31423
31605
|
/**
|
|
31424
31606
|
* Cache lookup helper used by the engine on scrub. Asks the cursor for a
|
|
31425
31607
|
* cached frame near the target and, if it is far enough from what the canvas
|
|
@@ -31427,10 +31609,8 @@ ${VERTEX_STAGE}
|
|
|
31427
31609
|
* calls cursor.seekTo to land the full-res frame; the cursor's emit listener
|
|
31428
31610
|
* stashes it for the next tick.
|
|
31429
31611
|
*
|
|
31430
|
-
*
|
|
31431
|
-
*
|
|
31432
|
-
* consumers tune to control single-frame-step UX: a higher value forces a
|
|
31433
|
-
* fresh decode on tighter movements, a value of 0 always serves the cache.
|
|
31612
|
+
* Nearby approximate hits respect cacheSkipNearMs. An exact hit for the
|
|
31613
|
+
* requested frame can advance immediately, without waiting for a decode.
|
|
31434
31614
|
*/
|
|
31435
31615
|
tryPaintFromCache(timestampMs) {
|
|
31436
31616
|
if (!this.sink)
|
|
@@ -31440,7 +31620,12 @@ ${VERTEX_STAGE}
|
|
|
31440
31620
|
return false;
|
|
31441
31621
|
const hitMs = Math.round(hit.timestampS * 1000);
|
|
31442
31622
|
if (this.lastPaintedMs !== null) {
|
|
31443
|
-
|
|
31623
|
+
const { timeline } = this.deps.cursor.track;
|
|
31624
|
+
const exactTarget = hit.quality === "exact" &&
|
|
31625
|
+
timeline.indexOfDecoded(hit.timestampS) ===
|
|
31626
|
+
timeline.indexAtOrBefore(timestampMs / 1000);
|
|
31627
|
+
if (!exactTarget &&
|
|
31628
|
+
Math.abs(hitMs - this.lastPaintedMs) < this.deps.cacheSkipNearMs)
|
|
31444
31629
|
return false;
|
|
31445
31630
|
// And it has to be an improvement on what is showing, measured from
|
|
31446
31631
|
// where the user is pointing. The check above measures only distance
|
|
@@ -31456,10 +31641,30 @@ ${VERTEX_STAGE}
|
|
|
31456
31641
|
return false;
|
|
31457
31642
|
}
|
|
31458
31643
|
}
|
|
31644
|
+
return this.paintCached(hit);
|
|
31645
|
+
}
|
|
31646
|
+
/** A new commit needs its own presentation even when its exact pixels were
|
|
31647
|
+
* already shown by the preceding scrub. Approximate neighbors cannot answer it. */
|
|
31648
|
+
presentExactFromCache(timestampMs) {
|
|
31649
|
+
if (!this.sink)
|
|
31650
|
+
return false;
|
|
31651
|
+
const hit = this.deps.cursor.peekCached(timestampMs);
|
|
31652
|
+
const { timeline } = this.deps.cursor.track;
|
|
31653
|
+
if (!hit ||
|
|
31654
|
+
hit.quality !== "exact" ||
|
|
31655
|
+
timeline.indexOfDecoded(hit.timestampS) !==
|
|
31656
|
+
timeline.indexAtOrBefore(timestampMs / 1000)) {
|
|
31657
|
+
return false;
|
|
31658
|
+
}
|
|
31659
|
+
return this.paintCached(hit);
|
|
31660
|
+
}
|
|
31661
|
+
paintCached(hit) {
|
|
31662
|
+
if (!this.sink)
|
|
31663
|
+
return false;
|
|
31459
31664
|
// A cache hit is always a canvas blit, never a live sample, so there is
|
|
31460
31665
|
// no close obligation here.
|
|
31461
31666
|
const presented = this.sink.present(hit);
|
|
31462
|
-
this.lastPaintedMs =
|
|
31667
|
+
this.lastPaintedMs = Math.round(hit.timestampS * 1000);
|
|
31463
31668
|
this.lastPaintedQuality = hit.quality;
|
|
31464
31669
|
// This is a full frame on the display canvas and the user sees the
|
|
31465
31670
|
// picture change, so it is a paint. Reporting only decoded frames left
|
|
@@ -32340,6 +32545,45 @@ ${VERTEX_STAGE}
|
|
|
32340
32545
|
return residency;
|
|
32341
32546
|
}
|
|
32342
32547
|
|
|
32548
|
+
function createPausableDeadline(timeoutMs, onExpire) {
|
|
32549
|
+
let cancelled = false;
|
|
32550
|
+
let remainingMs = timeoutMs;
|
|
32551
|
+
let startedAtMs = performance.now();
|
|
32552
|
+
let timer = null;
|
|
32553
|
+
const expire = () => {
|
|
32554
|
+
if (cancelled)
|
|
32555
|
+
return;
|
|
32556
|
+
cancelled = true;
|
|
32557
|
+
timer = null;
|
|
32558
|
+
onExpire();
|
|
32559
|
+
};
|
|
32560
|
+
const arm = () => {
|
|
32561
|
+
if (cancelled || timer !== null)
|
|
32562
|
+
return;
|
|
32563
|
+
startedAtMs = performance.now();
|
|
32564
|
+
timer = setTimeout(expire, remainingMs);
|
|
32565
|
+
};
|
|
32566
|
+
arm();
|
|
32567
|
+
return {
|
|
32568
|
+
cancel() {
|
|
32569
|
+
if (cancelled)
|
|
32570
|
+
return;
|
|
32571
|
+
cancelled = true;
|
|
32572
|
+
if (timer !== null)
|
|
32573
|
+
clearTimeout(timer);
|
|
32574
|
+
timer = null;
|
|
32575
|
+
},
|
|
32576
|
+
pause() {
|
|
32577
|
+
if (cancelled || timer === null)
|
|
32578
|
+
return;
|
|
32579
|
+
remainingMs = Math.max(0, remainingMs - (performance.now() - startedAtMs));
|
|
32580
|
+
clearTimeout(timer);
|
|
32581
|
+
timer = null;
|
|
32582
|
+
},
|
|
32583
|
+
resume: arm,
|
|
32584
|
+
};
|
|
32585
|
+
}
|
|
32586
|
+
|
|
32343
32587
|
function serializeEngineError(error) {
|
|
32344
32588
|
return { code: error.code, message: error.message };
|
|
32345
32589
|
}
|
|
@@ -32359,6 +32603,8 @@ ${VERTEX_STAGE}
|
|
|
32359
32603
|
* the core never emits a competing time that would yank a settled playhead.
|
|
32360
32604
|
*/
|
|
32361
32605
|
class EngineCore {
|
|
32606
|
+
decodeStrategy = nativeResolution();
|
|
32607
|
+
displayResize = null;
|
|
32362
32608
|
emit;
|
|
32363
32609
|
emitDiagnostics;
|
|
32364
32610
|
emitPresentedFrame;
|
|
@@ -32417,6 +32663,8 @@ ${VERTEX_STAGE}
|
|
|
32417
32663
|
lastHostFrame = null;
|
|
32418
32664
|
/** Rejects an older refresh that completes after a newer one in one epoch. */
|
|
32419
32665
|
lastHostPaintSeq = 0;
|
|
32666
|
+
/** Main-thread visibility pauses only the wait for host presentation. */
|
|
32667
|
+
presentationVisible = true;
|
|
32420
32668
|
/** Mirrors WebVideoEngine's interactive-seek latch: true when a drag paused a
|
|
32421
32669
|
* playing engine, so endInteractiveSeek knows to resume. */
|
|
32422
32670
|
resumeAfterInteractiveSeek = false;
|
|
@@ -32457,6 +32705,7 @@ ${VERTEX_STAGE}
|
|
|
32457
32705
|
this.abandonAwaitedSeek();
|
|
32458
32706
|
}
|
|
32459
32707
|
async load(config) {
|
|
32708
|
+
this.decodeStrategy = config.decodeStrategy ?? nativeResolution();
|
|
32460
32709
|
this.beginPresentationGeneration();
|
|
32461
32710
|
this.presentation = config.presentation ?? "canvas";
|
|
32462
32711
|
this.displayBox =
|
|
@@ -32541,11 +32790,163 @@ ${VERTEX_STAGE}
|
|
|
32541
32790
|
this.residency?.startWarming();
|
|
32542
32791
|
return this.metadata;
|
|
32543
32792
|
}
|
|
32793
|
+
setDisplay(display) {
|
|
32794
|
+
const cursor = this.cursor;
|
|
32795
|
+
if (this.presentation !== "frames" ||
|
|
32796
|
+
this.decodeStrategy.kind !== "displayBox" ||
|
|
32797
|
+
!cursor?.resizeOutput) {
|
|
32798
|
+
return Promise.reject(new WebVideoEngineError(WebVideoEngineErrorCode.PresentationMismatch, "display resizing requires frames presentation with a displayBox strategy"));
|
|
32799
|
+
}
|
|
32800
|
+
const resolved = resolveDecodeDimensions(displayBoxResolution(display), {
|
|
32801
|
+
nativeWidth: cursor.track.width,
|
|
32802
|
+
nativeHeight: cursor.track.height,
|
|
32803
|
+
displayWidth: null,
|
|
32804
|
+
devicePixelRatio: display.devicePixelRatio,
|
|
32805
|
+
});
|
|
32806
|
+
if (!this.displayResize &&
|
|
32807
|
+
resolved.width === cursor.track.decodeWidth &&
|
|
32808
|
+
resolved.height === cursor.track.decodeHeight) {
|
|
32809
|
+
this.displayBox = {
|
|
32810
|
+
cssWidth: display.boxWidth,
|
|
32811
|
+
cssHeight: display.boxHeight,
|
|
32812
|
+
devicePixelRatio: display.devicePixelRatio,
|
|
32813
|
+
};
|
|
32814
|
+
return Promise.resolve(false);
|
|
32815
|
+
}
|
|
32816
|
+
let state = this.displayResize;
|
|
32817
|
+
if (!state) {
|
|
32818
|
+
let cancel = () => { };
|
|
32819
|
+
const aborted = new Promise((resolve) => {
|
|
32820
|
+
cancel = resolve;
|
|
32821
|
+
});
|
|
32822
|
+
state = {
|
|
32823
|
+
tail: Promise.resolve(false),
|
|
32824
|
+
request: 0,
|
|
32825
|
+
navigation: 0,
|
|
32826
|
+
pendingNavigation: false,
|
|
32827
|
+
held: false,
|
|
32828
|
+
cancelled: false,
|
|
32829
|
+
cancel,
|
|
32830
|
+
aborted,
|
|
32831
|
+
};
|
|
32832
|
+
this.displayResize = state;
|
|
32833
|
+
}
|
|
32834
|
+
const active = state;
|
|
32835
|
+
const request = ++active.request;
|
|
32836
|
+
const previous = active.tail;
|
|
32837
|
+
if (active.held)
|
|
32838
|
+
this.abandonAwaitedSeek();
|
|
32839
|
+
const run = (async () => {
|
|
32840
|
+
await previous.catch((error) => {
|
|
32841
|
+
if (active.cancelled || !isResizeSuperseded(error))
|
|
32842
|
+
throw error;
|
|
32843
|
+
});
|
|
32844
|
+
if (active.cancelled)
|
|
32845
|
+
throw displayResizeAborted();
|
|
32846
|
+
if (request !== active.request || active.pendingNavigation)
|
|
32847
|
+
throw displayResizeSuperseded();
|
|
32848
|
+
const dimensions = resolved;
|
|
32849
|
+
this.displayBox = {
|
|
32850
|
+
cssWidth: display.boxWidth,
|
|
32851
|
+
cssHeight: display.boxHeight,
|
|
32852
|
+
devicePixelRatio: display.devicePixelRatio,
|
|
32853
|
+
};
|
|
32854
|
+
if (dimensions.width === cursor.track.decodeWidth &&
|
|
32855
|
+
dimensions.height === cursor.track.decodeHeight)
|
|
32856
|
+
return false;
|
|
32857
|
+
const target = this.lastHostFrame ??
|
|
32858
|
+
this.timeline().landingAt(this.timeline().indexAtOrBefore(this.clock.now()));
|
|
32859
|
+
this.clock.pause();
|
|
32860
|
+
this.clock.seek(target.mediaTimeS);
|
|
32861
|
+
this.controller?.holdOutput();
|
|
32862
|
+
active.held = true;
|
|
32863
|
+
this.abandonAwaitedSeek();
|
|
32864
|
+
this.beginPresentationGeneration();
|
|
32865
|
+
const untilDisposed = (work) => Promise.race([
|
|
32866
|
+
work,
|
|
32867
|
+
active.aborted.then(() => {
|
|
32868
|
+
throw displayResizeAborted();
|
|
32869
|
+
}),
|
|
32870
|
+
]);
|
|
32871
|
+
await untilDisposed(cursor.resizeOutput(dimensions));
|
|
32872
|
+
if (request !== active.request || active.pendingNavigation)
|
|
32873
|
+
throw displayResizeSuperseded();
|
|
32874
|
+
const wait = this.awaitSeekLanding(target.frame, false, true);
|
|
32875
|
+
void wait.presented.catch(() => undefined);
|
|
32876
|
+
const decoded = await untilDisposed(cursor.seekToFrame(target.frame));
|
|
32877
|
+
if (request !== active.request || active.pendingNavigation) {
|
|
32878
|
+
this.controller?.holdOutput();
|
|
32879
|
+
this.abandonAwaitedSeek();
|
|
32880
|
+
throw displayResizeSuperseded();
|
|
32881
|
+
}
|
|
32882
|
+
if (!decoded)
|
|
32883
|
+
throw new WebVideoEngineError(WebVideoEngineErrorCode.DecoderStalled, "resized frame could not be decoded");
|
|
32884
|
+
this.controller?.releaseOutput();
|
|
32885
|
+
await untilDisposed(wait.presented);
|
|
32886
|
+
if (request !== active.request || active.pendingNavigation)
|
|
32887
|
+
throw displayResizeSuperseded();
|
|
32888
|
+
return true;
|
|
32889
|
+
})();
|
|
32890
|
+
const settled = run
|
|
32891
|
+
.catch((error) => {
|
|
32892
|
+
if (active.held) {
|
|
32893
|
+
this.abandonAwaitedSeek();
|
|
32894
|
+
this.controller?.holdOutput();
|
|
32895
|
+
}
|
|
32896
|
+
if (!isResizeSuperseded(error)) {
|
|
32897
|
+
this.playing = false;
|
|
32898
|
+
if (!active.cancelled)
|
|
32899
|
+
this.emitStatus(PlaybackStatus.Paused);
|
|
32900
|
+
}
|
|
32901
|
+
throw error;
|
|
32902
|
+
})
|
|
32903
|
+
.finally(() => {
|
|
32904
|
+
if (active.tail !== settled)
|
|
32905
|
+
return;
|
|
32906
|
+
this.displayResize = null;
|
|
32907
|
+
this.controller?.releaseOutput();
|
|
32908
|
+
if ((active.held || !this.clock.playing) &&
|
|
32909
|
+
!active.cancelled &&
|
|
32910
|
+
!active.pendingNavigation &&
|
|
32911
|
+
this.playing) {
|
|
32912
|
+
this.clock.play();
|
|
32913
|
+
this.controller?.beginPlay(this.clock.now());
|
|
32914
|
+
this.emitStatus(PlaybackStatus.Playing);
|
|
32915
|
+
}
|
|
32916
|
+
});
|
|
32917
|
+
active.tail = settled;
|
|
32918
|
+
return settled;
|
|
32919
|
+
}
|
|
32920
|
+
async afterDisplayResize(run, superseded) {
|
|
32921
|
+
const state = this.displayResize;
|
|
32922
|
+
const navigation = ++state.navigation;
|
|
32923
|
+
state.pendingNavigation = true;
|
|
32924
|
+
state.held = true;
|
|
32925
|
+
this.controller?.holdOutput();
|
|
32926
|
+
this.abandonAwaitedSeek();
|
|
32927
|
+
this.beginPresentationGeneration();
|
|
32928
|
+
while (this.displayResize === state)
|
|
32929
|
+
await state.tail.catch((error) => {
|
|
32930
|
+
if (state.cancelled || !isResizeSuperseded(error))
|
|
32931
|
+
throw error;
|
|
32932
|
+
});
|
|
32933
|
+
if (state.cancelled)
|
|
32934
|
+
throw displayResizeAborted();
|
|
32935
|
+
if (navigation !== state.navigation)
|
|
32936
|
+
return superseded;
|
|
32937
|
+
if (this.playing)
|
|
32938
|
+
this.clock.play();
|
|
32939
|
+
return run();
|
|
32940
|
+
}
|
|
32544
32941
|
play() {
|
|
32545
32942
|
if (!this.cursor || !this.controller)
|
|
32546
32943
|
return;
|
|
32547
32944
|
if (this.decodeFailure)
|
|
32548
32945
|
return this.republishFailure();
|
|
32946
|
+
if (this.displayResize) {
|
|
32947
|
+
this.playing = true;
|
|
32948
|
+
return;
|
|
32949
|
+
}
|
|
32549
32950
|
// Resume-from-end: a play after the clock crossed duration would tick
|
|
32550
32951
|
// straight to Ended. Snap back to 0 first.
|
|
32551
32952
|
const durS = this.durationMs / 1000;
|
|
@@ -32594,7 +32995,7 @@ ${VERTEX_STAGE}
|
|
|
32594
32995
|
}
|
|
32595
32996
|
}
|
|
32596
32997
|
this.controller?.endPlay();
|
|
32597
|
-
if (wasPlaying)
|
|
32998
|
+
if (wasPlaying && !this.displayResize)
|
|
32598
32999
|
this.abandonAwaitedSeek();
|
|
32599
33000
|
if (this.decodeFailure)
|
|
32600
33001
|
return this.republishFailure();
|
|
@@ -32635,6 +33036,17 @@ ${VERTEX_STAGE}
|
|
|
32635
33036
|
* thread snapped the gesture and wrote it. Paints a cache preview, then
|
|
32636
33037
|
* walks the cursor. */
|
|
32637
33038
|
scrub(frameIndex, intent = "gesture") {
|
|
33039
|
+
if (this.displayResize) {
|
|
33040
|
+
void this.afterDisplayResize(() => this.scrub(frameIndex, intent), undefined).catch((error) => {
|
|
33041
|
+
if (error instanceof WebVideoEngineError &&
|
|
33042
|
+
error.code === WebVideoEngineErrorCode.Aborted)
|
|
33043
|
+
return;
|
|
33044
|
+
this.handleTerminalFailure(error instanceof WebVideoEngineError
|
|
33045
|
+
? error
|
|
33046
|
+
: new WebVideoEngineError(WebVideoEngineErrorCode.BackendCrashed, "display resize failed", error));
|
|
33047
|
+
});
|
|
33048
|
+
return;
|
|
33049
|
+
}
|
|
32638
33050
|
if (!this.cursor)
|
|
32639
33051
|
return;
|
|
32640
33052
|
const target = this.timeline().landingAt(frameIndex);
|
|
@@ -32667,7 +33079,6 @@ ${VERTEX_STAGE}
|
|
|
32667
33079
|
// crisp frame up, and then the wait is over before the walk restarts.
|
|
32668
33080
|
if (!this.awaitedSeek)
|
|
32669
33081
|
this.closePlaySeekWait();
|
|
32670
|
-
this.controller?.endPlay();
|
|
32671
33082
|
this.controller?.beginPlay(tSec);
|
|
32672
33083
|
}
|
|
32673
33084
|
/** Closes the wait of a seek served by re-anchoring, at the crisp paint that
|
|
@@ -32689,6 +33100,8 @@ ${VERTEX_STAGE}
|
|
|
32689
33100
|
this.playSeekMaxMs = waitMs;
|
|
32690
33101
|
}
|
|
32691
33102
|
async commit(frameIndex) {
|
|
33103
|
+
if (this.displayResize)
|
|
33104
|
+
return this.afterDisplayResize(() => this.commit(frameIndex), null);
|
|
32692
33105
|
if (!this.cursor)
|
|
32693
33106
|
return null;
|
|
32694
33107
|
if (this.decodeFailure)
|
|
@@ -32698,7 +33111,9 @@ ${VERTEX_STAGE}
|
|
|
32698
33111
|
this.lastStepLanded = null;
|
|
32699
33112
|
const wait = this.awaitSeekLanding(target.frame, false, true);
|
|
32700
33113
|
this.clock.seek(tSec);
|
|
32701
|
-
this.controller?.
|
|
33114
|
+
if (!this.controller?.presentExactFromCache(tSec * 1000)) {
|
|
33115
|
+
this.controller?.tryPaintFromCache(tSec * 1000);
|
|
33116
|
+
}
|
|
32702
33117
|
// While playing, moving the playhead means re-anchoring the playback
|
|
32703
33118
|
// walk, exactly as scrub does. Seeking the cursor instead leaves the walk
|
|
32704
33119
|
// where it was, so playback carries on from the old position and the seek
|
|
@@ -32746,6 +33161,8 @@ ${VERTEX_STAGE}
|
|
|
32746
33161
|
return landed;
|
|
32747
33162
|
}
|
|
32748
33163
|
async seekToKey(timeMs) {
|
|
33164
|
+
if (this.displayResize)
|
|
33165
|
+
return this.afterDisplayResize(() => this.seekToKey(timeMs), null);
|
|
32749
33166
|
if (!this.cursor)
|
|
32750
33167
|
return null;
|
|
32751
33168
|
if (this.decodeFailure)
|
|
@@ -32809,6 +33226,14 @@ ${VERTEX_STAGE}
|
|
|
32809
33226
|
* other.
|
|
32810
33227
|
*/
|
|
32811
33228
|
async step(direction) {
|
|
33229
|
+
if (this.displayResize) {
|
|
33230
|
+
return this.afterDisplayResize(async () => {
|
|
33231
|
+
const landed = await this.step(direction);
|
|
33232
|
+
if (this.playing)
|
|
33233
|
+
this.controller?.beginPlay(this.clock.now());
|
|
33234
|
+
return landed;
|
|
33235
|
+
}, null);
|
|
33236
|
+
}
|
|
32812
33237
|
if (!this.cursor || !this.controller)
|
|
32813
33238
|
return null;
|
|
32814
33239
|
const timeline = this.timeline();
|
|
@@ -32887,6 +33312,10 @@ ${VERTEX_STAGE}
|
|
|
32887
33312
|
this.play();
|
|
32888
33313
|
}
|
|
32889
33314
|
async dispose() {
|
|
33315
|
+
if (this.displayResize) {
|
|
33316
|
+
this.displayResize.cancelled = true;
|
|
33317
|
+
this.displayResize.cancel();
|
|
33318
|
+
}
|
|
32890
33319
|
this.playing = false;
|
|
32891
33320
|
this.diagnosticsStop();
|
|
32892
33321
|
// A capture still running is evidence someone asked for; keep it, marked
|
|
@@ -32957,7 +33386,7 @@ ${VERTEX_STAGE}
|
|
|
32957
33386
|
return { generation, presented: Promise.resolve(null) };
|
|
32958
33387
|
}
|
|
32959
33388
|
const presented = new Promise((resolve, reject) => {
|
|
32960
|
-
const
|
|
33389
|
+
const presentationDeadline = createPausableDeadline(HANG_RECOVERY.PRESENTATION_LATCH_TIMEOUT_MS, () => {
|
|
32961
33390
|
const awaited = this.awaitedSeek;
|
|
32962
33391
|
if (!awaited ||
|
|
32963
33392
|
awaited.generation !== generation ||
|
|
@@ -32970,20 +33399,33 @@ ${VERTEX_STAGE}
|
|
|
32970
33399
|
awaited.reject(error);
|
|
32971
33400
|
else
|
|
32972
33401
|
awaited.resolve(null);
|
|
32973
|
-
}
|
|
33402
|
+
});
|
|
32974
33403
|
const awaited = {
|
|
32975
33404
|
generation,
|
|
32976
33405
|
keyOnly,
|
|
32977
|
-
|
|
33406
|
+
presentationDeadline,
|
|
32978
33407
|
reject: (error) => reject(error),
|
|
32979
33408
|
rejectOnFailure,
|
|
32980
33409
|
resolve,
|
|
32981
33410
|
target,
|
|
32982
33411
|
};
|
|
32983
33412
|
this.awaitedSeek = awaited;
|
|
33413
|
+
if (!this.presentationVisible)
|
|
33414
|
+
presentationDeadline.pause();
|
|
32984
33415
|
});
|
|
32985
33416
|
return { generation, presented };
|
|
32986
33417
|
}
|
|
33418
|
+
/** Main-thread document visibility; decoding/watchdogs remain active. */
|
|
33419
|
+
setPresentationVisibility(visible) {
|
|
33420
|
+
this.presentationVisible = visible;
|
|
33421
|
+
const awaited = this.awaitedSeek;
|
|
33422
|
+
if (!awaited)
|
|
33423
|
+
return;
|
|
33424
|
+
if (visible)
|
|
33425
|
+
awaited.presentationDeadline.resume();
|
|
33426
|
+
else
|
|
33427
|
+
awaited.presentationDeadline.pause();
|
|
33428
|
+
}
|
|
32987
33429
|
isCurrentSeek(generation) {
|
|
32988
33430
|
return generation === this.seekGeneration;
|
|
32989
33431
|
}
|
|
@@ -32997,7 +33439,7 @@ ${VERTEX_STAGE}
|
|
|
32997
33439
|
if (!awaited)
|
|
32998
33440
|
return;
|
|
32999
33441
|
this.awaitedSeek = null;
|
|
33000
|
-
|
|
33442
|
+
awaited.presentationDeadline.cancel();
|
|
33001
33443
|
awaited.resolve(null);
|
|
33002
33444
|
}
|
|
33003
33445
|
/** Ends an impossible landing. Awaitable movement rejects with the engine's
|
|
@@ -33007,7 +33449,7 @@ ${VERTEX_STAGE}
|
|
|
33007
33449
|
if (!awaited)
|
|
33008
33450
|
return;
|
|
33009
33451
|
this.awaitedSeek = null;
|
|
33010
|
-
|
|
33452
|
+
awaited.presentationDeadline.cancel();
|
|
33011
33453
|
if (awaited.rejectOnFailure)
|
|
33012
33454
|
awaited.reject(error);
|
|
33013
33455
|
else
|
|
@@ -33104,7 +33546,7 @@ ${VERTEX_STAGE}
|
|
|
33104
33546
|
if (answeredSeek && this.awaitedSeek === answeredSeek) {
|
|
33105
33547
|
this.seekLanded = frameId;
|
|
33106
33548
|
this.awaitedSeek = null;
|
|
33107
|
-
|
|
33549
|
+
answeredSeek.presentationDeadline.cancel();
|
|
33108
33550
|
this.closePlaySeekWait();
|
|
33109
33551
|
// Paused paints normally never move the playhead, but the landing
|
|
33110
33552
|
// of an awaited seek is the one paint that says where the request
|
|
@@ -33486,6 +33928,17 @@ ${VERTEX_STAGE}
|
|
|
33486
33928
|
const wanted = Math.round((windowMs / 1000) * DIAGNOSTICS.BROADCAST_HZ);
|
|
33487
33929
|
return Math.min(DIAGNOSTICS.TRACE_SNAPSHOT_CAP, Math.max(1, wanted));
|
|
33488
33930
|
}
|
|
33931
|
+
function displayResizeAborted() {
|
|
33932
|
+
return new WebVideoEngineError(WebVideoEngineErrorCode.Aborted, "display resize aborted by disposal");
|
|
33933
|
+
}
|
|
33934
|
+
function displayResizeSuperseded() {
|
|
33935
|
+
return new WebVideoEngineError(WebVideoEngineErrorCode.Aborted, "display resize superseded");
|
|
33936
|
+
}
|
|
33937
|
+
function isResizeSuperseded(error) {
|
|
33938
|
+
return (error instanceof WebVideoEngineError &&
|
|
33939
|
+
error.code === WebVideoEngineErrorCode.Aborted &&
|
|
33940
|
+
error.message === "display resize superseded");
|
|
33941
|
+
}
|
|
33489
33942
|
|
|
33490
33943
|
function toSerializedError(error) {
|
|
33491
33944
|
if (error instanceof WebVideoEngineError)
|
|
@@ -33511,6 +33964,9 @@ ${VERTEX_STAGE}
|
|
|
33511
33964
|
case "unbindCanvas":
|
|
33512
33965
|
engine.setCanvas(null);
|
|
33513
33966
|
return;
|
|
33967
|
+
case "setPresentationVisibility":
|
|
33968
|
+
engine.setPresentationVisibility(command.visible);
|
|
33969
|
+
return;
|
|
33514
33970
|
case "scrub":
|
|
33515
33971
|
engine.scrub(command.frameIndex, command.intent);
|
|
33516
33972
|
return;
|
|
@@ -33552,6 +34008,11 @@ ${VERTEX_STAGE}
|
|
|
33552
34008
|
engine.play();
|
|
33553
34009
|
return { type: "ack", requestId: command.requestId };
|
|
33554
34010
|
});
|
|
34011
|
+
case "setDisplay":
|
|
34012
|
+
return settle(post, command.requestId, async () => {
|
|
34013
|
+
const outputChanged = await engine.setDisplay(command.display);
|
|
34014
|
+
return { type: "ack", requestId: command.requestId, outputChanged };
|
|
34015
|
+
});
|
|
33555
34016
|
case "commit":
|
|
33556
34017
|
return settle(post, command.requestId, async () => {
|
|
33557
34018
|
const landing = await engine.commit(command.frameIndex);
|