visualfries 0.1.10110 → 0.1.10115
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/SceneBuilder.svelte.js +3 -1
- package/dist/commands/SeekCommand.js +61 -25
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.d.ts +3 -0
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +69 -12
- package/dist/layers/Layer.svelte.js +40 -5
- package/dist/managers/ComponentsManager.svelte.js +31 -4
- package/dist/managers/DeterministicMediaManager.d.ts +3 -0
- package/dist/managers/DeterministicMediaManager.js +46 -0
- package/dist/schemas/runtime/deterministic.d.ts +16 -0
- package/dist/schemas/runtime/deterministic.js +4 -0
- package/package.json +2 -1
|
@@ -403,7 +403,9 @@ export class SceneBuilder {
|
|
|
403
403
|
framesSkipped += 1;
|
|
404
404
|
}
|
|
405
405
|
else {
|
|
406
|
-
|
|
406
|
+
// isSceneDirty() already sought and prepared server deterministic state.
|
|
407
|
+
// Render directly to avoid a second seek in the dirty path.
|
|
408
|
+
frame = await this.renderFrame(undefined, format, quality, imageOptions);
|
|
407
409
|
previousFrame = frame;
|
|
408
410
|
}
|
|
409
411
|
}
|
|
@@ -13,6 +13,7 @@ export class SeekCommand {
|
|
|
13
13
|
renderManager;
|
|
14
14
|
componentsManager;
|
|
15
15
|
deterministicMediaManager;
|
|
16
|
+
#didAwaitFontsReady = false;
|
|
16
17
|
constructor(cradle) {
|
|
17
18
|
this.timeline = cradle.timelineManager;
|
|
18
19
|
this.state = cradle.stateManager;
|
|
@@ -61,17 +62,62 @@ export class SeekCommand {
|
|
|
61
62
|
}
|
|
62
63
|
return pending;
|
|
63
64
|
}
|
|
65
|
+
#getCurrentSceneFrameIndex() {
|
|
66
|
+
const fps = this.state.data?.settings?.fps || 30;
|
|
67
|
+
const currentTime = this.state.currentTime ?? 0;
|
|
68
|
+
return Math.max(0, Math.round(currentTime * fps));
|
|
69
|
+
}
|
|
70
|
+
async #delay(ms) {
|
|
71
|
+
if (ms <= 0) {
|
|
72
|
+
const immediate = globalThis.setImmediate;
|
|
73
|
+
if (typeof immediate === 'function') {
|
|
74
|
+
await new Promise((resolve) => {
|
|
75
|
+
immediate(resolve);
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await Promise.resolve();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
83
|
+
}
|
|
84
|
+
async #awaitFontsReadyOnce() {
|
|
85
|
+
if (this.#didAwaitFontsReady) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.#didAwaitFontsReady = true;
|
|
89
|
+
if (typeof document === 'undefined' || !document.fonts?.ready) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await Promise.race([
|
|
94
|
+
document.fonts.ready,
|
|
95
|
+
new Promise((resolve) => setTimeout(resolve, 2000))
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Font readiness is best-effort in seek path.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
64
102
|
async #renderUntilDeterministicReady() {
|
|
65
|
-
const
|
|
103
|
+
const pendingBeforeRetry = this.#getPendingDeterministicComponents();
|
|
104
|
+
if (pendingBeforeRetry.length === 0) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const maxAttempts = this.deterministicMediaManager.config.seekMaxAttempts ?? 4;
|
|
108
|
+
const readyYieldMs = this.deterministicMediaManager.config.readyYieldMs ?? 0;
|
|
109
|
+
const sceneFrameIndex = this.#getCurrentSceneFrameIndex();
|
|
110
|
+
let pending = pendingBeforeRetry;
|
|
66
111
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
112
|
+
this.deterministicMediaManager.recordReadyAttempt(sceneFrameIndex);
|
|
113
|
+
this.deterministicMediaManager.recordExtraRenderPass(sceneFrameIndex);
|
|
114
|
+
await this.#delay(readyYieldMs);
|
|
67
115
|
await this.renderManager.render();
|
|
68
|
-
|
|
116
|
+
pending = this.#getPendingDeterministicComponents();
|
|
69
117
|
if (pending.length === 0) {
|
|
70
118
|
return;
|
|
71
119
|
}
|
|
72
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
73
120
|
}
|
|
74
|
-
const pending = this.#getPendingDeterministicComponents();
|
|
75
121
|
if (pending.length > 0) {
|
|
76
122
|
throw new Error(`Deterministic media was not ready after seek for active components: ${pending.join(', ')}`);
|
|
77
123
|
}
|
|
@@ -85,28 +131,18 @@ export class SeekCommand {
|
|
|
85
131
|
this.timeline.seek(time);
|
|
86
132
|
// Ensure a deterministic render on server after seek to advance media frames
|
|
87
133
|
if (this.state.environment === 'server') {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// animations may fail silently or not appear on first subtitles
|
|
92
|
-
if (typeof document !== 'undefined' && document.fonts?.ready) {
|
|
93
|
-
try {
|
|
94
|
-
await Promise.race([
|
|
95
|
-
document.fonts.ready,
|
|
96
|
-
new Promise((resolve) => setTimeout(resolve, 2000)) // 2s timeout
|
|
97
|
-
]);
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
// Ignore font loading errors, continue with rendering
|
|
101
|
-
}
|
|
134
|
+
const deterministicEnabled = this.deterministicMediaManager.isEnabled();
|
|
135
|
+
if (deterministicEnabled) {
|
|
136
|
+
await this.#awaitFontsReadyOnce();
|
|
102
137
|
}
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
138
|
+
const readyYieldMs = this.deterministicMediaManager.config.readyYieldMs ?? 0;
|
|
139
|
+
const loadingMaxAttempts = this.deterministicMediaManager.config.loadingMaxAttempts ?? 2;
|
|
140
|
+
const sceneFrameIndex = this.#getCurrentSceneFrameIndex();
|
|
141
|
+
await this.renderManager.render();
|
|
142
|
+
for (let i = 0; i < loadingMaxAttempts && this.state.state === 'loading'; i += 1) {
|
|
143
|
+
this.deterministicMediaManager.recordExtraRenderPass(sceneFrameIndex);
|
|
144
|
+
await this.#delay(readyYieldMs);
|
|
106
145
|
await this.renderManager.render();
|
|
107
|
-
if (this.state.state !== 'loading')
|
|
108
|
-
break;
|
|
109
|
-
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
110
146
|
}
|
|
111
147
|
if (this.state.state === 'loading') {
|
|
112
148
|
console.warn('SeekCommand: Max render attempts exhausted while still loading');
|
|
@@ -116,7 +152,7 @@ export class SeekCommand {
|
|
|
116
152
|
// This fixes the race condition where subtitle animations are added
|
|
117
153
|
// AFTER the initial seek, causing them to miss their initial state.
|
|
118
154
|
this.timeline.seek(time);
|
|
119
|
-
if (
|
|
155
|
+
if (deterministicEnabled) {
|
|
120
156
|
await this.#renderUntilDeterministicReady();
|
|
121
157
|
}
|
|
122
158
|
else {
|
|
@@ -2,14 +2,17 @@ import type { IComponentContext, IComponentHook, HookType } from '../..';
|
|
|
2
2
|
import { ImageComponentShape, VideoComponentShape } from '../..';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import type { StateManager } from '../../managers/StateManager.svelte.ts';
|
|
5
|
+
import type { DeterministicMediaManager } from '../../managers/DeterministicMediaManager.ts';
|
|
5
6
|
export declare class PixiSplitScreenDisplayObjectHook implements IComponentHook {
|
|
6
7
|
#private;
|
|
7
8
|
types: HookType[];
|
|
8
9
|
priority: number;
|
|
9
10
|
componentElement: z.infer<typeof VideoComponentShape> | z.infer<typeof ImageComponentShape>;
|
|
10
11
|
private sceneState;
|
|
12
|
+
private deterministicMediaManager?;
|
|
11
13
|
constructor(cradle: {
|
|
12
14
|
stateManager: StateManager;
|
|
15
|
+
deterministicMediaManager?: DeterministicMediaManager;
|
|
13
16
|
});
|
|
14
17
|
get sceneWidth(): number;
|
|
15
18
|
get sceneHeight(): number;
|
|
@@ -16,10 +16,18 @@ export class PixiSplitScreenDisplayObjectHook {
|
|
|
16
16
|
#pixiTexture;
|
|
17
17
|
#displayObject;
|
|
18
18
|
#bgCanvas = undefined;
|
|
19
|
+
#bgSprite = undefined;
|
|
20
|
+
#blurStrength = 50;
|
|
21
|
+
#blurDownscale = 0.33;
|
|
22
|
+
#lastBlurFrameKey = '';
|
|
23
|
+
#resourceIds = new WeakMap();
|
|
24
|
+
#nextResourceId = 1;
|
|
19
25
|
componentElement;
|
|
20
26
|
sceneState;
|
|
27
|
+
deterministicMediaManager;
|
|
21
28
|
constructor(cradle) {
|
|
22
29
|
this.sceneState = cradle.stateManager;
|
|
30
|
+
this.deterministicMediaManager = cradle.deterministicMediaManager;
|
|
23
31
|
}
|
|
24
32
|
get sceneWidth() {
|
|
25
33
|
return this.#context.sceneState.width;
|
|
@@ -29,10 +37,21 @@ export class PixiSplitScreenDisplayObjectHook {
|
|
|
29
37
|
}
|
|
30
38
|
initBlurBackground(strength = 50) {
|
|
31
39
|
const sanitizedStrength = this.#sanitizeBlurStrength(strength);
|
|
40
|
+
this.#blurDownscale = this.#getBlurDownscale();
|
|
41
|
+
this.#blurStrength = sanitizedStrength;
|
|
42
|
+
this.#lastBlurFrameKey = '';
|
|
32
43
|
const backgroundSprite = new PIXI.Sprite(this.#pixiTexture);
|
|
44
|
+
this.#bgSprite = backgroundSprite;
|
|
33
45
|
this.setupBackground(backgroundSprite, sanitizedStrength);
|
|
34
46
|
this.#displayObject.addChild(backgroundSprite);
|
|
35
47
|
}
|
|
48
|
+
#getBlurDownscale() {
|
|
49
|
+
const configured = this.deterministicMediaManager?.config.blurDownscale ?? 0.33;
|
|
50
|
+
if (!Number.isFinite(configured)) {
|
|
51
|
+
return 0.33;
|
|
52
|
+
}
|
|
53
|
+
return Math.max(0.05, Math.min(1, configured));
|
|
54
|
+
}
|
|
36
55
|
setupBackground(backgroundSprite, strength = 50) {
|
|
37
56
|
const sanitizedStrength = this.#sanitizeBlurStrength(strength);
|
|
38
57
|
const sceneRatio = this.sceneWidth / this.sceneHeight;
|
|
@@ -54,10 +73,10 @@ export class PixiSplitScreenDisplayObjectHook {
|
|
|
54
73
|
if (this.sceneState.environment === 'server') {
|
|
55
74
|
// Create a temporary canvas for blur effect
|
|
56
75
|
const bgCanvas = document.createElement('canvas');
|
|
57
|
-
bgCanvas.width = backgroundSprite.width;
|
|
58
|
-
bgCanvas.height = backgroundSprite.height;
|
|
76
|
+
bgCanvas.width = Math.max(1, Math.round(backgroundSprite.width * this.#blurDownscale));
|
|
77
|
+
bgCanvas.height = Math.max(1, Math.round(backgroundSprite.height * this.#blurDownscale));
|
|
59
78
|
this.#bgCanvas = bgCanvas;
|
|
60
|
-
this.#drawBlurredBackground(sanitizedStrength);
|
|
79
|
+
this.#drawBlurredBackground(sanitizedStrength, true);
|
|
61
80
|
const blurredTexture = PIXI.Texture.from(bgCanvas);
|
|
62
81
|
backgroundSprite.texture = blurredTexture;
|
|
63
82
|
}
|
|
@@ -67,23 +86,59 @@ export class PixiSplitScreenDisplayObjectHook {
|
|
|
67
86
|
backgroundSprite.filters = [blurFilter];
|
|
68
87
|
}
|
|
69
88
|
}
|
|
70
|
-
#drawBlurredBackground(strength = 50) {
|
|
89
|
+
#drawBlurredBackground(strength = 50, force = false) {
|
|
71
90
|
if (!this.#bgCanvas) {
|
|
72
|
-
return;
|
|
91
|
+
return false;
|
|
73
92
|
}
|
|
74
93
|
// Validate and sanitize strength parameter
|
|
75
94
|
const sanitizedStrength = this.#sanitizeBlurStrength(strength);
|
|
76
95
|
const ctx = this.#bgCanvas.getContext('2d');
|
|
77
|
-
// Use sanitized value to prevent XSS
|
|
78
|
-
ctx.filter = `blur(${sanitizedStrength}px)`;
|
|
79
96
|
// Get the source element (video/image)
|
|
80
97
|
const sourceElement = this.#context.getResource('videoElement') || this.#context.getResource('imageElement');
|
|
81
98
|
if (!sourceElement) {
|
|
82
99
|
// Video or Image element not ready yet - will be called again on next update
|
|
83
|
-
return;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const fps = this.sceneState.data.settings.fps || 30;
|
|
103
|
+
const componentFrameIndex = Math.max(0, Math.round(this.#context.currentComponentTime * fps));
|
|
104
|
+
const textureToken = this.#context.getResource('pixiTexture') ?? sourceElement;
|
|
105
|
+
const frameKey = `${componentFrameIndex}:${this.#resourceToken(textureToken)}`;
|
|
106
|
+
if (!force && frameKey === this.#lastBlurFrameKey) {
|
|
107
|
+
return false;
|
|
84
108
|
}
|
|
109
|
+
this.#lastBlurFrameKey = frameKey;
|
|
110
|
+
// Use sanitized value to prevent XSS
|
|
111
|
+
const effectiveBlurStrength = sanitizedStrength * this.#blurDownscale;
|
|
112
|
+
ctx.filter = `blur(${effectiveBlurStrength}px)`;
|
|
113
|
+
ctx.clearRect(0, 0, this.#bgCanvas.width, this.#bgCanvas.height);
|
|
85
114
|
// Draw the original texture with blur
|
|
86
|
-
|
|
115
|
+
try {
|
|
116
|
+
ctx.drawImage(sourceElement, 0, 0, this.#bgCanvas.width, this.#bgCanvas.height);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const texture = this.#bgSprite?.texture;
|
|
122
|
+
texture.baseTexture?.update?.();
|
|
123
|
+
texture.update?.();
|
|
124
|
+
this.deterministicMediaManager?.recordBlurRedraw(this.#currentSceneFrameIndex());
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
#resourceToken(value) {
|
|
128
|
+
if (typeof value === 'object' && value !== null) {
|
|
129
|
+
let id = this.#resourceIds.get(value);
|
|
130
|
+
if (!id) {
|
|
131
|
+
id = this.#nextResourceId;
|
|
132
|
+
this.#nextResourceId += 1;
|
|
133
|
+
this.#resourceIds.set(value, id);
|
|
134
|
+
}
|
|
135
|
+
return `obj-${id}`;
|
|
136
|
+
}
|
|
137
|
+
return String(value);
|
|
138
|
+
}
|
|
139
|
+
#currentSceneFrameIndex() {
|
|
140
|
+
const fps = this.sceneState.data.settings.fps || 30;
|
|
141
|
+
return Math.max(0, Math.round(this.sceneState.currentTime * fps));
|
|
87
142
|
}
|
|
88
143
|
/**
|
|
89
144
|
* Sanitizes blur strength parameter to prevent XSS and ensure valid numeric values
|
|
@@ -187,9 +242,6 @@ export class PixiSplitScreenDisplayObjectHook {
|
|
|
187
242
|
}
|
|
188
243
|
async #handleUpdate() {
|
|
189
244
|
const isActive = this.#context.isActive;
|
|
190
|
-
if (isActive) {
|
|
191
|
-
this.#drawBlurredBackground();
|
|
192
|
-
}
|
|
193
245
|
if (this.#displayObject) {
|
|
194
246
|
// Texture swaps are frequent in deterministic mode; update sprite textures
|
|
195
247
|
// in-place instead of rebuilding split/blur geometry each frame.
|
|
@@ -197,6 +249,9 @@ export class PixiSplitScreenDisplayObjectHook {
|
|
|
197
249
|
if (currentTexture && currentTexture !== this.#pixiTexture) {
|
|
198
250
|
this.#swapDisplayTexture(currentTexture);
|
|
199
251
|
}
|
|
252
|
+
if (isActive && this.#bgCanvas) {
|
|
253
|
+
this.#drawBlurredBackground(this.#blurStrength);
|
|
254
|
+
}
|
|
200
255
|
// Always re-assert the resource in case the context was cleared or updated
|
|
201
256
|
this.#context.setResource('pixiRenderObject', this.#displayObject);
|
|
202
257
|
if (this.#displayObject.visible != isActive) {
|
|
@@ -244,6 +299,8 @@ export class PixiSplitScreenDisplayObjectHook {
|
|
|
244
299
|
async #handleDestroy() {
|
|
245
300
|
// remove event listeners from video
|
|
246
301
|
this.#bgCanvas = undefined;
|
|
302
|
+
this.#bgSprite = undefined;
|
|
303
|
+
this.#lastBlurFrameKey = '';
|
|
247
304
|
}
|
|
248
305
|
async handle(type, context) {
|
|
249
306
|
this.#context = context;
|
|
@@ -57,8 +57,8 @@ export class Layer {
|
|
|
57
57
|
if (!this.#displayObject) {
|
|
58
58
|
return false;
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
let componentDisplayIndex = 0;
|
|
61
|
+
for (const component of this.components) {
|
|
62
62
|
const displayObject = component.displayObject;
|
|
63
63
|
if (!displayObject) {
|
|
64
64
|
continue;
|
|
@@ -68,16 +68,51 @@ export class Layer {
|
|
|
68
68
|
if (parent && typeof parent.removeChild === 'function') {
|
|
69
69
|
parent.removeChild(displayObject);
|
|
70
70
|
}
|
|
71
|
-
this.#displayObject.
|
|
71
|
+
const childCount = this.#displayObject.children?.length ?? 0;
|
|
72
|
+
const insertIndex = Math.max(0, Math.min(componentDisplayIndex, childCount));
|
|
73
|
+
if (typeof this.#displayObject.addChildAt === 'function') {
|
|
74
|
+
this.#displayObject.addChildAt(displayObject, insertIndex);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
this.#displayObject.addChild(displayObject);
|
|
78
|
+
}
|
|
72
79
|
changed = true;
|
|
73
80
|
}
|
|
74
81
|
if (typeof this.#displayObject.getChildIndex === 'function' &&
|
|
75
82
|
typeof this.#displayObject.setChildIndex === 'function') {
|
|
83
|
+
const childCount = this.#displayObject.children?.length ?? 0;
|
|
84
|
+
if (childCount > 0) {
|
|
85
|
+
const currentIndex = this.#displayObject.getChildIndex(displayObject);
|
|
86
|
+
const targetIndex = Math.max(0, Math.min(componentDisplayIndex, childCount - 1));
|
|
87
|
+
if (currentIndex >= 0 && currentIndex !== targetIndex) {
|
|
88
|
+
this.#displayObject.setChildIndex(displayObject, targetIndex);
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
componentDisplayIndex += 1;
|
|
94
|
+
}
|
|
95
|
+
// Ensure deterministic ordering among only component display objects without
|
|
96
|
+
// assuming a 1:1 mapping with all container children.
|
|
97
|
+
if (typeof this.#displayObject.getChildIndex === 'function' &&
|
|
98
|
+
typeof this.#displayObject.setChildIndex === 'function') {
|
|
99
|
+
let nextIndex = 0;
|
|
100
|
+
for (const component of this.components) {
|
|
101
|
+
const displayObject = component.displayObject;
|
|
102
|
+
if (!displayObject || displayObject.parent !== this.#displayObject) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const childCount = this.#displayObject.children?.length ?? 0;
|
|
106
|
+
if (childCount === 0) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
76
109
|
const currentIndex = this.#displayObject.getChildIndex(displayObject);
|
|
77
|
-
|
|
78
|
-
|
|
110
|
+
const targetIndex = Math.max(0, Math.min(nextIndex, childCount - 1));
|
|
111
|
+
if (currentIndex >= 0 && currentIndex !== targetIndex) {
|
|
112
|
+
this.#displayObject.setChildIndex(displayObject, targetIndex);
|
|
79
113
|
changed = true;
|
|
80
114
|
}
|
|
115
|
+
nextIndex += 1;
|
|
81
116
|
}
|
|
82
117
|
}
|
|
83
118
|
return changed;
|
|
@@ -199,11 +199,38 @@ export class ComponentsManager {
|
|
|
199
199
|
const displayObject = layer.displayObject;
|
|
200
200
|
if (!displayObject)
|
|
201
201
|
return;
|
|
202
|
-
// Sync displayObject children order with components array
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
// Sync displayObject children order with components array while clamping indexes.
|
|
203
|
+
let nextIndex = 0;
|
|
204
|
+
layer.components.forEach((component) => {
|
|
205
|
+
const componentDisplayObject = component.displayObject;
|
|
206
|
+
if (!componentDisplayObject) {
|
|
207
|
+
return;
|
|
206
208
|
}
|
|
209
|
+
const parent = componentDisplayObject.parent;
|
|
210
|
+
if (parent !== displayObject) {
|
|
211
|
+
if (parent && typeof parent.removeChild === 'function') {
|
|
212
|
+
parent.removeChild(componentDisplayObject);
|
|
213
|
+
}
|
|
214
|
+
const insertMax = displayObject.children?.length ?? 0;
|
|
215
|
+
const insertIndex = Math.max(0, Math.min(nextIndex, insertMax));
|
|
216
|
+
if (typeof displayObject.addChildAt === 'function') {
|
|
217
|
+
displayObject.addChildAt(componentDisplayObject, insertIndex);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
displayObject.addChild(componentDisplayObject);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const childCount = displayObject.children?.length ?? 0;
|
|
224
|
+
if (childCount > 0 &&
|
|
225
|
+
typeof displayObject.getChildIndex === 'function' &&
|
|
226
|
+
typeof displayObject.setChildIndex === 'function') {
|
|
227
|
+
const currentIndex = displayObject.getChildIndex(componentDisplayObject);
|
|
228
|
+
const targetIndex = Math.max(0, Math.min(nextIndex, childCount - 1));
|
|
229
|
+
if (currentIndex >= 0 && currentIndex !== targetIndex) {
|
|
230
|
+
displayObject.setChildIndex(componentDisplayObject, targetIndex);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
nextIndex += 1;
|
|
207
234
|
});
|
|
208
235
|
}
|
|
209
236
|
bulkUpdate(updates) {
|
|
@@ -16,6 +16,9 @@ export declare class DeterministicMediaManager {
|
|
|
16
16
|
getFingerprint(): string;
|
|
17
17
|
setOneTimeOverride(componentId: string, frameIndex: number, payload: DeterministicFramePayload): void;
|
|
18
18
|
getDiagnosticsReport(): DeterministicDiagnosticsReport | null;
|
|
19
|
+
recordReadyAttempt(sceneFrameIndex: number, count?: number): void;
|
|
20
|
+
recordExtraRenderPass(sceneFrameIndex: number, count?: number): void;
|
|
21
|
+
recordBlurRedraw(sceneFrameIndex: number, count?: number): void;
|
|
19
22
|
destroy(): Promise<void>;
|
|
20
23
|
releaseComponent(componentId: string): Promise<void>;
|
|
21
24
|
}
|
|
@@ -4,6 +4,10 @@ const createDefaultDiagnosticsState = () => ({
|
|
|
4
4
|
providerHits: 0,
|
|
5
5
|
providerMisses: 0,
|
|
6
6
|
cacheHits: 0,
|
|
7
|
+
readyAttempts: 0,
|
|
8
|
+
extraRenderPasses: 0,
|
|
9
|
+
blurRedraws: 0,
|
|
10
|
+
perFrame: new Map(),
|
|
7
11
|
latencyCount: 0,
|
|
8
12
|
latencyTotalMs: 0,
|
|
9
13
|
latencyMinMs: Number.POSITIVE_INFINITY,
|
|
@@ -127,11 +131,23 @@ export class DeterministicMediaManager {
|
|
|
127
131
|
? this.#diagnostics.latencyTotalMs / this.#diagnostics.latencyCount
|
|
128
132
|
: 0;
|
|
129
133
|
const minLatency = this.#diagnostics.latencyCount > 0 ? this.#diagnostics.latencyMinMs : 0;
|
|
134
|
+
const perFrame = {};
|
|
135
|
+
for (const [frameIndex, counters] of [...this.#diagnostics.perFrame.entries()].sort(([a], [b]) => a - b)) {
|
|
136
|
+
perFrame[String(frameIndex)] = {
|
|
137
|
+
readyAttempts: counters.readyAttempts,
|
|
138
|
+
extraRenderPasses: counters.extraRenderPasses,
|
|
139
|
+
blurRedraws: counters.blurRedraws
|
|
140
|
+
};
|
|
141
|
+
}
|
|
130
142
|
return {
|
|
131
143
|
providerHits: this.#diagnostics.providerHits,
|
|
132
144
|
providerMisses: this.#diagnostics.providerMisses,
|
|
133
145
|
cacheHits: this.#diagnostics.cacheHits,
|
|
134
146
|
cacheHitRatio,
|
|
147
|
+
readyAttempts: this.#diagnostics.readyAttempts,
|
|
148
|
+
extraRenderPasses: this.#diagnostics.extraRenderPasses,
|
|
149
|
+
blurRedraws: this.#diagnostics.blurRedraws,
|
|
150
|
+
perFrame,
|
|
135
151
|
latency: {
|
|
136
152
|
minMs: minLatency,
|
|
137
153
|
maxMs: this.#diagnostics.latencyMaxMs,
|
|
@@ -139,6 +155,15 @@ export class DeterministicMediaManager {
|
|
|
139
155
|
}
|
|
140
156
|
};
|
|
141
157
|
}
|
|
158
|
+
recordReadyAttempt(sceneFrameIndex, count = 1) {
|
|
159
|
+
this.#recordRuntimeCounter(sceneFrameIndex, 'readyAttempts', count);
|
|
160
|
+
}
|
|
161
|
+
recordExtraRenderPass(sceneFrameIndex, count = 1) {
|
|
162
|
+
this.#recordRuntimeCounter(sceneFrameIndex, 'extraRenderPasses', count);
|
|
163
|
+
}
|
|
164
|
+
recordBlurRedraw(sceneFrameIndex, count = 1) {
|
|
165
|
+
this.#recordRuntimeCounter(sceneFrameIndex, 'blurRedraws', count);
|
|
166
|
+
}
|
|
142
167
|
async destroy() {
|
|
143
168
|
for (const [cacheKey, cached] of this.#cacheByCacheKey.entries()) {
|
|
144
169
|
this.#disposeCachedOverride(cacheKey, cached);
|
|
@@ -314,6 +339,27 @@ export class DeterministicMediaManager {
|
|
|
314
339
|
// Diagnostics are best-effort and never allowed to fail the render path.
|
|
315
340
|
}
|
|
316
341
|
}
|
|
342
|
+
#recordRuntimeCounter(sceneFrameIndex, counter, count) {
|
|
343
|
+
if (!this.config.diagnostics) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (!Number.isFinite(count) || count <= 0) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
this.#diagnostics[counter] += count;
|
|
351
|
+
const existing = this.#diagnostics.perFrame.get(sceneFrameIndex) ?? {
|
|
352
|
+
readyAttempts: 0,
|
|
353
|
+
extraRenderPasses: 0,
|
|
354
|
+
blurRedraws: 0
|
|
355
|
+
};
|
|
356
|
+
existing[counter] += count;
|
|
357
|
+
this.#diagnostics.perFrame.set(sceneFrameIndex, existing);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Diagnostics are best-effort and never allowed to fail the render path.
|
|
361
|
+
}
|
|
362
|
+
}
|
|
317
363
|
#countSceneMediaComponents(sceneData) {
|
|
318
364
|
return sceneData.layers.reduce((count, layer) => {
|
|
319
365
|
const mediaCount = layer.components.filter((component) => component.type === 'VIDEO' || component.type === 'GIF').length;
|
|
@@ -35,6 +35,10 @@ export type DeterministicMediaConfig = {
|
|
|
35
35
|
strict: boolean;
|
|
36
36
|
diagnostics: boolean;
|
|
37
37
|
maxCachedTextures?: number;
|
|
38
|
+
seekMaxAttempts?: number;
|
|
39
|
+
loadingMaxAttempts?: number;
|
|
40
|
+
readyYieldMs?: number;
|
|
41
|
+
blurDownscale?: number;
|
|
38
42
|
provider?: DeterministicFrameProvider;
|
|
39
43
|
};
|
|
40
44
|
export declare const DeterministicMediaConfigShape: z.ZodPrefault<z.ZodObject<{
|
|
@@ -42,6 +46,10 @@ export declare const DeterministicMediaConfigShape: z.ZodPrefault<z.ZodObject<{
|
|
|
42
46
|
strict: z.ZodPrefault<z.ZodBoolean>;
|
|
43
47
|
diagnostics: z.ZodPrefault<z.ZodBoolean>;
|
|
44
48
|
maxCachedTextures: z.ZodOptional<z.ZodNumber>;
|
|
49
|
+
seekMaxAttempts: z.ZodPrefault<z.ZodNumber>;
|
|
50
|
+
loadingMaxAttempts: z.ZodPrefault<z.ZodNumber>;
|
|
51
|
+
readyYieldMs: z.ZodPrefault<z.ZodNumber>;
|
|
52
|
+
blurDownscale: z.ZodPrefault<z.ZodNumber>;
|
|
45
53
|
provider: z.ZodOptional<z.ZodCustom<DeterministicFrameProvider, DeterministicFrameProvider>>;
|
|
46
54
|
}, z.core.$strip>>;
|
|
47
55
|
export declare const defaultDeterministicMediaConfig: DeterministicMediaConfig;
|
|
@@ -55,6 +63,14 @@ export type DeterministicDiagnosticsReport = {
|
|
|
55
63
|
providerMisses: number;
|
|
56
64
|
cacheHits: number;
|
|
57
65
|
cacheHitRatio: number;
|
|
66
|
+
readyAttempts: number;
|
|
67
|
+
extraRenderPasses: number;
|
|
68
|
+
blurRedraws: number;
|
|
69
|
+
perFrame: Record<string, {
|
|
70
|
+
readyAttempts: number;
|
|
71
|
+
extraRenderPasses: number;
|
|
72
|
+
blurRedraws: number;
|
|
73
|
+
}>;
|
|
58
74
|
latency: {
|
|
59
75
|
minMs: number;
|
|
60
76
|
maxMs: number;
|
|
@@ -4,6 +4,10 @@ export const DeterministicMediaConfigShape = z.object({
|
|
|
4
4
|
strict: z.boolean().prefault(false),
|
|
5
5
|
diagnostics: z.boolean().prefault(false),
|
|
6
6
|
maxCachedTextures: z.number().int().positive().optional(),
|
|
7
|
+
seekMaxAttempts: z.number().int().positive().prefault(4),
|
|
8
|
+
loadingMaxAttempts: z.number().int().positive().prefault(2),
|
|
9
|
+
readyYieldMs: z.number().int().nonnegative().prefault(0),
|
|
10
|
+
blurDownscale: z.number().positive().max(1).prefault(0.33),
|
|
7
11
|
provider: z.custom().optional()
|
|
8
12
|
}).prefault({});
|
|
9
13
|
export const defaultDeterministicMediaConfig = DeterministicMediaConfigShape.parse(undefined);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "visualfries",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10115",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "ContentFries",
|
|
6
6
|
"repository": {
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
91
91
|
"test": "pnpm run test:unit -- --run",
|
|
92
92
|
"test:unit": "vitest",
|
|
93
|
+
"bench:deterministic": "pnpm vitest tests/perf/deterministic-server-render.perf.test.ts --run",
|
|
93
94
|
"lint": "eslint . && prettier --check .",
|
|
94
95
|
"format": "prettier --write .",
|
|
95
96
|
"knip": "knip",
|