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.
- package/dist/DIContainer.js +8 -0
- package/dist/SceneBuilder.svelte.d.ts +9 -0
- package/dist/SceneBuilder.svelte.js +76 -0
- package/dist/builders/PixiComponentBuilder.d.ts +3 -0
- package/dist/builders/PixiComponentBuilder.js +6 -0
- package/dist/builders/_ComponentState.svelte.d.ts +1 -1
- package/dist/commands/RenderFrameCommand.d.ts +4 -0
- package/dist/commands/RenderFrameCommand.js +7 -1
- package/dist/commands/ReplaceSourceOnTimeCommand.d.ts +9 -0
- package/dist/commands/ReplaceSourceOnTimeCommand.js +40 -7
- package/dist/components/Component.svelte.d.ts +1 -1
- package/dist/components/ComponentContext.svelte.d.ts +1 -1
- package/dist/components/hooks/DeterministicMediaFrameHook.d.ts +13 -0
- package/dist/components/hooks/DeterministicMediaFrameHook.js +89 -0
- package/dist/components/hooks/ImageHook.js +66 -10
- package/dist/components/hooks/PixiDisplayObjectHook.js +15 -3
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.d.ts +2 -2
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +35 -3
- package/dist/components/hooks/PixiTextureHook.js +78 -6
- package/dist/components/hooks/PixiVideoTextureHook.js +16 -0
- package/dist/components/hooks/VerifyImageHook.js +2 -2
- package/dist/directors/ComponentDirector.d.ts +4 -0
- package/dist/directors/ComponentDirector.js +14 -3
- package/dist/factories/SceneBuilderFactory.d.ts +2 -0
- package/dist/factories/SceneBuilderFactory.js +6 -2
- package/dist/layers/Layer.svelte.d.ts +1 -1
- package/dist/managers/DeterministicMediaManager.d.ts +21 -0
- package/dist/managers/DeterministicMediaManager.js +341 -0
- package/dist/managers/StateManager.svelte.d.ts +1 -1
- package/dist/schemas/runtime/deterministic.d.ts +94 -0
- package/dist/schemas/runtime/deterministic.js +21 -0
- package/dist/schemas/runtime/index.d.ts +2 -0
- package/dist/schemas/runtime/index.js +1 -1
- package/dist/schemas/runtime/types.d.ts +8 -1
- package/dist/schemas/scene/components.d.ts +2 -2
- package/dist/schemas/scene/core.d.ts +4 -4
- package/dist/schemas/scene/properties.d.ts +16 -16
- package/package.json +1 -1
package/dist/DIContainer.js
CHANGED
|
@@ -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(): "
|
|
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
|
-
|
|
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(): "
|
|
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(): "
|
|
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 #
|
|
14
|
-
if (this.#imageElement) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
16
|
+
async #loadImage(url) {
|
|
17
17
|
const img = new Image();
|
|
18
|
-
|
|
18
|
+
// crossOrigin must be set before src to ensure CORS mode is applied from the first request
|
|
19
19
|
img.crossOrigin = 'anonymous';
|
|
20
|
-
|
|
20
|
+
img.src = url;
|
|
21
21
|
await new Promise((resolve, reject) => {
|
|
22
22
|
img.onload = resolve;
|
|
23
|
-
img.onerror = (
|
|
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
|
-
|
|
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;
|