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.
@@ -403,7 +403,9 @@ export class SceneBuilder {
403
403
  framesSkipped += 1;
404
404
  }
405
405
  else {
406
- frame = await this.seekAndRenderFrame(frameTime, undefined, format, quality, imageOptions);
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 maxAttempts = 12;
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
- const pending = this.#getPendingDeterministicComponents();
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
- // Wait for fonts to be ready before rendering
89
- // This is critical for subtitle animations that use SplitText -
90
- // if fonts aren't loaded, text measurements will be wrong and
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
- // Try multiple render passes until loading state clears or attempts exhausted
104
- const maxAttempts = 10;
105
- for (let i = 0; i < maxAttempts; i += 1) {
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 (this.deterministicMediaManager.isEnabled()) {
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
- ctx.drawImage(sourceElement, 0, 0, this.#bgCanvas.width, this.#bgCanvas.height);
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
- for (let index = 0; index < this.components.length; index += 1) {
61
- const component = this.components[index];
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.addChild(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
- if (currentIndex !== index) {
78
- this.#displayObject.setChildIndex(displayObject, index);
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
- layer.components.forEach((component, index) => {
204
- if (component.displayObject) {
205
- displayObject.setChildIndex(component.displayObject, index);
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.10110",
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",