akarisub 1.1.2 → 1.1.4
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/README.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/COPYRIGHT +21 -22
- package/dist/ts/akarisub.d.ts +20 -40
- package/dist/ts/akarisub.d.ts.map +1 -1
- package/dist/ts/akarisub.js +291 -545
- package/dist/ts/akarisub.js.map +1 -1
- package/dist/ts/bitmap-settlements.d.ts +14 -0
- package/dist/ts/bitmap-settlements.d.ts.map +1 -0
- package/dist/ts/bitmap-settlements.js +23 -0
- package/dist/ts/bitmap-settlements.js.map +1 -0
- package/dist/ts/color-space.d.ts +1 -0
- package/dist/ts/color-space.d.ts.map +1 -1
- package/dist/ts/color-space.js +10 -1
- package/dist/ts/color-space.js.map +1 -1
- package/dist/ts/cue-events.d.ts +2 -0
- package/dist/ts/cue-events.d.ts.map +1 -1
- package/dist/ts/cue-events.js +2 -0
- package/dist/ts/cue-events.js.map +1 -1
- package/dist/ts/font-reload.d.ts +8 -0
- package/dist/ts/font-reload.d.ts.map +1 -0
- package/dist/ts/font-reload.js +21 -0
- package/dist/ts/font-reload.js.map +1 -0
- package/dist/ts/gpu-recovery.d.ts +50 -0
- package/dist/ts/gpu-recovery.d.ts.map +1 -0
- package/dist/ts/gpu-recovery.js +158 -0
- package/dist/ts/gpu-recovery.js.map +1 -0
- package/dist/ts/prepared-presentation.d.ts +55 -0
- package/dist/ts/prepared-presentation.d.ts.map +1 -0
- package/dist/ts/prepared-presentation.js +366 -0
- package/dist/ts/prepared-presentation.js.map +1 -0
- package/dist/ts/timing.d.ts.map +1 -1
- package/dist/ts/timing.js +31 -17
- package/dist/ts/timing.js.map +1 -1
- package/dist/ts/track-lifecycle.d.ts +47 -0
- package/dist/ts/track-lifecycle.d.ts.map +1 -0
- package/dist/ts/track-lifecycle.js +187 -0
- package/dist/ts/track-lifecycle.js.map +1 -0
- package/dist/ts/types.d.ts +6 -0
- package/dist/ts/types.d.ts.map +1 -1
- package/dist/ts/utils.d.ts.map +1 -1
- package/dist/ts/utils.js +3 -1
- package/dist/ts/utils.js.map +1 -1
- package/dist/ts/worker.js +150 -261
- package/dist/ts/worker.js.map +1 -1
- package/package.json +5 -5
- package/pkg/akarisub-mt.wasm +0 -0
- package/pkg/akarisub.wasm +0 -0
package/dist/ts/akarisub.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GPURecovery } from './gpu-recovery';
|
|
2
|
+
import { PreparedPresentation } from './prepared-presentation';
|
|
3
|
+
import { classifyPerformanceWarnings, parsePreloadTrackSource, resolveCueTracking } from './cue-events';
|
|
2
4
|
import { parseStreamingTrackOptions } from './streaming';
|
|
3
5
|
import { normalizeFontFamilySource } from './font-subsets';
|
|
4
6
|
import { computeCanvasSize, getVideoPosition, fixAlpha } from './utils';
|
|
@@ -6,7 +8,7 @@ import { canvas2dContextSettings, createSubtitleImageData, defaultVideoColorProf
|
|
|
6
8
|
import { selectWasmBinary } from './wasm-capabilities';
|
|
7
9
|
import { WebGPURenderer, isWebGPUSupported } from './webgpu-renderer';
|
|
8
10
|
import { WebGL2Renderer, isWebGL2Supported } from './webgl2-renderer';
|
|
9
|
-
import { compensatedMediaTime,
|
|
11
|
+
import { compensatedMediaTime, estimateRefreshIntervalMs, isStalePresentation, normalizeFrameTimeline, predictFrameDisplayTimeMs, presentedFrameIndex, presentationLeadSeconds, resolvePresentationMediaTime, selectRenderMediaTime, subtitleTimeForFrame, updateTimingCompensation } from './timing';
|
|
10
12
|
import { isVideoFrameLike, videoFrameCallbackMetadata } from './video-frame';
|
|
11
13
|
import { getDefaultFontUrl, getMtWasmGlueUrl, getMtWasmUrl, getWasmGlueUrl, getWasmUrl } from './wasm';
|
|
12
14
|
import { webYCbCrMap } from './utils';
|
|
@@ -45,7 +47,6 @@ const isLikelyWebKit = () => {
|
|
|
45
47
|
export default class AkariSub extends EventTarget {
|
|
46
48
|
static MAX_PENDING_DEMANDS = 3;
|
|
47
49
|
static MAX_FONT_BYTES = 32 * 1024 * 1024;
|
|
48
|
-
static WEBGL_RESTORE_TIMEOUT_MS = 3000;
|
|
49
50
|
static _hasAlphaBug = null;
|
|
50
51
|
static _hasBitmapBug = null;
|
|
51
52
|
_loaded;
|
|
@@ -53,6 +54,7 @@ export default class AkariSub extends EventTarget {
|
|
|
53
54
|
_destroyedSignal;
|
|
54
55
|
_resolveDestroyed;
|
|
55
56
|
_pendingWorkerRejectors = new Set();
|
|
57
|
+
_pendingTrackReadyWaiters = new Map();
|
|
56
58
|
_onDemandRender;
|
|
57
59
|
_offscreenRender;
|
|
58
60
|
_video;
|
|
@@ -85,12 +87,17 @@ export default class AkariSub extends EventTarget {
|
|
|
85
87
|
_demandTimings = new Map();
|
|
86
88
|
_adaptiveTiming;
|
|
87
89
|
_frameTimeline;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
_presentationOwner;
|
|
91
|
+
get _presentation() {
|
|
92
|
+
return this._presentationOwner ??= new PreparedPresentation({
|
|
93
|
+
canvas: () => this._canvas,
|
|
94
|
+
release: stage => this._releaseGPUStage(stage),
|
|
95
|
+
committed: frame => this._gpuRecovery.commit(this._snapshotRenderedCanvas(frame.stage, frame.gpuStage === true)),
|
|
96
|
+
refreshInterval: () => estimateRefreshIntervalMs(this._refreshSamples ?? []),
|
|
97
|
+
now: () => performance.now(),
|
|
98
|
+
documentTime: () => Number(document.timeline?.currentTime)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
94
101
|
_predictedDisplayTimes = new Map();
|
|
95
102
|
_displayClockOffsets = [];
|
|
96
103
|
_displayGridAnchorMs;
|
|
@@ -102,8 +109,10 @@ export default class AkariSub extends EventTarget {
|
|
|
102
109
|
_lastRefreshRafTime;
|
|
103
110
|
_prepareQueue = [];
|
|
104
111
|
_prepareRequests = new Map();
|
|
112
|
+
_lastRvfcMediaTime = null;
|
|
105
113
|
_nextPrepareId = 1;
|
|
106
114
|
_prepareForce = true;
|
|
115
|
+
_prepareFailureEpoch = -1;
|
|
107
116
|
_timingCompensationSeconds = 0;
|
|
108
117
|
_rvfcHandle = null;
|
|
109
118
|
_rvfcGeneration = 0;
|
|
@@ -120,14 +129,46 @@ export default class AkariSub extends EventTarget {
|
|
|
120
129
|
_boundHandleRVFC;
|
|
121
130
|
_gpuRenderer = null;
|
|
122
131
|
_rendererType = 'canvas2d';
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
_gpuRecoveryOwner;
|
|
133
|
+
get _gpuRecovery() {
|
|
134
|
+
return this._gpuRecoveryOwner ??= new GPURecovery({
|
|
135
|
+
current: () => this._gpuRenderer,
|
|
136
|
+
use: renderer => { this._gpuRenderer = renderer; },
|
|
137
|
+
begin: reason => {
|
|
138
|
+
this._emitPerformanceWarning({ kind: 'renderer-recovery', reason, rendererType: this._rendererType });
|
|
139
|
+
this._renderEpoch++;
|
|
140
|
+
this._pendingDemandTimes.length = 0;
|
|
141
|
+
this._demandTimings.clear();
|
|
142
|
+
this._clearPreparedFrames(false);
|
|
143
|
+
this._prepareForce = true;
|
|
144
|
+
this._showGPURecoveryFrame();
|
|
145
|
+
},
|
|
146
|
+
replay: async (renderer) => {
|
|
147
|
+
this._applyGpuColorManagement();
|
|
148
|
+
await this._restoreRetainedGPUFrame(renderer);
|
|
149
|
+
},
|
|
150
|
+
recovered: reason => {
|
|
151
|
+
this._activateBaseCanvas(this._currentExactFrameIndex());
|
|
152
|
+
this._setRendererType(this._rendererType, reason);
|
|
153
|
+
this._hideGPURecoveryFrame();
|
|
154
|
+
this._prepareForce = true;
|
|
155
|
+
this._syncVideoClock();
|
|
156
|
+
},
|
|
157
|
+
fallback: (reason, error) => {
|
|
158
|
+
console.warn(`[AkariSub] GPU recovery failed after ${reason}; falling back to Canvas2D.`, error);
|
|
159
|
+
this._adoptGPURecoveryCanvas();
|
|
160
|
+
this._setRendererType('canvas2d', reason);
|
|
161
|
+
void this.sendMessage('setAsyncRender', { value: false });
|
|
162
|
+
this._onCanvasFallback?.();
|
|
163
|
+
this._prepareForce = true;
|
|
164
|
+
this._syncVideoClock();
|
|
165
|
+
},
|
|
166
|
+
discard: snapshot => this._discardGPUSnapshot(snapshot)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
126
169
|
_recoveryCanvas = null;
|
|
127
170
|
_recoveryCtx = null;
|
|
128
|
-
_lastGPUFrame = null;
|
|
129
171
|
_gpuSnapshotPool = [];
|
|
130
|
-
_nextGPUSnapshotSequence = 1;
|
|
131
172
|
_onCanvasFallback;
|
|
132
173
|
_onCueEnter;
|
|
133
174
|
_onCueExit;
|
|
@@ -139,8 +180,7 @@ export default class AkariSub extends EventTarget {
|
|
|
139
180
|
_lastRenderWidth = 0;
|
|
140
181
|
_lastRenderHeight = 0;
|
|
141
182
|
_gpuBitmapImages = [];
|
|
142
|
-
|
|
143
|
-
timeOffset;
|
|
183
|
+
_timeOffset;
|
|
144
184
|
/** When true, the worker prints libass and renderer logs. */
|
|
145
185
|
debug;
|
|
146
186
|
/** Extra scale applied to the subtitle canvas before height limits. */
|
|
@@ -218,7 +258,7 @@ export default class AkariSub extends EventTarget {
|
|
|
218
258
|
// ASS composition remains opt-in because it regresses several ordinary and
|
|
219
259
|
// animation-heavy custom-canvas workloads despite helping dense overlays.
|
|
220
260
|
this._offscreenRender = canTransferOffscreen && !canUseGPURenderer && wantsOffscreenRender;
|
|
221
|
-
this.
|
|
261
|
+
this._timeOffset = options.timeOffset || 0;
|
|
222
262
|
this._video = options.video;
|
|
223
263
|
this._canvas = options.canvas;
|
|
224
264
|
if (this._video && !this._canvas) {
|
|
@@ -313,6 +353,7 @@ export default class AkariSub extends EventTarget {
|
|
|
313
353
|
blockingFullTrackWarmup: options.blockingFullTrackWarmup ?? false,
|
|
314
354
|
fullTrackWarmupStep: options.fullTrackWarmupStep ?? 1,
|
|
315
355
|
adaptiveBlendLayouts: options.adaptiveBlendLayouts ?? false,
|
|
356
|
+
cueTracking: resolveCueTracking(options.cueTracking),
|
|
316
357
|
rawAssImageGpu,
|
|
317
358
|
onDemandRender: this._onDemandRender,
|
|
318
359
|
initialTime,
|
|
@@ -502,125 +543,33 @@ export default class AkariSub extends EventTarget {
|
|
|
502
543
|
};
|
|
503
544
|
return;
|
|
504
545
|
}
|
|
505
|
-
renderer.onContextLost = () => this.
|
|
546
|
+
renderer.onContextLost = () => this._gpuRecovery.waitForRestore(renderer);
|
|
506
547
|
renderer.onContextRestored = (error) => {
|
|
507
548
|
if (error) {
|
|
508
|
-
this.
|
|
549
|
+
this._gpuRecovery.fail(renderer, error);
|
|
509
550
|
}
|
|
510
551
|
else {
|
|
511
|
-
void this.
|
|
552
|
+
void this._gpuRecovery.restore(renderer);
|
|
512
553
|
}
|
|
513
554
|
};
|
|
514
555
|
}
|
|
515
556
|
/** @internal */
|
|
516
|
-
_beginGPURecovery(renderer, reason) {
|
|
517
|
-
if (this._destroyed || this._gpuRenderer !== renderer || this._gpuRecovering)
|
|
518
|
-
return false;
|
|
519
|
-
this._gpuRecovering = renderer;
|
|
520
|
-
this._gpuRecoveryGeneration++;
|
|
521
|
-
this._emitPerformanceWarning({ kind: 'renderer-recovery', reason, rendererType: this._rendererType });
|
|
522
|
-
// GPU-backed prepared canvases cannot survive either kind of loss. Retire
|
|
523
|
-
// them without touching the retained CPU frame used by the recovery layer.
|
|
524
|
-
this._renderEpoch++;
|
|
525
|
-
this._pendingDemandTimes.length = 0;
|
|
526
|
-
this._demandTimings.clear();
|
|
527
|
-
this._clearPreparedFrames(false);
|
|
528
|
-
this._prepareForce = true;
|
|
529
|
-
this._showGPURecoveryFrame();
|
|
530
|
-
return true;
|
|
531
|
-
}
|
|
532
|
-
/** @internal */
|
|
533
557
|
async _recoverWebGPU(renderer) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const generation = this._gpuRecoveryGeneration;
|
|
537
|
-
let replacement = null;
|
|
538
|
-
try {
|
|
539
|
-
renderer.destroy();
|
|
540
|
-
replacement = new WebGPURenderer();
|
|
558
|
+
await this._gpuRecovery.replace(renderer, () => {
|
|
559
|
+
const replacement = new WebGPURenderer();
|
|
541
560
|
this._bindGPURecovery(replacement);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
561
|
+
return replacement;
|
|
562
|
+
}, async (replacement, isCurrent) => {
|
|
563
|
+
const gpu = replacement;
|
|
564
|
+
await gpu.init();
|
|
565
|
+
if (!isCurrent())
|
|
545
566
|
return;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
567
|
+
if (!this._canvas)
|
|
568
|
+
throw new Error('Subtitle canvas is unavailable during GPU recovery');
|
|
569
|
+
await gpu.setCanvas(this._canvas, Math.max(1, this._canvas.width || this._lastRenderWidth || 1), Math.max(1, this._canvas.height || this._lastRenderHeight || 1));
|
|
570
|
+
if (!gpu.initialized)
|
|
549
571
|
throw new Error('Replacement WebGPU device was lost during initialization');
|
|
550
|
-
|
|
551
|
-
replacement.destroy();
|
|
552
|
-
return;
|
|
553
|
-
}
|
|
554
|
-
this._gpuRenderer = replacement;
|
|
555
|
-
await this._finishGPURecovery(replacement, 'device-lost');
|
|
556
|
-
}
|
|
557
|
-
catch (error) {
|
|
558
|
-
if (replacement && this._gpuRenderer !== replacement)
|
|
559
|
-
replacement.destroy();
|
|
560
|
-
this._fallbackFromGPU(renderer, 'device-lost', error);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
/** @internal */
|
|
564
|
-
_beginWebGLRecovery(renderer) {
|
|
565
|
-
if (!this._beginGPURecovery(renderer, 'context-lost'))
|
|
566
|
-
return;
|
|
567
|
-
const generation = this._gpuRecoveryGeneration;
|
|
568
|
-
this._gpuRecoveryTimer = setTimeout(() => {
|
|
569
|
-
if (generation === this._gpuRecoveryGeneration) {
|
|
570
|
-
this._fallbackFromGPU(renderer, 'context-lost', new Error('Timed out waiting for WebGL2 context restoration'));
|
|
571
|
-
}
|
|
572
|
-
}, AkariSub.WEBGL_RESTORE_TIMEOUT_MS);
|
|
573
|
-
}
|
|
574
|
-
/** @internal */
|
|
575
|
-
async _finishGPURecovery(renderer, reason) {
|
|
576
|
-
if (this._destroyed || this._gpuRenderer !== renderer || this._gpuRecovering == null)
|
|
577
|
-
return;
|
|
578
|
-
try {
|
|
579
|
-
this._clearGPURecoveryTimer();
|
|
580
|
-
this._applyGpuColorManagement();
|
|
581
|
-
await this._restoreRetainedGPUFrame(renderer);
|
|
582
|
-
if (this._destroyed || this._gpuRenderer !== renderer)
|
|
583
|
-
return;
|
|
584
|
-
this._gpuRecovering = null;
|
|
585
|
-
this._setRendererType(this._rendererType, reason);
|
|
586
|
-
this._hideGPURecoveryFrame();
|
|
587
|
-
this._prepareForce = true;
|
|
588
|
-
this._syncVideoClock();
|
|
589
|
-
}
|
|
590
|
-
catch (error) {
|
|
591
|
-
this._fallbackFromGPU(renderer, reason, error);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
/** @internal */
|
|
595
|
-
_fallbackFromGPU(renderer, reason, error) {
|
|
596
|
-
if (this._destroyed || this._gpuRecovering == null)
|
|
597
|
-
return;
|
|
598
|
-
if (this._gpuRenderer !== renderer && this._gpuRecovering !== renderer)
|
|
599
|
-
return;
|
|
600
|
-
console.warn(`[AkariSub] GPU recovery failed after ${reason}; falling back to Canvas2D.`, error);
|
|
601
|
-
this._clearGPURecoveryTimer();
|
|
602
|
-
try {
|
|
603
|
-
renderer.destroy();
|
|
604
|
-
}
|
|
605
|
-
catch {
|
|
606
|
-
// The failed device/context may already reject resource cleanup.
|
|
607
|
-
}
|
|
608
|
-
this._gpuRenderer = null;
|
|
609
|
-
this._gpuRecovering = null;
|
|
610
|
-
this._gpuRecoveryGeneration++;
|
|
611
|
-
this._adoptGPURecoveryCanvas();
|
|
612
|
-
this._setRendererType('canvas2d', reason);
|
|
613
|
-
void this.sendMessage('setAsyncRender', { value: false });
|
|
614
|
-
this._onCanvasFallback?.();
|
|
615
|
-
this._prepareForce = true;
|
|
616
|
-
this._syncVideoClock();
|
|
617
|
-
}
|
|
618
|
-
/** @internal */
|
|
619
|
-
_clearGPURecoveryTimer() {
|
|
620
|
-
if (this._gpuRecoveryTimer == null)
|
|
621
|
-
return;
|
|
622
|
-
clearTimeout(this._gpuRecoveryTimer);
|
|
623
|
-
this._gpuRecoveryTimer = null;
|
|
572
|
+
});
|
|
624
573
|
}
|
|
625
574
|
/** Active compositor: WebGPU, WebGL2, or Canvas2D. */
|
|
626
575
|
get rendererType() {
|
|
@@ -666,7 +615,7 @@ export default class AkariSub extends EventTarget {
|
|
|
666
615
|
_snapshotRenderImages(images, width, height) {
|
|
667
616
|
if (!this._gpuSnapshotPool)
|
|
668
617
|
return null;
|
|
669
|
-
const sequence = this.
|
|
618
|
+
const sequence = this._gpuRecovery.sequence();
|
|
670
619
|
const canRetainRaw = images.every((image) => image.image instanceof ArrayBuffer ||
|
|
671
620
|
image.image instanceof Uint8Array ||
|
|
672
621
|
image.image instanceof Uint8ClampedArray);
|
|
@@ -733,7 +682,7 @@ export default class AkariSub extends EventTarget {
|
|
|
733
682
|
context.filter = 'none';
|
|
734
683
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
735
684
|
context.drawImage(source, 0, 0);
|
|
736
|
-
return { canvas, sequence: this.
|
|
685
|
+
return { canvas, sequence: this._gpuRecovery.sequence(), colorManaged };
|
|
737
686
|
}
|
|
738
687
|
catch {
|
|
739
688
|
this._gpuSnapshotPool.push(canvas);
|
|
@@ -742,54 +691,19 @@ export default class AkariSub extends EventTarget {
|
|
|
742
691
|
}
|
|
743
692
|
/** @internal */
|
|
744
693
|
_discardGPUSnapshot(snapshot) {
|
|
745
|
-
if (snapshot && 'canvas' in snapshot)
|
|
694
|
+
if (!this._destroyed && snapshot && 'canvas' in snapshot)
|
|
746
695
|
this._gpuSnapshotPool.push(snapshot.canvas);
|
|
747
696
|
}
|
|
748
697
|
/** @internal */
|
|
749
|
-
_commitGPUSnapshot(snapshot) {
|
|
750
|
-
if (!snapshot)
|
|
751
|
-
return;
|
|
752
|
-
if (this._destroyed || (this._lastGPUFrame && snapshot.sequence < this._lastGPUFrame.sequence)) {
|
|
753
|
-
this._discardGPUSnapshot(snapshot);
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
const previous = this._lastGPUFrame;
|
|
757
|
-
this._lastGPUFrame = snapshot;
|
|
758
|
-
if (previous && 'canvas' in previous)
|
|
759
|
-
this._gpuSnapshotPool.push(previous.canvas);
|
|
760
|
-
}
|
|
761
|
-
/** @internal */
|
|
762
698
|
_materializeGPUSnapshot(snapshot) {
|
|
763
699
|
if (!snapshot || 'canvas' in snapshot)
|
|
764
700
|
return snapshot;
|
|
765
701
|
const materialized = this._rasterizeGPUFrame(snapshot.images, snapshot.width, snapshot.height, snapshot.sequence);
|
|
766
|
-
if (materialized
|
|
767
|
-
this.
|
|
702
|
+
if (materialized)
|
|
703
|
+
this._gpuRecovery.materialize(snapshot, materialized);
|
|
768
704
|
return materialized;
|
|
769
705
|
}
|
|
770
706
|
/** @internal */
|
|
771
|
-
_retainGPUFrameAfter(completion, snapshot) {
|
|
772
|
-
if (!snapshot)
|
|
773
|
-
return completion;
|
|
774
|
-
if (typeof completion === 'boolean') {
|
|
775
|
-
if (completion)
|
|
776
|
-
this._commitGPUSnapshot(snapshot);
|
|
777
|
-
else
|
|
778
|
-
this._discardGPUSnapshot(snapshot);
|
|
779
|
-
return completion;
|
|
780
|
-
}
|
|
781
|
-
return completion.then((painted) => {
|
|
782
|
-
if (painted)
|
|
783
|
-
this._commitGPUSnapshot(snapshot);
|
|
784
|
-
else
|
|
785
|
-
this._discardGPUSnapshot(snapshot);
|
|
786
|
-
return painted;
|
|
787
|
-
}, (error) => {
|
|
788
|
-
this._discardGPUSnapshot(snapshot);
|
|
789
|
-
throw error;
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
/** @internal */
|
|
793
707
|
_currentCanvasFilter() {
|
|
794
708
|
const target = this._videoColorProfile.matrix ?? this._videoColorSpace;
|
|
795
709
|
if (!this._lastSubtitleColorSpace || !target)
|
|
@@ -797,10 +711,18 @@ export default class AkariSub extends EventTarget {
|
|
|
797
711
|
return getColorSpaceFilterUrl(this._lastSubtitleColorSpace, target) ?? 'none';
|
|
798
712
|
}
|
|
799
713
|
/** @internal */
|
|
714
|
+
_requiresCanvasColorConversion() {
|
|
715
|
+
const target = this._videoColorProfile?.matrix ?? this._videoColorSpace;
|
|
716
|
+
return (!this._gpuRenderer &&
|
|
717
|
+
!!this._lastSubtitleColorSpace &&
|
|
718
|
+
!!target &&
|
|
719
|
+
this._lastSubtitleColorSpace !== target);
|
|
720
|
+
}
|
|
721
|
+
/** @internal */
|
|
800
722
|
_showGPURecoveryFrame() {
|
|
801
723
|
if (!this._canvasParent || !this._canvas)
|
|
802
724
|
return;
|
|
803
|
-
const snapshot = this._materializeGPUSnapshot(this.
|
|
725
|
+
const snapshot = this._materializeGPUSnapshot(this._gpuRecovery.snapshot);
|
|
804
726
|
const width = snapshot?.canvas.width || this._canvas.width || this._lastRenderWidth || 1;
|
|
805
727
|
const height = snapshot?.canvas.height || this._canvas.height || this._lastRenderHeight || 1;
|
|
806
728
|
const canvas = this._recoveryCanvas ?? document.createElement('canvas');
|
|
@@ -854,7 +776,7 @@ export default class AkariSub extends EventTarget {
|
|
|
854
776
|
}
|
|
855
777
|
/** @internal */
|
|
856
778
|
async _restoreRetainedGPUFrame(renderer) {
|
|
857
|
-
const snapshot = this.
|
|
779
|
+
const snapshot = this._gpuRecovery.snapshot;
|
|
858
780
|
let painted;
|
|
859
781
|
if (!snapshot) {
|
|
860
782
|
painted = renderer.clear();
|
|
@@ -885,7 +807,6 @@ export default class AkariSub extends EventTarget {
|
|
|
885
807
|
throw new Error('The recovered GPU renderer rejected the retained subtitle frame');
|
|
886
808
|
if (renderer instanceof WebGPURenderer)
|
|
887
809
|
await renderer.submittedWorkDone();
|
|
888
|
-
this._activateBaseCanvas(this._currentExactFrameIndex());
|
|
889
810
|
}
|
|
890
811
|
/** @internal */
|
|
891
812
|
_shouldUseHdr() {
|
|
@@ -1024,10 +945,18 @@ export default class AkariSub extends EventTarget {
|
|
|
1024
945
|
this._canvas.style.width = videoSize.width + 'px';
|
|
1025
946
|
this._canvas.style.height = videoSize.height + 'px';
|
|
1026
947
|
}
|
|
948
|
+
else if ((!width || !height) && this._videoFrameClock) {
|
|
949
|
+
const renderSize = computeCanvasSize(this._videoFrameClock.width, this._videoFrameClock.height, this.prescaleFactor, this.prescaleHeightLimit, this.maxRenderHeight);
|
|
950
|
+
width = renderSize.width;
|
|
951
|
+
height = renderSize.height;
|
|
952
|
+
}
|
|
1027
953
|
this._canvas.style.top = top + 'px';
|
|
1028
954
|
this._canvas.style.left = left + 'px';
|
|
1029
|
-
|
|
1030
|
-
|
|
955
|
+
this._presentation.layout(stage => this._syncStagedCanvasLayout(stage));
|
|
956
|
+
// Explicit resize calls can also supply fractional CSS/DPR dimensions.
|
|
957
|
+
// Keep canvas storage, GPU uploads and worker snapshots on the same size.
|
|
958
|
+
width = Math.max(0, Math.floor(width));
|
|
959
|
+
height = Math.max(0, Math.floor(height));
|
|
1031
960
|
if (width > 0 && height > 0 && (this._canvasctrl.width !== width || this._canvasctrl.height !== height)) {
|
|
1032
961
|
this._bumpRenderEpoch();
|
|
1033
962
|
this._canvasctrl.width = width;
|
|
@@ -1063,6 +992,7 @@ export default class AkariSub extends EventTarget {
|
|
|
1063
992
|
this._videoFrameClock = null;
|
|
1064
993
|
this._video = video;
|
|
1065
994
|
this._playstate = video.paused || video.ended;
|
|
995
|
+
this._lastRvfcMediaTime = null;
|
|
1066
996
|
if (this._onDemandRender) {
|
|
1067
997
|
if (!this._destroyed && this._video === video) {
|
|
1068
998
|
this._scheduleRVFC(video);
|
|
@@ -1153,6 +1083,7 @@ export default class AkariSub extends EventTarget {
|
|
|
1153
1083
|
}
|
|
1154
1084
|
/** Fetch and load an ASS/SSA track from `url`. */
|
|
1155
1085
|
setTrackByUrl(url) {
|
|
1086
|
+
this._supersedePendingTrackActivation();
|
|
1156
1087
|
this._bumpRenderEpoch();
|
|
1157
1088
|
this.sendMessage('setTrackByUrl', { url });
|
|
1158
1089
|
this._reAttachOffscreen();
|
|
@@ -1161,6 +1092,7 @@ export default class AkariSub extends EventTarget {
|
|
|
1161
1092
|
}
|
|
1162
1093
|
/** Replace the current track with ASS/SSA text or bytes. */
|
|
1163
1094
|
setTrack(content) {
|
|
1095
|
+
this._supersedePendingTrackActivation();
|
|
1164
1096
|
this._bumpRenderEpoch();
|
|
1165
1097
|
this.sendMessage('setTrack', { content }, AkariSub._getSubtitleTransfers(content));
|
|
1166
1098
|
this._reAttachOffscreen();
|
|
@@ -1173,6 +1105,7 @@ export default class AkariSub extends EventTarget {
|
|
|
1173
1105
|
* materialized in the main thread.
|
|
1174
1106
|
*/
|
|
1175
1107
|
setEncryptedTrack(content) {
|
|
1108
|
+
this._supersedePendingTrackActivation();
|
|
1176
1109
|
this._bumpRenderEpoch();
|
|
1177
1110
|
this.sendMessage('setEncryptedTrack', { content }, AkariSub._getSubtitleTransfers(undefined, content));
|
|
1178
1111
|
this._reAttachOffscreen();
|
|
@@ -1181,6 +1114,7 @@ export default class AkariSub extends EventTarget {
|
|
|
1181
1114
|
}
|
|
1182
1115
|
/** Unload the current track without destroying the renderer. */
|
|
1183
1116
|
freeTrack() {
|
|
1117
|
+
this._supersedePendingTrackActivation();
|
|
1184
1118
|
this._sendMutatingMessage('freeTrack');
|
|
1185
1119
|
}
|
|
1186
1120
|
/**
|
|
@@ -1191,6 +1125,7 @@ export default class AkariSub extends EventTarget {
|
|
|
1191
1125
|
*/
|
|
1192
1126
|
initStreamingTrack(options) {
|
|
1193
1127
|
const parsed = parseStreamingTrackOptions(options);
|
|
1128
|
+
this._supersedePendingTrackActivation();
|
|
1194
1129
|
this._bumpRenderEpoch();
|
|
1195
1130
|
this.sendMessage('initStreamingTrack', { options: parsed }, parsed.header ? AkariSub._getSubtitleTransfers(parsed.header) : undefined);
|
|
1196
1131
|
this._reAttachOffscreen();
|
|
@@ -1265,8 +1200,9 @@ export default class AkariSub extends EventTarget {
|
|
|
1265
1200
|
return { id: result.id };
|
|
1266
1201
|
}
|
|
1267
1202
|
/**
|
|
1268
|
-
* Replace the visible track with a previously preloaded one. The
|
|
1269
|
-
*
|
|
1203
|
+
* Replace the visible track with a previously preloaded one. The returned
|
|
1204
|
+
* promise waits for the matching readiness cycle; paused exact-timeline
|
|
1205
|
+
* renderers also finish their configured frame-prefetch runway.
|
|
1270
1206
|
*/
|
|
1271
1207
|
async activatePreloadedTrack(id = this._preloadedTrackId ?? -1) {
|
|
1272
1208
|
const ready = this._workerReady ||
|
|
@@ -1277,22 +1213,38 @@ export default class AkariSub extends EventTarget {
|
|
|
1277
1213
|
throw new Error('No preloaded track is ready');
|
|
1278
1214
|
this._bumpRenderEpoch();
|
|
1279
1215
|
const requestId = this._nextTrackRequestId++;
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1216
|
+
const readiness = this._waitForTrackReady(requestId);
|
|
1217
|
+
try {
|
|
1218
|
+
const result = await this._fetchFromWorker({
|
|
1219
|
+
target: 'activatePreloadedTrack',
|
|
1220
|
+
requestId,
|
|
1221
|
+
id,
|
|
1222
|
+
timeoutMs: null
|
|
1223
|
+
});
|
|
1224
|
+
if (!result.success || result.id == null) {
|
|
1225
|
+
throw new Error(result.error || 'The preloaded track could not be activated');
|
|
1226
|
+
}
|
|
1227
|
+
if (this._preloadedTrackId === result.id)
|
|
1228
|
+
this._preloadedTrackId = null;
|
|
1229
|
+
const requiresCanvasColorConversion = this._requiresCanvasColorConversion();
|
|
1230
|
+
const reattachedOffscreen = !requiresCanvasColorConversion && this._offscreenRender && !!this._ctx;
|
|
1231
|
+
if (!requiresCanvasColorConversion) {
|
|
1232
|
+
this._reAttachOffscreen();
|
|
1233
|
+
if (this._ctx)
|
|
1234
|
+
this._ctx.filter = 'none';
|
|
1235
|
+
}
|
|
1236
|
+
// The matching trackReady already synchronized the clock. Repeat it only
|
|
1237
|
+
// when reattachment replaced the canvas that received that first render.
|
|
1238
|
+
if (reattachedOffscreen)
|
|
1239
|
+
this._syncVideoClock();
|
|
1240
|
+
const readinessError = await readiness.promise;
|
|
1241
|
+
if (readinessError)
|
|
1242
|
+
throw readinessError;
|
|
1243
|
+
return { id: result.id };
|
|
1244
|
+
}
|
|
1245
|
+
finally {
|
|
1246
|
+
readiness.cancel();
|
|
1288
1247
|
}
|
|
1289
|
-
if (this._preloadedTrackId === result.id)
|
|
1290
|
-
this._preloadedTrackId = null;
|
|
1291
|
-
this._reAttachOffscreen();
|
|
1292
|
-
if (this._ctx)
|
|
1293
|
-
this._ctx.filter = 'none';
|
|
1294
|
-
this._syncVideoClock();
|
|
1295
|
-
return { id: result.id };
|
|
1296
1248
|
}
|
|
1297
1249
|
/** Tell the worker whether playback is paused. Ignored when a video element is attached. */
|
|
1298
1250
|
setIsPaused(isPaused) {
|
|
@@ -1464,6 +1416,22 @@ export default class AkariSub extends EventTarget {
|
|
|
1464
1416
|
this._primePreparedFrames(this._currentExactFrameMediaTime());
|
|
1465
1417
|
this._dispatchNextPreparation();
|
|
1466
1418
|
}
|
|
1419
|
+
/** Subtitle time offset in seconds. Changing it discards prepared frames rendered with the previous offset. */
|
|
1420
|
+
get timeOffset() {
|
|
1421
|
+
return this._timeOffset;
|
|
1422
|
+
}
|
|
1423
|
+
set timeOffset(value) {
|
|
1424
|
+
if (!Number.isFinite(value) || value === this._timeOffset)
|
|
1425
|
+
return;
|
|
1426
|
+
const previous = this._timeOffset;
|
|
1427
|
+
this._timeOffset = value;
|
|
1428
|
+
if (typeof previous !== 'number' || this._destroyed)
|
|
1429
|
+
return;
|
|
1430
|
+
this._bumpRenderEpoch();
|
|
1431
|
+
this._syncVideoClock();
|
|
1432
|
+
this._primePreparedFrames(this._currentExactFrameMediaTime());
|
|
1433
|
+
this._dispatchNextPreparation();
|
|
1434
|
+
}
|
|
1467
1435
|
/** Number of events in the loaded track. */
|
|
1468
1436
|
async getEventCount() {
|
|
1469
1437
|
const data = await this._fetchFromWorker({ target: 'getEventCount' });
|
|
@@ -1565,12 +1533,11 @@ export default class AkariSub extends EventTarget {
|
|
|
1565
1533
|
return true;
|
|
1566
1534
|
if (metadata.width !== this._videoWidth || metadata.height !== this._videoHeight)
|
|
1567
1535
|
return false;
|
|
1568
|
-
const prepared = this.
|
|
1536
|
+
const prepared = this._presentation.take(frameIndex);
|
|
1569
1537
|
if (!prepared)
|
|
1570
1538
|
return false;
|
|
1571
|
-
this._preparedFrames.delete(frameIndex);
|
|
1572
1539
|
if (prepared.width !== this._canvasctrl.width || prepared.height !== this._canvasctrl.height) {
|
|
1573
|
-
this.
|
|
1540
|
+
this._presentation.dispose(prepared);
|
|
1574
1541
|
this._prepareForce = true;
|
|
1575
1542
|
return false;
|
|
1576
1543
|
}
|
|
@@ -1588,7 +1555,7 @@ export default class AkariSub extends EventTarget {
|
|
|
1588
1555
|
}
|
|
1589
1556
|
const event = this._frameBufferReadyEvent;
|
|
1590
1557
|
this._frameBufferReadyEvent = null;
|
|
1591
|
-
this.dispatchEvent(new CustomEvent(event));
|
|
1558
|
+
this.dispatchEvent(new CustomEvent(event.type, event.requestId == null ? undefined : { detail: { requestId: event.requestId } }));
|
|
1592
1559
|
}
|
|
1593
1560
|
/** @internal */
|
|
1594
1561
|
_startRefreshSampling() {
|
|
@@ -1633,18 +1600,6 @@ export default class AkariSub extends EventTarget {
|
|
|
1633
1600
|
this._gpuRenderer?.releaseCanvas(stage);
|
|
1634
1601
|
}
|
|
1635
1602
|
/** @internal */
|
|
1636
|
-
_removeStagedCanvas(stage) {
|
|
1637
|
-
for (const animation of stage.getAnimations())
|
|
1638
|
-
animation.cancel();
|
|
1639
|
-
this._releaseGPUStage(stage);
|
|
1640
|
-
stage.remove();
|
|
1641
|
-
if (this._committedStage === stage)
|
|
1642
|
-
this._committedStage = null;
|
|
1643
|
-
this._stagedCanvases.delete(stage);
|
|
1644
|
-
this._stageFrameIndices.delete(stage);
|
|
1645
|
-
this._stageDisplayTimes.delete(stage);
|
|
1646
|
-
}
|
|
1647
|
-
/** @internal */
|
|
1648
1603
|
_currentExactFrameIndex() {
|
|
1649
1604
|
if (!this._frameTimeline)
|
|
1650
1605
|
return undefined;
|
|
@@ -1665,49 +1620,7 @@ export default class AkariSub extends EventTarget {
|
|
|
1665
1620
|
}
|
|
1666
1621
|
/** Make a freshly painted base canvas visible without discarding future prefetch. */
|
|
1667
1622
|
_activateBaseCanvas(presentedIndex) {
|
|
1668
|
-
|
|
1669
|
-
return;
|
|
1670
|
-
// A compositor-scheduled frame can become visible before its RVFC arrives.
|
|
1671
|
-
// Never let an older demand response roll that already-visible frame back.
|
|
1672
|
-
if (presentedIndex != null) {
|
|
1673
|
-
const now = performance.now();
|
|
1674
|
-
let visibleIndex = this._committedStage ? this._stageFrameIndices.get(this._committedStage) : undefined;
|
|
1675
|
-
for (const stage of this._stagedCanvases) {
|
|
1676
|
-
const boundary = this._stageDisplayTimes.get(stage);
|
|
1677
|
-
const index = this._stageFrameIndices.get(stage);
|
|
1678
|
-
if (boundary != null && boundary <= now && index != null && (visibleIndex == null || index > visibleIndex)) {
|
|
1679
|
-
visibleIndex = index;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
if (visibleIndex != null && visibleIndex > presentedIndex)
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
for (const animation of this._canvas.getAnimations())
|
|
1686
|
-
animation.cancel();
|
|
1687
|
-
this._canvas.style.opacity = '1';
|
|
1688
|
-
const retainedStages = new Set();
|
|
1689
|
-
for (const [index, frame] of [...this._preparedFrames]) {
|
|
1690
|
-
const frameIndex = frame.index ?? index;
|
|
1691
|
-
if (presentedIndex != null && frameIndex > presentedIndex && frame.stage) {
|
|
1692
|
-
for (const animation of frame.animations ?? [])
|
|
1693
|
-
animation.cancel();
|
|
1694
|
-
frame.animations = undefined;
|
|
1695
|
-
frame.scheduled = false;
|
|
1696
|
-
frame.committed = false;
|
|
1697
|
-
frame.stage.style.opacity = '0';
|
|
1698
|
-
retainedStages.add(frame.stage);
|
|
1699
|
-
continue;
|
|
1700
|
-
}
|
|
1701
|
-
this._disposePreparedFrame(frame);
|
|
1702
|
-
this._preparedFrames.delete(index);
|
|
1703
|
-
}
|
|
1704
|
-
this._scheduledPreparedFrame = null;
|
|
1705
|
-
for (const stage of [...this._stagedCanvases]) {
|
|
1706
|
-
if (!retainedStages.has(stage))
|
|
1707
|
-
this._removeStagedCanvas(stage);
|
|
1708
|
-
}
|
|
1709
|
-
this._committedStage = null;
|
|
1710
|
-
this._scheduleNextPreparedFrame();
|
|
1623
|
+
this._presentation.activateBase(presentedIndex);
|
|
1711
1624
|
}
|
|
1712
1625
|
/** @internal */
|
|
1713
1626
|
_activateBaseCanvasAfterGPUWork(renderEpoch = this._renderEpoch, presentedIndex) {
|
|
@@ -1734,22 +1647,6 @@ export default class AkariSub extends EventTarget {
|
|
|
1734
1647
|
});
|
|
1735
1648
|
}
|
|
1736
1649
|
/** @internal */
|
|
1737
|
-
_disposePreparedFrame(frame) {
|
|
1738
|
-
if (this._scheduledPreparedFrame === frame)
|
|
1739
|
-
this._scheduledPreparedFrame = null;
|
|
1740
|
-
for (const animation of frame.animations ?? [])
|
|
1741
|
-
animation.cancel();
|
|
1742
|
-
frame.animations = undefined;
|
|
1743
|
-
frame.scheduled = false;
|
|
1744
|
-
frame.bitmap?.close();
|
|
1745
|
-
frame.bitmap = undefined;
|
|
1746
|
-
frame.gpuStage = false;
|
|
1747
|
-
if (frame.stage) {
|
|
1748
|
-
this._removeStagedCanvas(frame.stage);
|
|
1749
|
-
frame.stage = undefined;
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
/** @internal */
|
|
1753
1650
|
_stagePreparedFrame(index, frame, allowWebGPU = true) {
|
|
1754
1651
|
if (!this._canvasParent || !frame.bitmap || this._destroyed)
|
|
1755
1652
|
return;
|
|
@@ -1761,8 +1658,7 @@ export default class AkariSub extends EventTarget {
|
|
|
1761
1658
|
frame.index = index;
|
|
1762
1659
|
frame.stage = stage;
|
|
1763
1660
|
frame.ready = false;
|
|
1764
|
-
this.
|
|
1765
|
-
this._stageFrameIndices.set(stage, index);
|
|
1661
|
+
this._presentation.register(stage, index);
|
|
1766
1662
|
let rendered = false;
|
|
1767
1663
|
const webgpuRenderer = allowWebGPU && this._rendererType === 'webgpu' && this._gpuRenderer instanceof WebGPURenderer
|
|
1768
1664
|
? this._gpuRenderer
|
|
@@ -1786,18 +1682,17 @@ export default class AkariSub extends EventTarget {
|
|
|
1786
1682
|
// desynchronized:true can expose a newly appended but not-yet-painted layer.
|
|
1787
1683
|
const context = stage.getContext('2d', this._canvas2dSettings());
|
|
1788
1684
|
if (!context) {
|
|
1789
|
-
this.
|
|
1685
|
+
this._presentation.remove(stage);
|
|
1790
1686
|
stage = document.createElement('canvas');
|
|
1791
1687
|
stage.width = frame.width;
|
|
1792
1688
|
stage.height = frame.height;
|
|
1793
1689
|
this._syncStagedCanvasLayout(stage);
|
|
1794
1690
|
stage.style.opacity = '0';
|
|
1795
1691
|
frame.stage = stage;
|
|
1796
|
-
this.
|
|
1797
|
-
this._stageFrameIndices.set(stage, index);
|
|
1692
|
+
this._presentation.register(stage, index);
|
|
1798
1693
|
const fallbackContext = stage.getContext('2d', this._canvas2dSettings());
|
|
1799
1694
|
if (!fallbackContext) {
|
|
1800
|
-
this.
|
|
1695
|
+
this._presentation.remove(stage);
|
|
1801
1696
|
frame.stage = undefined;
|
|
1802
1697
|
return;
|
|
1803
1698
|
}
|
|
@@ -1824,157 +1719,7 @@ export default class AkariSub extends EventTarget {
|
|
|
1824
1719
|
// promise is pending, rejecting it and starving prefetch forever.
|
|
1825
1720
|
frame.ready = true;
|
|
1826
1721
|
}
|
|
1827
|
-
this.
|
|
1828
|
-
}
|
|
1829
|
-
/** @internal */
|
|
1830
|
-
_scheduleNextPreparedFrame() {
|
|
1831
|
-
if (this._scheduledPreparedFrame || this._destroyed)
|
|
1832
|
-
return;
|
|
1833
|
-
let next;
|
|
1834
|
-
let nextTime = Number.POSITIVE_INFINITY;
|
|
1835
|
-
const now = performance.now();
|
|
1836
|
-
for (const frame of this._preparedFrames.values()) {
|
|
1837
|
-
const target = frame.targetDisplayTime;
|
|
1838
|
-
if (!frame.stage ||
|
|
1839
|
-
frame.committed ||
|
|
1840
|
-
frame.scheduled ||
|
|
1841
|
-
!Number.isFinite(target) ||
|
|
1842
|
-
target <= now ||
|
|
1843
|
-
target >= nextTime) {
|
|
1844
|
-
continue;
|
|
1845
|
-
}
|
|
1846
|
-
next = frame;
|
|
1847
|
-
nextTime = target;
|
|
1848
|
-
}
|
|
1849
|
-
if (next?.ready)
|
|
1850
|
-
this._schedulePreparedFrame(next, nextTime);
|
|
1851
|
-
}
|
|
1852
|
-
/** @internal */
|
|
1853
|
-
_commitPreparedStage(frame) {
|
|
1854
|
-
const stage = frame.stage;
|
|
1855
|
-
if (!stage || !this._stagedCanvases.has(stage) || this._destroyed)
|
|
1856
|
-
return;
|
|
1857
|
-
if (!frame.committed) {
|
|
1858
|
-
this._commitGPUSnapshot(this._snapshotRenderedCanvas(stage, frame.gpuStage === true));
|
|
1859
|
-
}
|
|
1860
|
-
for (const animation of frame.animations ?? [])
|
|
1861
|
-
animation.cancel();
|
|
1862
|
-
frame.animations = undefined;
|
|
1863
|
-
frame.scheduled = false;
|
|
1864
|
-
frame.committed = true;
|
|
1865
|
-
if (this._scheduledPreparedFrame === frame)
|
|
1866
|
-
this._scheduledPreparedFrame = null;
|
|
1867
|
-
for (const animation of this._canvas.getAnimations())
|
|
1868
|
-
animation.cancel();
|
|
1869
|
-
this._canvas.style.opacity = '0';
|
|
1870
|
-
stage.style.opacity = '1';
|
|
1871
|
-
this._committedStage = stage;
|
|
1872
|
-
const frameIndex = frame.index ?? this._stageFrameIndices.get(stage);
|
|
1873
|
-
for (const candidate of [...this._stagedCanvases]) {
|
|
1874
|
-
if (candidate === stage)
|
|
1875
|
-
continue;
|
|
1876
|
-
for (const animation of candidate.getAnimations())
|
|
1877
|
-
animation.cancel();
|
|
1878
|
-
const candidateIndex = this._stageFrameIndices.get(candidate);
|
|
1879
|
-
const shouldRetire = frame.replaceAll || frameIndex == null || candidateIndex == null || candidateIndex < frameIndex;
|
|
1880
|
-
if (shouldRetire) {
|
|
1881
|
-
this._removeStagedCanvas(candidate);
|
|
1882
|
-
}
|
|
1883
|
-
else {
|
|
1884
|
-
candidate.style.opacity = '0';
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
this._scheduleNextPreparedFrame();
|
|
1888
|
-
}
|
|
1889
|
-
/** @internal */
|
|
1890
|
-
_schedulePreparedFrame(frame, targetDisplayTime) {
|
|
1891
|
-
const stage = frame.stage;
|
|
1892
|
-
frame.targetDisplayTime = targetDisplayTime;
|
|
1893
|
-
if (!stage ||
|
|
1894
|
-
!frame.ready ||
|
|
1895
|
-
frame.scheduled ||
|
|
1896
|
-
frame.committed ||
|
|
1897
|
-
this._scheduledPreparedFrame ||
|
|
1898
|
-
this._destroyed ||
|
|
1899
|
-
targetDisplayTime <= performance.now()) {
|
|
1900
|
-
return;
|
|
1901
|
-
}
|
|
1902
|
-
// Each swap independently hides every other layer. Do not form a single
|
|
1903
|
-
// predecessor chain: removing one skipped prefetched frame would otherwise
|
|
1904
|
-
// cancel the only animation capable of hiding its predecessor. Future and
|
|
1905
|
-
// unscheduled stages are hidden too, but retained for their own later show.
|
|
1906
|
-
const previousStages = [...this._stagedCanvases].filter((candidate) => candidate !== stage);
|
|
1907
|
-
this._stageDisplayTimes.set(stage, targetDisplayTime);
|
|
1908
|
-
const performanceTime = performance.now();
|
|
1909
|
-
const compositorSwapTime = targetDisplayTime - compositorScheduleLeadMs(estimateRefreshIntervalMs(this._refreshSamples ?? []));
|
|
1910
|
-
const documentTime = Number(document.timeline?.currentTime);
|
|
1911
|
-
const hasDocumentTime = Number.isFinite(documentTime);
|
|
1912
|
-
const sharedStartTime = hasDocumentTime ? documentTime + (compositorSwapTime - performanceTime) : undefined;
|
|
1913
|
-
const animationOptions = {
|
|
1914
|
-
delay: hasDocumentTime ? 0 : Math.max(0, compositorSwapTime - performanceTime),
|
|
1915
|
-
// A near-zero positive interval keeps the swap compositor-scheduled while
|
|
1916
|
-
// allowing its cleanup promise to run in the same refresh. A full 1 ms
|
|
1917
|
-
// interval can survive until the next paint when animation composite order
|
|
1918
|
-
// temporarily favors an older stage.
|
|
1919
|
-
duration: 0.001,
|
|
1920
|
-
easing: 'steps(1, jump-start)',
|
|
1921
|
-
fill: 'forwards'
|
|
1922
|
-
};
|
|
1923
|
-
const hiddenLayers = [this._canvas, ...previousStages];
|
|
1924
|
-
let showAnimation = null;
|
|
1925
|
-
const hideAnimations = [];
|
|
1926
|
-
try {
|
|
1927
|
-
showAnimation = stage.animate([{ opacity: '0' }, { opacity: '1' }], animationOptions);
|
|
1928
|
-
for (const layer of hiddenLayers) {
|
|
1929
|
-
hideAnimations.push(layer.animate([{ opacity: '1' }, { opacity: '0' }], animationOptions));
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
catch {
|
|
1933
|
-
showAnimation?.cancel();
|
|
1934
|
-
for (const animation of hideAnimations)
|
|
1935
|
-
animation.cancel();
|
|
1936
|
-
return;
|
|
1937
|
-
}
|
|
1938
|
-
if (!showAnimation)
|
|
1939
|
-
return;
|
|
1940
|
-
if (sharedStartTime != null) {
|
|
1941
|
-
showAnimation.startTime = sharedStartTime;
|
|
1942
|
-
for (const animation of hideAnimations)
|
|
1943
|
-
animation.startTime = sharedStartTime;
|
|
1944
|
-
}
|
|
1945
|
-
frame.scheduled = true;
|
|
1946
|
-
this._scheduledPreparedFrame = frame;
|
|
1947
|
-
const swapAnimations = [showAnimation, ...hideAnimations];
|
|
1948
|
-
frame.animations = swapAnimations;
|
|
1949
|
-
const releaseSwapAnimations = () => {
|
|
1950
|
-
for (const animation of swapAnimations)
|
|
1951
|
-
animation.cancel();
|
|
1952
|
-
if (frame.animations === swapAnimations)
|
|
1953
|
-
frame.animations = undefined;
|
|
1954
|
-
};
|
|
1955
|
-
// The show animation is the authoritative commit. Once it completes, make
|
|
1956
|
-
// its state explicit, release this swap's fill animations, and remove all
|
|
1957
|
-
// older layers. A later prefetched swap may already have its own independent
|
|
1958
|
-
// hide animation on this stage; it is intentionally left untouched.
|
|
1959
|
-
void showAnimation.finished.then(() => {
|
|
1960
|
-
if (frame.animations !== swapAnimations)
|
|
1961
|
-
return;
|
|
1962
|
-
if (!frame.committed)
|
|
1963
|
-
this._commitPreparedStage(frame);
|
|
1964
|
-
releaseSwapAnimations();
|
|
1965
|
-
if (this._scheduledPreparedFrame === frame)
|
|
1966
|
-
this._scheduledPreparedFrame = null;
|
|
1967
|
-
this._scheduleNextPreparedFrame();
|
|
1968
|
-
}, () => {
|
|
1969
|
-
if (frame.animations !== swapAnimations)
|
|
1970
|
-
return;
|
|
1971
|
-
releaseSwapAnimations();
|
|
1972
|
-
if (this._scheduledPreparedFrame === frame)
|
|
1973
|
-
this._scheduledPreparedFrame = null;
|
|
1974
|
-
frame.scheduled = false;
|
|
1975
|
-
if (!frame.committed)
|
|
1976
|
-
this._scheduleNextPreparedFrame();
|
|
1977
|
-
});
|
|
1722
|
+
this._presentation.scheduleNext();
|
|
1978
1723
|
}
|
|
1979
1724
|
/** @internal */
|
|
1980
1725
|
_recordPresentationClock(frameIndex, mediaTime, expectedDisplayTime) {
|
|
@@ -2009,70 +1754,14 @@ export default class AkariSub extends EventTarget {
|
|
|
2009
1754
|
if (!Number.isFinite(targetDisplayTime))
|
|
2010
1755
|
return;
|
|
2011
1756
|
this._predictedDisplayTimes.set(frameIndex + 1, targetDisplayTime);
|
|
2012
|
-
const prepared = this.
|
|
1757
|
+
const prepared = this._presentation.get(frameIndex + 1);
|
|
2013
1758
|
if (prepared)
|
|
2014
1759
|
prepared.targetDisplayTime = targetDisplayTime;
|
|
2015
|
-
this.
|
|
1760
|
+
this._presentation.scheduleNext();
|
|
2016
1761
|
}
|
|
2017
1762
|
/** @internal */
|
|
2018
1763
|
_clearPreparedFrames(preservePresentation = true) {
|
|
2019
|
-
|
|
2020
|
-
if (preservePresentation) {
|
|
2021
|
-
const now = performance.now();
|
|
2022
|
-
let latestBoundary = preservedStage ? (this._stageDisplayTimes.get(preservedStage) ?? -Infinity) : -Infinity;
|
|
2023
|
-
for (const stage of this._stagedCanvases) {
|
|
2024
|
-
const boundary = this._stageDisplayTimes.get(stage);
|
|
2025
|
-
if (boundary != null && boundary <= now && boundary >= latestBoundary) {
|
|
2026
|
-
preservedStage = stage;
|
|
2027
|
-
latestBoundary = boundary;
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
const preservedFrameIndex = preservedStage ? this._stageFrameIndices.get(preservedStage) : undefined;
|
|
2032
|
-
const preservedDisplayTime = preservedStage ? this._stageDisplayTimes.get(preservedStage) : undefined;
|
|
2033
|
-
this._scheduledPreparedFrame = null;
|
|
2034
|
-
for (const frame of this._preparedFrames.values()) {
|
|
2035
|
-
if (frame.stage === preservedStage) {
|
|
2036
|
-
for (const animation of frame.animations ?? [])
|
|
2037
|
-
animation.cancel();
|
|
2038
|
-
frame.animations = undefined;
|
|
2039
|
-
frame.scheduled = false;
|
|
2040
|
-
frame.bitmap?.close();
|
|
2041
|
-
frame.bitmap = undefined;
|
|
2042
|
-
continue;
|
|
2043
|
-
}
|
|
2044
|
-
this._disposePreparedFrame(frame);
|
|
2045
|
-
}
|
|
2046
|
-
this._preparedFrames.clear();
|
|
2047
|
-
for (const stage of [...this._stagedCanvases]) {
|
|
2048
|
-
if (stage !== preservedStage)
|
|
2049
|
-
this._removeStagedCanvas(stage);
|
|
2050
|
-
}
|
|
2051
|
-
this._stagedCanvases.clear();
|
|
2052
|
-
this._stageFrameIndices.clear();
|
|
2053
|
-
this._stageDisplayTimes.clear();
|
|
2054
|
-
for (const animation of this._canvas?.getAnimations?.() ?? [])
|
|
2055
|
-
animation.cancel();
|
|
2056
|
-
if (preservedStage?.isConnected) {
|
|
2057
|
-
for (const animation of preservedStage.getAnimations())
|
|
2058
|
-
animation.cancel();
|
|
2059
|
-
preservedStage.style.opacity = '1';
|
|
2060
|
-
this._stagedCanvases.add(preservedStage);
|
|
2061
|
-
if (preservedFrameIndex != null)
|
|
2062
|
-
this._stageFrameIndices.set(preservedStage, preservedFrameIndex);
|
|
2063
|
-
if (preservedDisplayTime != null)
|
|
2064
|
-
this._stageDisplayTimes.set(preservedStage, preservedDisplayTime);
|
|
2065
|
-
if (this._canvas)
|
|
2066
|
-
this._canvas.style.opacity = '0';
|
|
2067
|
-
this._committedStage = preservedStage;
|
|
2068
|
-
}
|
|
2069
|
-
else {
|
|
2070
|
-
if (preservedStage)
|
|
2071
|
-
this._removeStagedCanvas(preservedStage);
|
|
2072
|
-
this._committedStage = null;
|
|
2073
|
-
if (this._canvas)
|
|
2074
|
-
this._canvas.style.opacity = '1';
|
|
2075
|
-
}
|
|
1764
|
+
this._presentation.clear(preservePresentation);
|
|
2076
1765
|
this._predictedDisplayTimes.clear();
|
|
2077
1766
|
this._displayClockOffsets.length = 0;
|
|
2078
1767
|
this._displayGridAnchorMs = undefined;
|
|
@@ -2085,26 +1774,16 @@ export default class AkariSub extends EventTarget {
|
|
|
2085
1774
|
/** @internal */
|
|
2086
1775
|
_primePreparedFrames(mediaTime) {
|
|
2087
1776
|
const timeline = this._frameTimeline;
|
|
2088
|
-
if (!timeline ||
|
|
1777
|
+
if (!timeline ||
|
|
1778
|
+
timeline.length === 0 ||
|
|
1779
|
+
this.framePrefetch <= 0 ||
|
|
1780
|
+
this._destroyed ||
|
|
1781
|
+
(this._prepareFailureEpoch ?? -1) >= 0 && this._prepareFailureEpoch === this._renderEpoch) {
|
|
2089
1782
|
return;
|
|
2090
1783
|
}
|
|
2091
1784
|
const currentIndex = presentedFrameIndex(timeline, mediaTime);
|
|
2092
1785
|
const lastIndex = Math.min(timeline.length - 1, currentIndex + this.framePrefetch);
|
|
2093
|
-
|
|
2094
|
-
if (index < currentIndex || index > lastIndex) {
|
|
2095
|
-
// A scheduled stage may already be the compositor's visible frame even
|
|
2096
|
-
// when Chromium skips/delays its RVFC. Removing it here produces a blank
|
|
2097
|
-
// refresh. Its successor's completed hide animation owns stage cleanup.
|
|
2098
|
-
if ((frame.scheduled || frame.committed) && index < currentIndex) {
|
|
2099
|
-
frame.bitmap?.close();
|
|
2100
|
-
frame.bitmap = undefined;
|
|
2101
|
-
}
|
|
2102
|
-
else {
|
|
2103
|
-
this._disposePreparedFrame(frame);
|
|
2104
|
-
}
|
|
2105
|
-
this._preparedFrames.delete(index);
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
1786
|
+
this._presentation.prune(currentIndex, lastIndex);
|
|
2108
1787
|
const queued = this._prepareQueue.filter((index) => index > currentIndex && index <= lastIndex);
|
|
2109
1788
|
this._prepareQueue.length = 0;
|
|
2110
1789
|
this._prepareQueue.push(...queued);
|
|
@@ -2114,17 +1793,22 @@ export default class AkariSub extends EventTarget {
|
|
|
2114
1793
|
for (const index of this._prepareQueue)
|
|
2115
1794
|
requested.add(index);
|
|
2116
1795
|
for (let index = currentIndex + 1; index <= lastIndex; index++) {
|
|
2117
|
-
if (!this.
|
|
1796
|
+
if (!this._presentation.has(index) && !requested.has(index))
|
|
2118
1797
|
this._prepareQueue.push(index);
|
|
2119
1798
|
}
|
|
2120
1799
|
}
|
|
2121
1800
|
/** @internal */
|
|
2122
1801
|
_dispatchNextPreparation() {
|
|
2123
|
-
if (this.busy ||
|
|
1802
|
+
if (this.busy ||
|
|
1803
|
+
!this._workerReady ||
|
|
1804
|
+
!this._frameTimeline ||
|
|
1805
|
+
this.framePrefetch <= 0 ||
|
|
1806
|
+
(this._prepareFailureEpoch ?? -1) >= 0 && this._prepareFailureEpoch === this._renderEpoch) {
|
|
2124
1807
|
return;
|
|
1808
|
+
}
|
|
2125
1809
|
let index;
|
|
2126
1810
|
while ((index = this._prepareQueue.shift()) != null) {
|
|
2127
|
-
if (!this.
|
|
1811
|
+
if (!this._presentation.has(index))
|
|
2128
1812
|
break;
|
|
2129
1813
|
}
|
|
2130
1814
|
if (index == null)
|
|
@@ -2148,9 +1832,15 @@ export default class AkariSub extends EventTarget {
|
|
|
2148
1832
|
_preparedFrame(data) {
|
|
2149
1833
|
const request = this._prepareRequests.get(data.prepareId);
|
|
2150
1834
|
this._prepareRequests.delete(data.prepareId);
|
|
1835
|
+
const validEpoch = request != null && request.renderEpoch === this._renderEpoch && data.renderEpoch === this._renderEpoch;
|
|
1836
|
+
if (data.failed && validEpoch) {
|
|
1837
|
+
// A snapshot failure must not immediately enqueue the same frame again.
|
|
1838
|
+
// Fall back to demand rendering for this epoch and let a later epoch retry.
|
|
1839
|
+
this._prepareFailureEpoch = this._renderEpoch;
|
|
1840
|
+
this._prepareQueue.length = 0;
|
|
1841
|
+
}
|
|
2151
1842
|
if (request?.presentation) {
|
|
2152
1843
|
const presentation = request.presentation;
|
|
2153
|
-
const validEpoch = request.renderEpoch === this._renderEpoch && data.renderEpoch === this._renderEpoch;
|
|
2154
1844
|
const currentIndex = this._currentExactFrameIndex();
|
|
2155
1845
|
const obsolete = currentIndex != null && request.index < currentIndex;
|
|
2156
1846
|
if (data.bitmap && validEpoch && !obsolete) {
|
|
@@ -2185,9 +1875,6 @@ export default class AkariSub extends EventTarget {
|
|
|
2185
1875
|
request.renderEpoch === this._renderEpoch &&
|
|
2186
1876
|
data.renderEpoch === this._renderEpoch &&
|
|
2187
1877
|
request.index >= currentIndex) {
|
|
2188
|
-
const previous = this._preparedFrames.get(request.index);
|
|
2189
|
-
if (previous)
|
|
2190
|
-
this._disposePreparedFrame(previous);
|
|
2191
1878
|
const prepared = {
|
|
2192
1879
|
width: data.width ?? this._canvasctrl.width,
|
|
2193
1880
|
height: data.height ?? this._canvasctrl.height,
|
|
@@ -2195,8 +1882,7 @@ export default class AkariSub extends EventTarget {
|
|
|
2195
1882
|
time: data.time
|
|
2196
1883
|
};
|
|
2197
1884
|
this._stagePreparedFrame(request.index, prepared);
|
|
2198
|
-
this.
|
|
2199
|
-
this._scheduleNextPreparedFrame();
|
|
1885
|
+
this._presentation.store(request.index, prepared);
|
|
2200
1886
|
}
|
|
2201
1887
|
else {
|
|
2202
1888
|
data.bitmap?.close();
|
|
@@ -2209,35 +1895,12 @@ export default class AkariSub extends EventTarget {
|
|
|
2209
1895
|
/** @internal */
|
|
2210
1896
|
_presentPreparedFrame(frame, presentationId, expectedDisplayTime) {
|
|
2211
1897
|
if (!this._activatePresentation(presentationId, frame.time)) {
|
|
2212
|
-
|
|
2213
|
-
// newer callback has already advanced the presentation watermark, this
|
|
2214
|
-
// stage can still be the compositor's current frame. Never tear down a
|
|
2215
|
-
// scheduled stage from a stale callback; the next atomic swap removes it.
|
|
2216
|
-
if (frame.stage && (frame.scheduled || frame.committed || this._committedStage === frame.stage)) {
|
|
2217
|
-
frame.bitmap?.close();
|
|
2218
|
-
frame.bitmap = undefined;
|
|
2219
|
-
}
|
|
2220
|
-
else {
|
|
2221
|
-
this._disposePreparedFrame(frame);
|
|
2222
|
-
}
|
|
1898
|
+
this._presentation.reject(frame);
|
|
2223
1899
|
return;
|
|
2224
1900
|
}
|
|
2225
1901
|
const { bitmap, width, height, stage } = frame;
|
|
2226
1902
|
if (stage) {
|
|
2227
|
-
|
|
2228
|
-
for (const animation of frame.animations ?? []) {
|
|
2229
|
-
try {
|
|
2230
|
-
animation.finish();
|
|
2231
|
-
}
|
|
2232
|
-
catch {
|
|
2233
|
-
animation.cancel();
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
this._commitPreparedStage(frame);
|
|
2237
|
-
}
|
|
2238
|
-
this._stageDisplayTimes.set(stage, Number.isFinite(expectedDisplayTime) ? expectedDisplayTime : performance.now());
|
|
2239
|
-
bitmap?.close();
|
|
2240
|
-
frame.bitmap = undefined;
|
|
1903
|
+
this._presentation.present(frame, expectedDisplayTime);
|
|
2241
1904
|
return;
|
|
2242
1905
|
}
|
|
2243
1906
|
if (!bitmap)
|
|
@@ -2258,11 +1921,11 @@ export default class AkariSub extends EventTarget {
|
|
|
2258
1921
|
frame.replaceAll = true;
|
|
2259
1922
|
this._stagePreparedFrame(-1, frame, false);
|
|
2260
1923
|
if (frame.stage)
|
|
2261
|
-
this.
|
|
1924
|
+
this._presentation.commit(frame);
|
|
2262
1925
|
return;
|
|
2263
1926
|
}
|
|
2264
1927
|
const completion = this._activateBaseCanvasAfterGPUWork(this._renderEpoch, frame.index) ?? true;
|
|
2265
|
-
void this.
|
|
1928
|
+
void this._gpuRecovery.retainAfter(completion, snapshot);
|
|
2266
1929
|
return;
|
|
2267
1930
|
}
|
|
2268
1931
|
if (this._ctx) {
|
|
@@ -2300,6 +1963,7 @@ export default class AkariSub extends EventTarget {
|
|
|
2300
1963
|
/** @internal */
|
|
2301
1964
|
_bumpRenderEpoch() {
|
|
2302
1965
|
this._renderEpoch++;
|
|
1966
|
+
this._prepareFailureEpoch = -1;
|
|
2303
1967
|
this._pendingDemandTimes.length = 0;
|
|
2304
1968
|
this._demandTimings.clear();
|
|
2305
1969
|
this._clearPreparedFrames();
|
|
@@ -2343,6 +2007,16 @@ export default class AkariSub extends EventTarget {
|
|
|
2343
2007
|
}
|
|
2344
2008
|
if (this._destroyed || this._video !== video || generation !== this._rvfcGeneration)
|
|
2345
2009
|
return;
|
|
2010
|
+
if (this._playstate &&
|
|
2011
|
+
!video.paused &&
|
|
2012
|
+
!video.ended &&
|
|
2013
|
+
!video.seeking &&
|
|
2014
|
+
this._lastRvfcMediaTime != null &&
|
|
2015
|
+
metadata.mediaTime > this._lastRvfcMediaTime) {
|
|
2016
|
+
this._playstate = false;
|
|
2017
|
+
this._syncVideoClock();
|
|
2018
|
+
}
|
|
2019
|
+
this._lastRvfcMediaTime = metadata.mediaTime;
|
|
2346
2020
|
this._handleRVFC(now, metadata);
|
|
2347
2021
|
});
|
|
2348
2022
|
}
|
|
@@ -2384,14 +2058,12 @@ export default class AkariSub extends EventTarget {
|
|
|
2384
2058
|
switch (event.type) {
|
|
2385
2059
|
case 'play':
|
|
2386
2060
|
case 'playing':
|
|
2387
|
-
case 'canplay':
|
|
2388
2061
|
this._playstate = false;
|
|
2389
2062
|
break;
|
|
2390
2063
|
case 'pause':
|
|
2391
2064
|
case 'ended':
|
|
2392
2065
|
case 'seeking':
|
|
2393
2066
|
case 'waiting':
|
|
2394
|
-
case 'stalled':
|
|
2395
2067
|
this._playstate = true;
|
|
2396
2068
|
break;
|
|
2397
2069
|
case 'seeked':
|
|
@@ -2498,15 +2170,14 @@ export default class AkariSub extends EventTarget {
|
|
|
2498
2170
|
const frameIndex = presentedFrameIndex(this._frameTimeline, mediaTime);
|
|
2499
2171
|
if (frameIndex >= 0)
|
|
2500
2172
|
this._lastPresentedFrameIndex = frameIndex;
|
|
2501
|
-
const prepared = this.
|
|
2173
|
+
const prepared = this._presentation.take(frameIndex);
|
|
2502
2174
|
if (prepared) {
|
|
2503
|
-
this._preparedFrames.delete(frameIndex);
|
|
2504
2175
|
if (prepared.width === this._canvasctrl.width && prepared.height === this._canvasctrl.height) {
|
|
2505
2176
|
this._presentPreparedFrame(prepared, presentationId, expectedDisplayTime);
|
|
2506
2177
|
presented = true;
|
|
2507
2178
|
}
|
|
2508
2179
|
else {
|
|
2509
|
-
this.
|
|
2180
|
+
this._presentation.dispose(prepared);
|
|
2510
2181
|
this._prepareForce = true;
|
|
2511
2182
|
}
|
|
2512
2183
|
}
|
|
@@ -2730,6 +2401,7 @@ export default class AkariSub extends EventTarget {
|
|
|
2730
2401
|
const hasAlphaBug = AkariSub._hasAlphaBug ?? false;
|
|
2731
2402
|
const bufferCanvas = this._bufferCanvas;
|
|
2732
2403
|
const bufferCtx = this._bufferCtx;
|
|
2404
|
+
const canWritePreblendedDirectly = data.preblended === true && ctx.filter === 'none';
|
|
2733
2405
|
for (let i = 0; i < imageCount; i++) {
|
|
2734
2406
|
const image = images[i];
|
|
2735
2407
|
if (image.image) {
|
|
@@ -2742,11 +2414,20 @@ export default class AkariSub extends EventTarget {
|
|
|
2742
2414
|
? new Uint8ClampedArray(rawImage.buffer, rawImage.byteOffset, rawImage.byteLength)
|
|
2743
2415
|
: new Uint8ClampedArray(rawImage);
|
|
2744
2416
|
const fixedData = fixAlpha(rawData, hasAlphaBug);
|
|
2417
|
+
const imageData = createSubtitleImageData(fixedData, imgW, imgH);
|
|
2418
|
+
if (canWritePreblendedDirectly) {
|
|
2419
|
+
// renderBlendCollect guarantees these rectangles are already
|
|
2420
|
+
// source-over flattened and mutually disjoint. The render target
|
|
2421
|
+
// is cleared above, so a direct write preserves the same frame
|
|
2422
|
+
// while avoiding one upload and draw per plane.
|
|
2423
|
+
ctx.putImageData(imageData, image.x, image.y);
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2745
2426
|
if (bufferCanvas.width !== imgW || bufferCanvas.height !== imgH) {
|
|
2746
2427
|
bufferCanvas.width = imgW;
|
|
2747
2428
|
bufferCanvas.height = imgH;
|
|
2748
2429
|
}
|
|
2749
|
-
bufferCtx.putImageData(
|
|
2430
|
+
bufferCtx.putImageData(imageData, 0, 0);
|
|
2750
2431
|
ctx.drawImage(bufferCanvas, image.x, image.y);
|
|
2751
2432
|
}
|
|
2752
2433
|
}
|
|
@@ -2797,7 +2478,7 @@ export default class AkariSub extends EventTarget {
|
|
|
2797
2478
|
}
|
|
2798
2479
|
if (painted !== false) {
|
|
2799
2480
|
const completion = this._activateBaseCanvasAfterGPUWork(data.renderEpoch, presentedIndex) ?? true;
|
|
2800
|
-
return this.
|
|
2481
|
+
return this._gpuRecovery.retainAfter(completion, snapshot);
|
|
2801
2482
|
}
|
|
2802
2483
|
this._discardGPUSnapshot(snapshot);
|
|
2803
2484
|
return false;
|
|
@@ -2842,7 +2523,7 @@ export default class AkariSub extends EventTarget {
|
|
|
2842
2523
|
}
|
|
2843
2524
|
}
|
|
2844
2525
|
const completion = painted !== false ? (this._activateBaseCanvasAfterGPUWork(data.renderEpoch, presentedIndex) ?? true) : false;
|
|
2845
|
-
const retainedCompletion = this.
|
|
2526
|
+
const retainedCompletion = this._gpuRecovery.retainAfter(completion, snapshot);
|
|
2846
2527
|
if (this.debug) {
|
|
2847
2528
|
data.times.JSRenderTime = Date.now() - (data.times.JSRenderTime || 0) - (data.times.IPCTime || 0);
|
|
2848
2529
|
let total = 0;
|
|
@@ -2863,7 +2544,7 @@ export default class AkariSub extends EventTarget {
|
|
|
2863
2544
|
const currentTime = this._clockCurrentTime() ?? 0;
|
|
2864
2545
|
const isPaused = this._isVideoPausedForWorker();
|
|
2865
2546
|
const bufferExactFrames = isPaused && this._onDemandRender && !!this._frameTimeline?.length && this.framePrefetch > 0;
|
|
2866
|
-
this._frameBufferReadyEvent = bufferExactFrames ? 'ready' : null;
|
|
2547
|
+
this._frameBufferReadyEvent = bufferExactFrames ? { type: 'ready' } : null;
|
|
2867
2548
|
this.setCurrentTime(isPaused, currentTime + this.timeOffset, this._videoPlaybackRateForWorker());
|
|
2868
2549
|
if (!this._onDemandRender) {
|
|
2869
2550
|
this.dispatchEvent(new CustomEvent('ready'));
|
|
@@ -2892,20 +2573,39 @@ export default class AkariSub extends EventTarget {
|
|
|
2892
2573
|
this.dispatchEvent(new CustomEvent('partial_ready'));
|
|
2893
2574
|
}
|
|
2894
2575
|
/** @internal */
|
|
2895
|
-
_trackReady() {
|
|
2576
|
+
_trackReady(data = {}) {
|
|
2577
|
+
this._supersedePendingTrackActivation(data.requestId);
|
|
2896
2578
|
const bufferExactFrames = this._workerReady &&
|
|
2897
2579
|
this._isVideoPausedForWorker() &&
|
|
2898
2580
|
this._onDemandRender &&
|
|
2899
2581
|
!!this._frameTimeline?.length &&
|
|
2900
2582
|
this.framePrefetch > 0;
|
|
2901
|
-
this._frameBufferReadyEvent = bufferExactFrames
|
|
2583
|
+
this._frameBufferReadyEvent = bufferExactFrames
|
|
2584
|
+
? { type: 'trackReady', requestId: data.requestId }
|
|
2585
|
+
: null;
|
|
2902
2586
|
this._syncVideoClock();
|
|
2903
2587
|
if (bufferExactFrames) {
|
|
2904
2588
|
this._primePreparedFrames(this._currentExactFrameMediaTime());
|
|
2905
2589
|
this._dispatchNextPreparation();
|
|
2906
2590
|
return;
|
|
2907
2591
|
}
|
|
2908
|
-
this.dispatchEvent(new CustomEvent('trackReady'));
|
|
2592
|
+
this.dispatchEvent(new CustomEvent('trackReady', data.requestId == null ? undefined : { detail: { requestId: data.requestId } }));
|
|
2593
|
+
}
|
|
2594
|
+
/** @internal */
|
|
2595
|
+
_supersedePendingTrackActivation(nextRequestId) {
|
|
2596
|
+
const supersededRequestIds = new Set();
|
|
2597
|
+
for (const [requestId, supersede] of [...(this._pendingTrackReadyWaiters ?? [])]) {
|
|
2598
|
+
if (requestId === nextRequestId)
|
|
2599
|
+
continue;
|
|
2600
|
+
supersededRequestIds.add(requestId);
|
|
2601
|
+
supersede(new Error('Track activation was superseded by a newer track change'));
|
|
2602
|
+
}
|
|
2603
|
+
const pending = this._frameBufferReadyEvent;
|
|
2604
|
+
if (pending?.type === 'trackReady' &&
|
|
2605
|
+
pending.requestId != null &&
|
|
2606
|
+
supersededRequestIds.has(pending.requestId)) {
|
|
2607
|
+
this._frameBufferReadyEvent = null;
|
|
2608
|
+
}
|
|
2909
2609
|
}
|
|
2910
2610
|
/** Send a raw message to the worker. Prefer the typed methods above. */
|
|
2911
2611
|
async sendMessage(target, data = {}, transferable) {
|
|
@@ -2944,6 +2644,15 @@ export default class AkariSub extends EventTarget {
|
|
|
2944
2644
|
reject(new Error('Error: Timeout while trying to fetch ' + target));
|
|
2945
2645
|
}, timeoutMs);
|
|
2946
2646
|
const handleMessage = (event) => {
|
|
2647
|
+
if (event.data.target === 'error') {
|
|
2648
|
+
const workerError = event.data.error;
|
|
2649
|
+
handleError(workerError instanceof Error
|
|
2650
|
+
? workerError
|
|
2651
|
+
: new Error(typeof workerError === 'string'
|
|
2652
|
+
? workerError
|
|
2653
|
+
: workerError?.message || 'Unknown worker error'));
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2947
2656
|
if (event.data.target === target &&
|
|
2948
2657
|
(workerMessage.requestId === undefined || event.data.requestId === workerMessage.requestId)) {
|
|
2949
2658
|
cleanup();
|
|
@@ -2982,6 +2691,45 @@ export default class AkariSub extends EventTarget {
|
|
|
2982
2691
|
});
|
|
2983
2692
|
}
|
|
2984
2693
|
/** @internal */
|
|
2694
|
+
_waitForTrackReady(requestId) {
|
|
2695
|
+
let cancel = () => { };
|
|
2696
|
+
const promise = new Promise((resolve) => {
|
|
2697
|
+
let settled = false;
|
|
2698
|
+
const cleanup = () => {
|
|
2699
|
+
this.removeEventListener('trackReady', handleTrackReady);
|
|
2700
|
+
this.removeEventListener('error', handleError);
|
|
2701
|
+
this._pendingWorkerRejectors.delete(handleDestroyed);
|
|
2702
|
+
if (this._pendingTrackReadyWaiters.get(requestId) === handleSuperseded) {
|
|
2703
|
+
this._pendingTrackReadyWaiters.delete(requestId);
|
|
2704
|
+
}
|
|
2705
|
+
};
|
|
2706
|
+
const settle = (error) => {
|
|
2707
|
+
if (settled)
|
|
2708
|
+
return;
|
|
2709
|
+
settled = true;
|
|
2710
|
+
cleanup();
|
|
2711
|
+
resolve(error);
|
|
2712
|
+
};
|
|
2713
|
+
const handleTrackReady = (event) => {
|
|
2714
|
+
if (event.detail?.requestId !== requestId)
|
|
2715
|
+
return;
|
|
2716
|
+
settle(null);
|
|
2717
|
+
};
|
|
2718
|
+
const handleError = (event) => {
|
|
2719
|
+
const errorEvent = event;
|
|
2720
|
+
settle(errorEvent.error instanceof Error ? errorEvent.error : new Error(errorEvent.message || 'Renderer error'));
|
|
2721
|
+
};
|
|
2722
|
+
const handleDestroyed = (error) => settle(error);
|
|
2723
|
+
const handleSuperseded = (error) => settle(error);
|
|
2724
|
+
cancel = () => settle(null);
|
|
2725
|
+
this.addEventListener('trackReady', handleTrackReady);
|
|
2726
|
+
this.addEventListener('error', handleError);
|
|
2727
|
+
this._pendingWorkerRejectors.add(handleDestroyed);
|
|
2728
|
+
this._pendingTrackReadyWaiters.set(requestId, handleSuperseded);
|
|
2729
|
+
});
|
|
2730
|
+
return { promise, cancel: () => cancel() };
|
|
2731
|
+
}
|
|
2732
|
+
/** @internal */
|
|
2985
2733
|
_cues(data) {
|
|
2986
2734
|
for (const cue of data.exited) {
|
|
2987
2735
|
this._onCueExit?.(cue);
|
|
@@ -3101,13 +2849,12 @@ export default class AkariSub extends EventTarget {
|
|
|
3101
2849
|
cancelAnimationFrame(this._refreshRafHandle);
|
|
3102
2850
|
this._refreshRafHandle = null;
|
|
3103
2851
|
}
|
|
3104
|
-
this.
|
|
3105
|
-
this._gpuRecoveryGeneration++;
|
|
3106
|
-
this._gpuRecovering = null;
|
|
2852
|
+
this._gpuRecovery.destroy();
|
|
3107
2853
|
this._recoveryCanvas?.remove();
|
|
3108
2854
|
this._recoveryCanvas = null;
|
|
3109
2855
|
this._recoveryCtx = null;
|
|
3110
2856
|
this._clearPreparedFrames(false);
|
|
2857
|
+
this._presentation.destroy();
|
|
3111
2858
|
if (this._video && this._canvasParent) {
|
|
3112
2859
|
this._video.parentNode?.removeChild(this._canvasParent);
|
|
3113
2860
|
}
|
|
@@ -3116,7 +2863,6 @@ export default class AkariSub extends EventTarget {
|
|
|
3116
2863
|
this._gpuRenderer = null;
|
|
3117
2864
|
this._rendererType = 'canvas2d';
|
|
3118
2865
|
}
|
|
3119
|
-
this._lastGPUFrame = null;
|
|
3120
2866
|
this._gpuSnapshotPool.length = 0;
|
|
3121
2867
|
this._destroyed = true;
|
|
3122
2868
|
this._resolveDestroyed?.();
|