visualfries 0.1.1097 → 0.1.1099

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.
Files changed (38) hide show
  1. package/dist/DIContainer.js +8 -0
  2. package/dist/SceneBuilder.svelte.d.ts +9 -0
  3. package/dist/SceneBuilder.svelte.js +76 -0
  4. package/dist/builders/PixiComponentBuilder.d.ts +3 -0
  5. package/dist/builders/PixiComponentBuilder.js +6 -0
  6. package/dist/builders/_ComponentState.svelte.d.ts +1 -1
  7. package/dist/commands/RenderFrameCommand.d.ts +4 -0
  8. package/dist/commands/RenderFrameCommand.js +7 -1
  9. package/dist/commands/ReplaceSourceOnTimeCommand.d.ts +9 -0
  10. package/dist/commands/ReplaceSourceOnTimeCommand.js +40 -7
  11. package/dist/components/Component.svelte.d.ts +1 -1
  12. package/dist/components/ComponentContext.svelte.d.ts +1 -1
  13. package/dist/components/hooks/DeterministicMediaFrameHook.d.ts +13 -0
  14. package/dist/components/hooks/DeterministicMediaFrameHook.js +89 -0
  15. package/dist/components/hooks/ImageHook.js +66 -10
  16. package/dist/components/hooks/PixiDisplayObjectHook.js +15 -3
  17. package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.d.ts +2 -2
  18. package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +35 -3
  19. package/dist/components/hooks/PixiTextureHook.js +78 -6
  20. package/dist/components/hooks/PixiVideoTextureHook.js +16 -0
  21. package/dist/components/hooks/VerifyImageHook.js +2 -2
  22. package/dist/directors/ComponentDirector.d.ts +4 -0
  23. package/dist/directors/ComponentDirector.js +14 -3
  24. package/dist/factories/SceneBuilderFactory.d.ts +2 -0
  25. package/dist/factories/SceneBuilderFactory.js +6 -2
  26. package/dist/layers/Layer.svelte.d.ts +1 -1
  27. package/dist/managers/DeterministicMediaManager.d.ts +21 -0
  28. package/dist/managers/DeterministicMediaManager.js +341 -0
  29. package/dist/managers/StateManager.svelte.d.ts +1 -1
  30. package/dist/schemas/runtime/deterministic.d.ts +94 -0
  31. package/dist/schemas/runtime/deterministic.js +21 -0
  32. package/dist/schemas/runtime/index.d.ts +2 -0
  33. package/dist/schemas/runtime/index.js +1 -1
  34. package/dist/schemas/runtime/types.d.ts +8 -1
  35. package/dist/schemas/scene/components.d.ts +2 -2
  36. package/dist/schemas/scene/core.d.ts +4 -4
  37. package/dist/schemas/scene/properties.d.ts +16 -16
  38. package/package.json +1 -1
@@ -41,10 +41,12 @@ import { Layer } from './layers/Layer.svelte.js';
41
41
  import { LayersManager } from './managers/LayersManager.svelte.js';
42
42
  import { MediaSeekingHook } from './components/hooks/MediaSeekingHook.js';
43
43
  import { VerifyGifHook } from './components/hooks/VerifyGifHook.js';
44
+ import { DeterministicMediaFrameHook } from './components/hooks/DeterministicMediaFrameHook.js';
44
45
  import { ComponentAnimationTransformer } from './animations/transformers/AnimationReferenceTransformer.js';
45
46
  import { AnimationPresetsRegister } from './animations/AnimationPresetsRegister.js';
46
47
  import { SplitTextCache } from './animations/SplitTextCache.js';
47
48
  import { TimeManager } from './managers/TimeManager.svelte.js';
49
+ import { DeterministicMediaManager } from './managers/DeterministicMediaManager.js';
48
50
  const containers = new Map();
49
51
  export const registerNewContainer = function (data, instances) {
50
52
  if (containers.has(data.id)) {
@@ -75,6 +77,9 @@ export const registerNewContainer = function (data, instances) {
75
77
  renderManager: asClass(RenderManager, { lifetime: Lifetime.SINGLETON }),
76
78
  domManager: asClass(DomManager, { lifetime: Lifetime.SINGLETON }),
77
79
  mediaManager: asClass(MediaManager, { lifetime: Lifetime.SINGLETON }),
80
+ deterministicMediaManager: asClass(DeterministicMediaManager, {
81
+ lifetime: Lifetime.SINGLETON
82
+ }),
78
83
  sceneBuilder: asClass(SceneBuilder, { lifetime: Lifetime.SINGLETON }),
79
84
  layersManager: asClass(LayersManager, { lifetime: Lifetime.SINGLETON }),
80
85
  // transients
@@ -87,6 +92,9 @@ export const registerNewContainer = function (data, instances) {
87
92
  imageHook: asClass(ImageHook, { lifetime: Lifetime.TRANSIENT }),
88
93
  verifyImageHook: asClass(VerifyImageHook, { lifetime: Lifetime.TRANSIENT }),
89
94
  verifyGifHook: asClass(VerifyGifHook, { lifetime: Lifetime.TRANSIENT }),
95
+ deterministicMediaFrameHook: asClass(DeterministicMediaFrameHook, {
96
+ lifetime: Lifetime.TRANSIENT
97
+ }),
90
98
  videoTextureHook: asClass(PixiVideoTextureHook, { lifetime: Lifetime.TRANSIENT }),
91
99
  splitScreenHook: asClass(PixiSplitScreenDisplayObjectHook, { lifetime: Lifetime.TRANSIENT }),
92
100
  htmlTextHook: asClass(HtmlTextHook, { lifetime: Lifetime.TRANSIENT }),
@@ -8,7 +8,9 @@ import { DomManager } from './managers/DomManager.js';
8
8
  import { AppManager } from './managers/AppManager.svelte.js';
9
9
  import { ComponentsManager } from './managers/ComponentsManager.svelte.js';
10
10
  import type { EventMap, EventType, EventPayload, BuilderState, ISceneBuilder } from './';
11
+ import type { DeterministicFrameProvider, DeterministicMediaConfig, DeterministicDiagnosticsReport, RenderFrameRangeOptions, RenderFrameRangeSummary } from './';
11
12
  import { MediaManager } from './managers/MediaManager.js';
13
+ import { DeterministicMediaManager } from './managers/DeterministicMediaManager.js';
12
14
  import { LayersManager } from './managers/LayersManager.svelte.js';
13
15
  import { SubtitlesManager } from './managers/SubtitlesManager.svelte.js';
14
16
  import type { Component } from './components/Component.svelte.js';
@@ -24,6 +26,7 @@ export declare class SceneBuilder implements ISceneBuilder {
24
26
  private stateManager;
25
27
  private commandRunner;
26
28
  private mediaManager;
29
+ private deterministicMediaManager;
27
30
  private subtitlesManager;
28
31
  private fonts;
29
32
  constructor(cradle: {
@@ -36,6 +39,7 @@ export declare class SceneBuilder implements ISceneBuilder {
36
39
  stateManager: StateManager;
37
40
  commandRunner: CommandRunner;
38
41
  mediaManager: MediaManager;
42
+ deterministicMediaManager: DeterministicMediaManager;
39
43
  subtitlesManager: SubtitlesManager;
40
44
  fonts: FontType[];
41
45
  });
@@ -5369,6 +5373,10 @@ export declare class SceneBuilder implements ISceneBuilder {
5369
5373
  splitComponent(component: Component): Promise<boolean>;
5370
5374
  seek(time: number): Promise<void>;
5371
5375
  replaceSourceOnTime(time: number, componentId: string, base64data: string): Promise<void>;
5376
+ setDeterministicFrameProvider(provider: DeterministicFrameProvider | null): void;
5377
+ getDeterministicFrameProvider(): DeterministicFrameProvider | null;
5378
+ getDeterministicMediaConfig(): DeterministicMediaConfig;
5379
+ getDiagnosticsReport(): DeterministicDiagnosticsReport | null;
5372
5380
  seekAndRenderFrame(time: number, target?: PIXI.DisplayObject | PIXI.RenderTexture, format?: string, quality?: number): Promise<string | ArrayBuffer | Blob>;
5373
5381
  /**
5374
5382
  * Check if seeking to a specific time would result in visual changes
@@ -5395,6 +5403,7 @@ export declare class SceneBuilder implements ISceneBuilder {
5395
5403
  */
5396
5404
  isSceneDirty(time: number): Promise<boolean>;
5397
5405
  renderFrame(target?: PIXI.DisplayObject | PIXI.RenderTexture, format?: string, quality?: number): Promise<string | ArrayBuffer | Blob>;
5406
+ renderFrameRange(options: RenderFrameRangeOptions): Promise<RenderFrameRangeSummary>;
5398
5407
  log(message: string): void;
5399
5408
  play(changeState?: boolean): void;
5400
5409
  pause(changeState?: boolean): void;
@@ -13,6 +13,7 @@ import { AppManager } from './managers/AppManager.svelte.js';
13
13
  import { ComponentsManager } from './managers/ComponentsManager.svelte.js';
14
14
  import { v4 as uuidv4 } from 'uuid';
15
15
  import { MediaManager } from './managers/MediaManager.js';
16
+ import { DeterministicMediaManager } from './managers/DeterministicMediaManager.js';
16
17
  import { LayersManager } from './managers/LayersManager.svelte.js';
17
18
  import { SubtitlesManager } from './managers/SubtitlesManager.svelte.js';
18
19
  import { removeContainer } from './DIContainer.js';
@@ -28,6 +29,7 @@ export class SceneBuilder {
28
29
  stateManager;
29
30
  commandRunner;
30
31
  mediaManager;
32
+ deterministicMediaManager;
31
33
  subtitlesManager;
32
34
  fonts;
33
35
  // Replace constructor with cradle pattern
@@ -41,6 +43,7 @@ export class SceneBuilder {
41
43
  this.stateManager = cradle.stateManager;
42
44
  this.commandRunner = cradle.commandRunner;
43
45
  this.mediaManager = cradle.mediaManager;
46
+ this.deterministicMediaManager = cradle.deterministicMediaManager;
44
47
  this.subtitlesManager = cradle.subtitlesManager;
45
48
  this.fonts = cradle.fonts;
46
49
  // TODO - check scene is v2
@@ -295,6 +298,18 @@ export class SceneBuilder {
295
298
  base64data
296
299
  });
297
300
  }
301
+ setDeterministicFrameProvider(provider) {
302
+ this.deterministicMediaManager.setProvider(provider);
303
+ }
304
+ getDeterministicFrameProvider() {
305
+ return this.deterministicMediaManager.getProvider();
306
+ }
307
+ getDeterministicMediaConfig() {
308
+ return this.deterministicMediaManager.config;
309
+ }
310
+ getDiagnosticsReport() {
311
+ return this.deterministicMediaManager.getDiagnosticsReport();
312
+ }
298
313
  async seekAndRenderFrame(time, target, format = 'png', quality = 1) {
299
314
  await this.seek(time);
300
315
  // Ensure scene is rendered after seek so media hooks apply their updates
@@ -345,6 +360,66 @@ export class SceneBuilder {
345
360
  }
346
361
  return frame;
347
362
  }
363
+ async renderFrameRange(options) {
364
+ if (this.environment !== 'server') {
365
+ throw new Error('renderFrameRange is only available in server environment');
366
+ }
367
+ const format = options.format ?? 'blob';
368
+ const quality = options.quality ?? 1;
369
+ const skipDuplicates = options.skipDuplicates ?? false;
370
+ const fromFrame = Math.max(0, Math.floor(options.fromFrame));
371
+ const toFrame = Math.max(fromFrame, Math.floor(options.toFrame));
372
+ let framesRendered = 0;
373
+ let framesSkipped = 0;
374
+ let aborted = false;
375
+ let previousFrame = null;
376
+ for (let frameIndex = fromFrame; frameIndex < toFrame; frameIndex += 1) {
377
+ if (options.signal?.aborted) {
378
+ aborted = true;
379
+ break;
380
+ }
381
+ let frame;
382
+ let isDuplicate = false;
383
+ const frameTime = frameIndex / this.fps;
384
+ if (skipDuplicates) {
385
+ const isDirty = await this.isSceneDirty(frameTime);
386
+ if (!isDirty && previousFrame) {
387
+ frame = previousFrame;
388
+ isDuplicate = true;
389
+ framesSkipped += 1;
390
+ }
391
+ else {
392
+ frame = await this.seekAndRenderFrame(frameTime, undefined, format, quality);
393
+ previousFrame = frame;
394
+ }
395
+ }
396
+ else {
397
+ frame = await this.seekAndRenderFrame(frameTime, undefined, format, quality);
398
+ previousFrame = frame;
399
+ }
400
+ let released = false;
401
+ const release = () => {
402
+ if (released) {
403
+ return;
404
+ }
405
+ released = true;
406
+ };
407
+ await options.onFrame({
408
+ frameIndex,
409
+ frame,
410
+ isDuplicate,
411
+ release
412
+ });
413
+ release();
414
+ framesRendered += 1;
415
+ }
416
+ return {
417
+ framesRendered,
418
+ framesSkipped,
419
+ aborted,
420
+ diagnostics: this.getDiagnosticsReport()
421
+ };
422
+ }
348
423
  log(message) {
349
424
  $effect.root(function () {
350
425
  $inspect(message);
@@ -401,6 +476,7 @@ export class SceneBuilder {
401
476
  this.componentsManager.destroy();
402
477
  // media manages should be destroyed last
403
478
  this.mediaManager.destroy();
479
+ void this.deterministicMediaManager.destroy();
404
480
  // Remove the container from the DI container cache
405
481
  removeContainer(this.sceneData.id);
406
482
  }
@@ -20,6 +20,7 @@ type PixiComponentCradle = {
20
20
  pixiProgressShapeHook: ComponentHook;
21
21
  htmlToCanvasHook: ComponentHook;
22
22
  mediaSeekingHook: ComponentHook;
23
+ deterministicMediaFrameHook: ComponentHook;
23
24
  };
24
25
  export declare class PixiComponentBuilder implements IComponentBuilder {
25
26
  private component;
@@ -41,11 +42,13 @@ export declare class PixiComponentBuilder implements IComponentBuilder {
41
42
  private htmlToCanvasHook;
42
43
  private animationHook;
43
44
  private mediaSeekingHook;
45
+ private deterministicMediaFrameHook;
44
46
  constructor(cradle: PixiComponentCradle);
45
47
  withCanvasShape(): this;
46
48
  withProgressShape(): this;
47
49
  withMedia(): this;
48
50
  withMediaSeeking(): this;
51
+ withDeterministicMedia(): this;
49
52
  withImage(): this;
50
53
  withTexture(): this;
51
54
  withDisplayObject(): this;
@@ -19,6 +19,7 @@ export class PixiComponentBuilder {
19
19
  htmlToCanvasHook;
20
20
  animationHook;
21
21
  mediaSeekingHook;
22
+ deterministicMediaFrameHook;
22
23
  constructor(cradle) {
23
24
  this.component = cradle.component;
24
25
  this.mediaHook = cradle.mediaHook;
@@ -39,6 +40,7 @@ export class PixiComponentBuilder {
39
40
  this.pixiProgressShapeHook = cradle.pixiProgressShapeHook;
40
41
  this.htmlToCanvasHook = cradle.htmlToCanvasHook;
41
42
  this.mediaSeekingHook = cradle.mediaSeekingHook;
43
+ this.deterministicMediaFrameHook = cradle.deterministicMediaFrameHook;
42
44
  }
43
45
  withCanvasShape() {
44
46
  this.component.addHook(this.canvasShapeHook);
@@ -57,6 +59,10 @@ export class PixiComponentBuilder {
57
59
  this.component.addHook(this.mediaSeekingHook);
58
60
  return this;
59
61
  }
62
+ withDeterministicMedia() {
63
+ this.component.addHook(this.deterministicMediaFrameHook, 4);
64
+ return this;
65
+ }
60
66
  withImage() {
61
67
  this.component.addHook(this.verifyImageHook);
62
68
  this.component.addHook(this.imageHook);
@@ -15,7 +15,7 @@ export declare class ComponentState implements ComponentProps {
15
15
  setRefreshCallback(callback: () => Promise<void>): void;
16
16
  private maybeAutoRefresh;
17
17
  get id(): string;
18
- get type(): "IMAGE" | "GIF" | "VIDEO" | "TEXT" | "SHAPE" | "AUDIO" | "COLOR" | "GRADIENT" | "SUBTITLES";
18
+ get type(): "VIDEO" | "GIF" | "IMAGE" | "TEXT" | "SHAPE" | "AUDIO" | "COLOR" | "GRADIENT" | "SUBTITLES";
19
19
  get name(): string;
20
20
  get start_at(): number;
21
21
  get end_at(): number;
@@ -2,16 +2,20 @@ import type { Command } from './Command.js';
2
2
  import { StateManager } from '../managers/StateManager.svelte.js';
3
3
  import { DomManager } from '../managers/DomManager.js';
4
4
  import { AppManager } from '../managers/AppManager.svelte.js';
5
+ import { DeterministicMediaManager } from '../managers/DeterministicMediaManager.js';
5
6
  export declare class RenderFrameCommand implements Command<string | ArrayBuffer | Blob | null> {
6
7
  private sceneState;
7
8
  private domManager;
8
9
  private appManager;
9
10
  private lastRenderedFrame;
10
11
  private lastRenderArgs;
12
+ private deterministicMediaManager?;
13
+ private lastDeterministicFingerprint;
11
14
  constructor(cradle: {
12
15
  stateManager: StateManager;
13
16
  domManager: DomManager;
14
17
  appManager: AppManager;
18
+ deterministicMediaManager?: DeterministicMediaManager;
15
19
  });
16
20
  execute(args: unknown): Promise<string | ArrayBuffer | Blob | null>;
17
21
  }
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { StateManager } from '../managers/StateManager.svelte.js';
3
3
  import { DomManager } from '../managers/DomManager.js';
4
4
  import { AppManager } from '../managers/AppManager.svelte.js';
5
+ import { DeterministicMediaManager } from '../managers/DeterministicMediaManager.js';
5
6
  const replaceSourceOnTimeSchema = z.object({
6
7
  format: z.enum(['arraybuffer', 'blob', 'png', 'jpg', 'jpeg']),
7
8
  quality: z.number().min(0).max(1),
@@ -13,10 +14,13 @@ export class RenderFrameCommand {
13
14
  appManager;
14
15
  lastRenderedFrame = null;
15
16
  lastRenderArgs = null;
17
+ deterministicMediaManager;
18
+ lastDeterministicFingerprint = '';
16
19
  constructor(cradle) {
17
20
  this.sceneState = cradle.stateManager;
18
21
  this.domManager = cradle.domManager;
19
22
  this.appManager = cradle.appManager;
23
+ this.deterministicMediaManager = cradle.deterministicMediaManager;
20
24
  }
21
25
  async execute(args) {
22
26
  const check = replaceSourceOnTimeSchema.safeParse(args);
@@ -24,6 +28,7 @@ export class RenderFrameCommand {
24
28
  return null;
25
29
  }
26
30
  const { format, quality, target } = check.data;
31
+ const currentDeterministicFingerprint = this.deterministicMediaManager?.isEnabled() ? this.deterministicMediaManager.getFingerprint() : '';
27
32
  // Server optimization: Return cached frame if nothing changed visually and render args match
28
33
  if (this.sceneState.environment === 'server' && !this.sceneState.isDirty) {
29
34
  if (this.lastRenderedFrame && this.lastRenderArgs) {
@@ -31,7 +36,7 @@ export class RenderFrameCommand {
31
36
  const argsMatch = this.lastRenderArgs.format === format &&
32
37
  this.lastRenderArgs.quality === quality &&
33
38
  this.lastRenderArgs.target === target;
34
- if (argsMatch) {
39
+ if (argsMatch && this.lastDeterministicFingerprint === currentDeterministicFingerprint) {
35
40
  return this.lastRenderedFrame;
36
41
  }
37
42
  }
@@ -86,6 +91,7 @@ export class RenderFrameCommand {
86
91
  if (this.sceneState.environment === 'server') {
87
92
  this.lastRenderedFrame = frame;
88
93
  this.lastRenderArgs = { format, quality, target };
94
+ this.lastDeterministicFingerprint = currentDeterministicFingerprint;
89
95
  this.sceneState.clearDirty();
90
96
  }
91
97
  return frame;
@@ -1,4 +1,13 @@
1
1
  import type { Command } from './Command.js';
2
+ import { StateManager } from '../managers/StateManager.svelte.js';
3
+ import { DeterministicMediaManager } from '../managers/DeterministicMediaManager.js';
2
4
  export declare class ReplaceSourceOnTimeCommand implements Command<void> {
5
+ #private;
6
+ private stateManager;
7
+ private deterministicMediaManager;
8
+ constructor(cradle: {
9
+ stateManager: StateManager;
10
+ deterministicMediaManager: DeterministicMediaManager;
11
+ });
3
12
  execute(args: unknown): Promise<void>;
4
13
  }
@@ -1,22 +1,55 @@
1
1
  import { z } from 'zod';
2
+ import { StateManager } from '../managers/StateManager.svelte.js';
3
+ import { DeterministicMediaManager } from '../managers/DeterministicMediaManager.js';
2
4
  const replaceSourceOnTimeSchema = z.object({
3
5
  componentId: z.string(),
4
6
  base64data: z.string(),
5
7
  time: z.number()
6
8
  });
7
9
  export class ReplaceSourceOnTimeCommand {
10
+ stateManager;
11
+ deterministicMediaManager;
12
+ constructor(cradle) {
13
+ this.stateManager = cradle.stateManager;
14
+ this.deterministicMediaManager = cradle.deterministicMediaManager;
15
+ }
8
16
  async execute(args) {
9
- // TODO: Complete implementation - this is work in progress
10
- // if (this.sceneBuilder.environment != 'server') {
11
- // this.sceneBuilder.log('replaceSource is only available in server environment');
12
- // return;
13
- // }
14
17
  const check = replaceSourceOnTimeSchema.safeParse(args);
15
18
  if (!check.success) {
16
- // this.sceneBuilder.log('ReplaceSourceOnTimeCommand failed with error: ' + check.error);
17
19
  return;
18
20
  }
19
- // WIP: Need to implement the actual source replacement logic
21
+ const { componentId, base64data, time } = check.data;
22
+ if (!this.deterministicMediaManager.isEnabled()) {
23
+ return;
24
+ }
25
+ const frameIndex = Math.max(0, Math.round(time * (this.stateManager.data.settings.fps || 30)));
26
+ const blob = this.#base64ToBlob(base64data);
27
+ if (!blob) {
28
+ return;
29
+ }
30
+ const cacheKey = `replace:${componentId}:${frameIndex}:${base64data.length}`;
31
+ this.deterministicMediaManager.setOneTimeOverride(componentId, frameIndex, {
32
+ kind: 'blob',
33
+ cacheKey,
34
+ blob
35
+ });
36
+ this.stateManager.markDirty();
20
37
  return;
21
38
  }
39
+ #base64ToBlob(base64Data) {
40
+ try {
41
+ const [header, encoded] = base64Data.split(',', 2);
42
+ const mimeMatch = /data:(.*?);base64/.exec(header ?? '');
43
+ const mimeType = mimeMatch?.[1] || 'image/png';
44
+ const binary = atob(encoded ?? base64Data);
45
+ const bytes = new Uint8Array(binary.length);
46
+ for (let index = 0; index < binary.length; index += 1) {
47
+ bytes[index] = binary.charCodeAt(index);
48
+ }
49
+ return new Blob([bytes], { type: mimeType });
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
22
55
  }
@@ -8,7 +8,7 @@ export declare class Component implements IComponent {
8
8
  componentContext: ComponentContext;
9
9
  });
10
10
  get id(): string;
11
- get type(): "IMAGE" | "GIF" | "VIDEO" | "TEXT" | "SHAPE" | "AUDIO" | "COLOR" | "GRADIENT" | "SUBTITLES";
11
+ get type(): "VIDEO" | "GIF" | "IMAGE" | "TEXT" | "SHAPE" | "AUDIO" | "COLOR" | "GRADIENT" | "SUBTITLES";
12
12
  get props(): ComponentProps;
13
13
  get displayObject(): import("pixi.js-legacy").Container<import("pixi.js-legacy").DisplayObject> | undefined;
14
14
  get context(): IComponentContext;
@@ -16,7 +16,7 @@ export declare class ComponentContext implements IComponentContext {
16
16
  get contextData(): ComponentData;
17
17
  get data(): ComponentData;
18
18
  get id(): string;
19
- get type(): "IMAGE" | "GIF" | "VIDEO" | "TEXT" | "SHAPE" | "AUDIO" | "COLOR" | "GRADIENT" | "SUBTITLES";
19
+ get type(): "VIDEO" | "GIF" | "IMAGE" | "TEXT" | "SHAPE" | "AUDIO" | "COLOR" | "GRADIENT" | "SUBTITLES";
20
20
  get isActive(): boolean;
21
21
  get progress(): number;
22
22
  get currentComponentTime(): number;
@@ -0,0 +1,13 @@
1
+ import type { HookType, IComponentContext, IComponentHook } from '../..';
2
+ import { DeterministicMediaManager } from '../../managers/DeterministicMediaManager.js';
3
+ import { StateManager } from '../../managers/StateManager.svelte.js';
4
+ export declare class DeterministicMediaFrameHook implements IComponentHook {
5
+ #private;
6
+ types: HookType[];
7
+ priority: number;
8
+ constructor(cradle: {
9
+ stateManager: StateManager;
10
+ deterministicMediaManager: DeterministicMediaManager;
11
+ });
12
+ handle(type: HookType, context: IComponentContext): Promise<void>;
13
+ }
@@ -0,0 +1,89 @@
1
+ import { DeterministicRenderError } from '../../schemas/runtime/deterministic.js';
2
+ import { DeterministicMediaManager } from '../../managers/DeterministicMediaManager.js';
3
+ import { StateManager } from '../../managers/StateManager.svelte.js';
4
+ export class DeterministicMediaFrameHook {
5
+ types = ['setup', 'update', 'destroy'];
6
+ priority = 4;
7
+ #context;
8
+ #state;
9
+ #manager;
10
+ constructor(cradle) {
11
+ this.#state = cradle.stateManager;
12
+ this.#manager = cradle.deterministicMediaManager;
13
+ }
14
+ #clearDeterministicResources(componentId) {
15
+ const cacheKeyChanged = this.#manager.clearCacheKey(componentId);
16
+ this.#context.removeResource('pixiResource');
17
+ this.#context.removeResource('imageElement');
18
+ return cacheKeyChanged;
19
+ }
20
+ async #handleUpdate() {
21
+ if (!this.#manager.isEnabled() || this.#state.environment !== 'server') {
22
+ return;
23
+ }
24
+ const fps = this.#state.data.settings.fps || 30;
25
+ const data = this.#context.contextData;
26
+ if (!this.#context.isActive) {
27
+ const cacheKeyChanged = this.#clearDeterministicResources(data.id);
28
+ if (cacheKeyChanged) {
29
+ this.#state.markDirty();
30
+ }
31
+ return;
32
+ }
33
+ const frameIndex = Math.max(0, Math.round(this.#context.currentComponentTime * fps));
34
+ const override = await this.#manager.resolveOverride({
35
+ componentId: data.id,
36
+ componentType: data.type === 'GIF' ? 'GIF' : 'VIDEO',
37
+ frameIndex,
38
+ fps,
39
+ width: this.#state.width,
40
+ height: this.#state.height
41
+ });
42
+ if (!override) {
43
+ const cacheKeyChanged = this.#clearDeterministicResources(data.id);
44
+ if (cacheKeyChanged) {
45
+ this.#state.markDirty();
46
+ }
47
+ if (this.#manager.config.strict && this.#manager.getProvider()) {
48
+ throw new DeterministicRenderError('Deterministic frame provider returned null', {
49
+ componentId: data.id,
50
+ frameIndex,
51
+ sceneTime: this.#state.currentTime
52
+ });
53
+ }
54
+ return;
55
+ }
56
+ this.#context.setResource('pixiResource', override.pixiResource);
57
+ if (override.imageElement) {
58
+ this.#context.setResource('imageElement', override.imageElement);
59
+ }
60
+ const cacheKeyChanged = this.#manager.commitCacheKey(data.id, override.cacheKey);
61
+ if (cacheKeyChanged) {
62
+ this.#state.markDirty();
63
+ }
64
+ }
65
+ async #handleDestroy() {
66
+ const data = this.#context.contextData;
67
+ this.#clearDeterministicResources(data.id);
68
+ try {
69
+ await this.#manager.releaseComponent(data.id);
70
+ }
71
+ catch {
72
+ // Best-effort cleanup only.
73
+ }
74
+ }
75
+ async handle(type, context) {
76
+ this.#context = context;
77
+ const data = this.#context.contextData;
78
+ if (data.type !== 'VIDEO' && data.type !== 'GIF') {
79
+ return;
80
+ }
81
+ if (type === 'update') {
82
+ await this.#handleUpdate();
83
+ return;
84
+ }
85
+ if (type === 'destroy') {
86
+ await this.#handleDestroy();
87
+ }
88
+ }
89
+ }
@@ -1,37 +1,93 @@
1
1
  import { ImageComponentShape } from '../..';
2
2
  import { z } from 'zod';
3
3
  export class ImageHook {
4
- types = ['setup', 'destroy'];
4
+ types = ['setup', 'refresh', 'refresh:content', 'destroy'];
5
5
  #handlers = {
6
6
  setup: this.#handleSetup.bind(this),
7
+ refresh: this.#handleRefreshContent.bind(this),
8
+ 'refresh:content': this.#handleRefreshContent.bind(this),
7
9
  destroy: this.#handleDestroy.bind(this)
8
10
  };
9
11
  priority = 1;
10
12
  #context;
11
13
  #imageElement;
14
+ #lastUrl;
12
15
  componentElement;
13
- async #handleSetup() {
14
- if (this.#imageElement) {
15
- return;
16
- }
16
+ async #loadImage(url) {
17
17
  const img = new Image();
18
- img.src = this.componentElement.source.url;
18
+ // crossOrigin must be set before src to ensure CORS mode is applied from the first request
19
19
  img.crossOrigin = 'anonymous';
20
- // Wait for the image to load
20
+ img.src = url;
21
21
  await new Promise((resolve, reject) => {
22
22
  img.onload = resolve;
23
- img.onerror = (error) => reject(new Error(`Failed to load image: ${this.componentElement.source.url}`));
23
+ img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
24
24
  });
25
+ return img;
26
+ }
27
+ #clearResources() {
28
+ if (this.#imageElement) {
29
+ this.#imageElement.remove();
30
+ this.#imageElement = undefined;
31
+ }
32
+ this.#lastUrl = undefined;
33
+ this.#context.removeResource('imageElement');
34
+ this.#context.removeResource('pixiResource');
35
+ }
36
+ #resolveUrl() {
37
+ const url = this.componentElement.source.url;
38
+ if (!url || typeof url !== 'string') {
39
+ // Clear stale resources before disabling so downstream hooks never see
40
+ // an old image/pixiResource paired with the now-invalid source URL.
41
+ this.#clearResources();
42
+ this.#context.disabled = true;
43
+ throw new Error(`ImageHook: source.url is missing or invalid (got ${JSON.stringify(url)})`);
44
+ }
45
+ return url;
46
+ }
47
+ async #handleSetup() {
48
+ const url = this.#resolveUrl();
49
+ if (this.#imageElement && this.#lastUrl === url) {
50
+ // Already loaded and URL unchanged — re-assert resources in case context was cleared
51
+ this.#context.setResource('imageElement', this.#imageElement);
52
+ this.#context.setResource('pixiResource', this.#imageElement);
53
+ return;
54
+ }
55
+ if (this.#imageElement) {
56
+ this.#clearResources();
57
+ }
58
+ const img = await this.#loadImage(url);
25
59
  this.#imageElement = img;
60
+ this.#lastUrl = url;
61
+ this.#context.setResource('imageElement', img);
62
+ this.#context.setResource('pixiResource', img);
63
+ }
64
+ async #handleRefreshContent() {
65
+ const url = this.#resolveUrl();
66
+ if (this.#lastUrl === url) {
67
+ // URL unchanged — re-assert resources in case context was cleared between frames
68
+ if (this.#imageElement) {
69
+ this.#context.setResource('imageElement', this.#imageElement);
70
+ this.#context.setResource('pixiResource', this.#imageElement);
71
+ }
72
+ return;
73
+ }
74
+ this.#clearResources();
75
+ const img = await this.#loadImage(url);
76
+ this.#imageElement = img;
77
+ this.#lastUrl = url;
26
78
  this.#context.setResource('imageElement', img);
27
79
  this.#context.setResource('pixiResource', img);
28
80
  }
29
81
  async #handleDestroy() {
30
- // remove event listeners from video
31
- this.#imageElement.remove();
82
+ this.#clearResources();
32
83
  }
33
84
  async handle(type, context) {
34
85
  this.#context = context;
86
+ // Destroy must run regardless of whether imageShape is present,
87
+ // so that stale resources are always cleaned up.
88
+ if (type === 'destroy') {
89
+ return await this.#handleDestroy();
90
+ }
35
91
  const data = this.#context.getResource('imageShape');
36
92
  if (!data) {
37
93
  return;