visualfries 0.1.1098 → 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/PixiDisplayObjectHook.js +4 -0
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +28 -2
- package/dist/components/hooks/PixiTextureHook.js +35 -9
- package/dist/components/hooks/PixiVideoTextureHook.js +16 -0
- 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
|
+
}
|
|
@@ -79,6 +79,10 @@ export class PixiDisplayObjectHook {
|
|
|
79
79
|
// Regular shapes need texture to create sprite
|
|
80
80
|
const texture = this.#context.getResource('pixiTexture');
|
|
81
81
|
if (!texture) {
|
|
82
|
+
const type = this.#context.contextData.type;
|
|
83
|
+
if (type === 'VIDEO' || type === 'GIF') {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
82
86
|
throw new Error('pixiTexture not found in resources.');
|
|
83
87
|
}
|
|
84
88
|
this.#pixiTexture = texture;
|
|
@@ -164,16 +164,38 @@ export class PixiSplitScreenDisplayObjectHook {
|
|
|
164
164
|
}
|
|
165
165
|
this.initMainSprite();
|
|
166
166
|
}
|
|
167
|
+
#swapDisplayTexture(nextTexture) {
|
|
168
|
+
if (!this.#displayObject) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const previousTexture = this.#pixiTexture;
|
|
172
|
+
const walk = (node) => {
|
|
173
|
+
const children = node.children;
|
|
174
|
+
if (!Array.isArray(children)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const child of children) {
|
|
178
|
+
const spriteLike = child;
|
|
179
|
+
if (spriteLike.texture && spriteLike.texture === previousTexture) {
|
|
180
|
+
spriteLike.texture = nextTexture;
|
|
181
|
+
}
|
|
182
|
+
walk(child);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
walk(this.#displayObject);
|
|
186
|
+
this.#pixiTexture = nextTexture;
|
|
187
|
+
}
|
|
167
188
|
async #handleUpdate() {
|
|
168
189
|
const isActive = this.#context.isActive;
|
|
169
190
|
if (isActive) {
|
|
170
191
|
this.#drawBlurredBackground();
|
|
171
192
|
}
|
|
172
193
|
if (this.#displayObject) {
|
|
173
|
-
//
|
|
194
|
+
// Texture swaps are frequent in deterministic mode; update sprite textures
|
|
195
|
+
// in-place instead of rebuilding split/blur geometry each frame.
|
|
174
196
|
const currentTexture = this.#context.getResource('pixiTexture');
|
|
175
197
|
if (currentTexture && currentTexture !== this.#pixiTexture) {
|
|
176
|
-
|
|
198
|
+
this.#swapDisplayTexture(currentTexture);
|
|
177
199
|
}
|
|
178
200
|
// Always re-assert the resource in case the context was cleared or updated
|
|
179
201
|
this.#context.setResource('pixiRenderObject', this.#displayObject);
|
|
@@ -184,6 +206,10 @@ export class PixiSplitScreenDisplayObjectHook {
|
|
|
184
206
|
}
|
|
185
207
|
const texture = this.#context.getResource('pixiTexture');
|
|
186
208
|
if (!texture) {
|
|
209
|
+
const type = this.#context.contextData.type;
|
|
210
|
+
if (type === 'VIDEO' || type === 'GIF') {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
187
213
|
throw new Error('pixiTexture not found in resources.');
|
|
188
214
|
}
|
|
189
215
|
this.#pixiTexture = texture;
|