visualfries 0.1.10115 → 0.3.2
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/README.md +122 -0
- package/bin/visualfries.js +1570 -0
- package/dist/DIContainer.d.ts +2 -1
- package/dist/DIContainer.js +16 -5
- package/dist/SceneBuilder.svelte.d.ts +70 -1
- package/dist/SceneBuilder.svelte.js +96 -29
- package/dist/agent/audioMixer.d.ts +34 -0
- package/dist/agent/audioMixer.js +308 -0
- package/dist/agent/broll.d.ts +35 -0
- package/dist/agent/broll.js +226 -0
- package/dist/agent/capabilities.d.ts +34 -0
- package/dist/agent/capabilities.js +246 -0
- package/dist/agent/captionScene.d.ts +23 -0
- package/dist/agent/captionScene.js +204 -0
- package/dist/agent/catalog.d.ts +24 -0
- package/dist/agent/catalog.js +67 -0
- package/dist/agent/cueFiles.d.ts +54 -0
- package/dist/agent/cueFiles.js +219 -0
- package/dist/agent/cuePresets.d.ts +7 -0
- package/dist/agent/cuePresets.js +50 -0
- package/dist/agent/deterministicMedia.d.ts +61 -0
- package/dist/agent/deterministicMedia.js +303 -0
- package/dist/agent/index.d.ts +18 -0
- package/dist/agent/index.js +18 -0
- package/dist/agent/inspectScene.d.ts +36 -0
- package/dist/agent/inspectScene.js +139 -0
- package/dist/agent/localRender.d.ts +82 -0
- package/dist/agent/localRender.js +702 -0
- package/dist/agent/overlays.d.ts +40 -0
- package/dist/agent/overlays.js +325 -0
- package/dist/agent/productionPlan.d.ts +131 -0
- package/dist/agent/productionPlan.js +322 -0
- package/dist/agent/renderPlan.d.ts +27 -0
- package/dist/agent/renderPlan.js +68 -0
- package/dist/agent/renderRanges.d.ts +24 -0
- package/dist/agent/renderRanges.js +73 -0
- package/dist/agent/runtimeSupport.d.ts +19 -0
- package/dist/agent/runtimeSupport.js +192 -0
- package/dist/agent/streamingEncoder.d.ts +31 -0
- package/dist/agent/streamingEncoder.js +208 -0
- package/dist/agent/transcripts.d.ts +29 -0
- package/dist/agent/transcripts.js +146 -0
- package/dist/agent/transitions.d.ts +20 -0
- package/dist/agent/transitions.js +169 -0
- package/dist/animations/PixiAnimationTarget.d.ts +29 -0
- package/dist/animations/PixiAnimationTarget.js +63 -0
- package/dist/animations/builders/LineHighlighterAnimationBuilder.js +20 -3
- package/dist/animations/builders/WordHighlighterAnimationBuilder.js +15 -2
- package/dist/browser/browserExport.d.ts +73 -0
- package/dist/browser/browserExport.js +180 -0
- package/dist/browser/glTransitions.d.ts +37 -0
- package/dist/browser/glTransitions.js +261 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.js +3 -0
- package/dist/browser/mediaDecode.d.ts +31 -0
- package/dist/browser/mediaDecode.js +152 -0
- package/dist/builders/PixiComponentBuilder.d.ts +6 -0
- package/dist/builders/PixiComponentBuilder.js +12 -0
- package/dist/builders/_ComponentState.svelte.d.ts +7 -5
- package/dist/builders/_ComponentState.svelte.js +29 -15
- package/dist/builders/html/StyleBuilder.js +9 -8
- package/dist/builders/html/TextComponentHtmlBuilder.js +4 -2
- package/dist/builders/html/processors/AppearanceStyleProcessor.js +1 -1
- package/dist/builders/html/processors/TextAppearanceStyleProcessor.js +7 -0
- package/dist/builders/html/processors/TextEffectsStyleProcessor.js +32 -13
- package/dist/commands/ReplaceSourceOnTimeCommand.js +9 -1
- package/dist/commands/SeekCommand.js +19 -6
- package/dist/components/AnimatedGIF.d.ts +3 -0
- package/dist/components/AnimatedGIF.js +28 -1
- package/dist/components/Component.svelte.d.ts +5 -5
- package/dist/components/Component.svelte.js +29 -13
- package/dist/components/ComponentContext.svelte.js +68 -23
- package/dist/components/hooks/AnimationHook.js +1 -1
- package/dist/components/hooks/CanvasFillHook.d.ts +12 -0
- package/dist/components/hooks/CanvasFillHook.js +75 -0
- package/dist/components/hooks/DeterministicMediaFrameHook.js +3 -0
- package/dist/components/hooks/HtmlTextHook.js +11 -3
- package/dist/components/hooks/HtmlToCanvasHook.js +8 -25
- package/dist/components/hooks/MediaHook.js +172 -47
- package/dist/components/hooks/MediaSeekingHook.js +138 -88
- package/dist/components/hooks/PixiGifHook.js +4 -4
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.d.ts +3 -0
- package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +85 -8
- package/dist/components/hooks/PixiVideoTextureHook.js +10 -4
- package/dist/components/hooks/PixiVisualTransformHook.d.ts +16 -0
- package/dist/components/hooks/PixiVisualTransformHook.js +102 -0
- package/dist/components/hooks/SubtitlesHook.js +48 -24
- package/dist/components/hooks/VerifyMediaHook.js +2 -2
- package/dist/directors/ComponentDirector.d.ts +1 -0
- package/dist/directors/ComponentDirector.js +41 -8
- package/dist/factories/SceneBuilderFactory.d.ts +3 -0
- package/dist/factories/SceneBuilderFactory.js +6 -0
- package/dist/fonts/fontDiscovery.js +12 -1
- package/dist/layers/Layer.svelte.d.ts +54 -0
- package/dist/layers/Layer.svelte.js +2 -25
- package/dist/managers/AppManager.svelte.d.ts +12 -0
- package/dist/managers/AppManager.svelte.js +180 -8
- package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
- package/dist/managers/ComponentsManager.svelte.js +29 -15
- package/dist/managers/DeterministicMediaManager.d.ts +6 -0
- package/dist/managers/DeterministicMediaManager.js +57 -3
- package/dist/managers/EventManager.js +3 -1
- package/dist/managers/LayersManager.svelte.d.ts +54 -0
- package/dist/managers/RenderManager.d.ts +5 -0
- package/dist/managers/RenderManager.js +67 -21
- package/dist/managers/StateManager.svelte.d.ts +56 -0
- package/dist/managers/StateManager.svelte.js +60 -58
- package/dist/managers/SubtitlesManager.svelte.js +1 -3
- package/dist/managers/TimeManager.svelte.d.ts +4 -0
- package/dist/managers/TimeManager.svelte.js +23 -7
- package/dist/schemas/runtime/deterministic.d.ts +3 -0
- package/dist/schemas/runtime/deterministic.js +5 -1
- package/dist/schemas/runtime/types.d.ts +36 -15
- package/dist/schemas/scene/components.d.ts +126 -0
- package/dist/schemas/scene/components.js +11 -0
- package/dist/schemas/scene/core.d.ts +109 -10
- package/dist/schemas/scene/core.js +1 -1
- package/dist/utils/mediaWindow.d.ts +13 -0
- package/dist/utils/mediaWindow.js +25 -0
- package/dist/utils/svgGenerator.js +1 -1
- package/dist/utils/timelineWindow.d.ts +2 -0
- package/dist/utils/timelineWindow.js +7 -0
- package/dist/utils/utils.js +2 -4
- package/docs/AGENT_PATTERNS.md +243 -0
- package/docs/AGENT_WORKFLOW.md +364 -0
- package/docs/AUTHORING_BEST_PRACTICES.md +188 -0
- package/docs/AUTHORING_SYSTEM_AUDIT.md +98 -0
- package/docs/BROWSER_EXPORT_SPIKE.md +55 -0
- package/docs/DETERMINISTIC_RENDERER.md +83 -0
- package/docs/NEXT_RELEASE.md +28 -0
- package/docs/PROPOSED_VISUALFRIES_SKILL.md +68 -0
- package/docs/examples/agent-cues.json +49 -0
- package/docs/examples/agent-transcript.json +23 -0
- package/docs/examples/agent-transcript.srt +7 -0
- package/docs/examples/agent-transcript.vtt +7 -0
- package/docs/examples/image-animation.json +87 -0
- package/docs/examples/native-text-metric-badge.json +109 -0
- package/package.json +133 -100
- package/skills/visualfries/SKILL.md +328 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { computeXYAngle } from '../../utils/canvas.js';
|
|
2
|
+
/** Renders COLOR and GRADIENT schema components into a Pixi texture source. */
|
|
3
|
+
export class CanvasFillHook {
|
|
4
|
+
types = ['setup', 'update', 'refresh', 'destroy'];
|
|
5
|
+
priority = 1;
|
|
6
|
+
#context;
|
|
7
|
+
#canvas;
|
|
8
|
+
#state;
|
|
9
|
+
constructor(cradle) {
|
|
10
|
+
this.#state = cradle.stateManager;
|
|
11
|
+
}
|
|
12
|
+
#draw() {
|
|
13
|
+
if (!this.#canvas)
|
|
14
|
+
return;
|
|
15
|
+
const data = this.#context.data;
|
|
16
|
+
if (data.type !== 'COLOR' && data.type !== 'GRADIENT')
|
|
17
|
+
return;
|
|
18
|
+
const ctx = this.#canvas.getContext('2d');
|
|
19
|
+
if (!ctx)
|
|
20
|
+
return;
|
|
21
|
+
const { width, height } = data.appearance;
|
|
22
|
+
if (this.#canvas.width !== width || this.#canvas.height !== height) {
|
|
23
|
+
this.#canvas.width = width;
|
|
24
|
+
this.#canvas.height = height;
|
|
25
|
+
}
|
|
26
|
+
ctx.clearRect(0, 0, width, height);
|
|
27
|
+
if (data.type === 'COLOR') {
|
|
28
|
+
ctx.fillStyle = data.appearance.background;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const definition = data.appearance.background;
|
|
32
|
+
const gradient = definition.type === 'radial'
|
|
33
|
+
? ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, Math.max(width, height) / 2)
|
|
34
|
+
: (() => {
|
|
35
|
+
const { x1, y1, x2, y2 } = computeXYAngle(definition.angle ?? 180, width, height);
|
|
36
|
+
return ctx.createLinearGradient(x1, y1, x2, y2);
|
|
37
|
+
})();
|
|
38
|
+
definition.colors.forEach((color, index) => {
|
|
39
|
+
const stop = definition.stops?.[index] ?? (index / (definition.colors.length - 1)) * 100;
|
|
40
|
+
gradient.addColorStop(Math.max(0, Math.min(1, stop / 100)), color);
|
|
41
|
+
});
|
|
42
|
+
ctx.fillStyle = gradient;
|
|
43
|
+
}
|
|
44
|
+
ctx.fillRect(0, 0, width, height);
|
|
45
|
+
const texture = this.#context.getResource('pixiTexture');
|
|
46
|
+
texture?.baseTexture?.update?.();
|
|
47
|
+
texture?.update?.();
|
|
48
|
+
this.#state.markDirty();
|
|
49
|
+
}
|
|
50
|
+
async #setup() {
|
|
51
|
+
const data = this.#context.data;
|
|
52
|
+
if (data.type !== 'COLOR' && data.type !== 'GRADIENT')
|
|
53
|
+
return;
|
|
54
|
+
if (!this.#canvas)
|
|
55
|
+
this.#canvas = document.createElement('canvas');
|
|
56
|
+
this.#canvas.width = data.appearance.width;
|
|
57
|
+
this.#canvas.height = data.appearance.height;
|
|
58
|
+
this.#draw();
|
|
59
|
+
this.#context.setResource('pixiResource', this.#canvas);
|
|
60
|
+
}
|
|
61
|
+
async #destroy() {
|
|
62
|
+
this.#canvas?.remove();
|
|
63
|
+
this.#canvas = undefined;
|
|
64
|
+
this.#context.removeResource('pixiResource');
|
|
65
|
+
}
|
|
66
|
+
async handle(type, context) {
|
|
67
|
+
this.#context = context;
|
|
68
|
+
if (type === 'destroy')
|
|
69
|
+
return this.#destroy();
|
|
70
|
+
if (!this.#canvas || type === 'setup' || type === 'refresh')
|
|
71
|
+
await this.#setup();
|
|
72
|
+
else
|
|
73
|
+
this.#draw();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -57,6 +57,9 @@ export class DeterministicMediaFrameHook {
|
|
|
57
57
|
if (override.imageElement) {
|
|
58
58
|
this.#context.setResource('imageElement', override.imageElement);
|
|
59
59
|
}
|
|
60
|
+
else {
|
|
61
|
+
this.#context.removeResource('imageElement');
|
|
62
|
+
}
|
|
60
63
|
const cacheKeyChanged = this.#manager.commitCacheKey(data.id, override.cacheKey);
|
|
61
64
|
if (cacheKeyChanged) {
|
|
62
65
|
this.#state.markDirty();
|
|
@@ -39,6 +39,7 @@ export class HtmlTextHook {
|
|
|
39
39
|
if (this.#htmlEl) {
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
+
const componentData = this.#context.contextData;
|
|
42
43
|
this.#currentId = this.#context.contextData.id;
|
|
43
44
|
const comp = this.componentsManager.get(this.#context.contextData.id);
|
|
44
45
|
if (this.#context.type === 'TEXT' && comp) {
|
|
@@ -53,8 +54,15 @@ export class HtmlTextHook {
|
|
|
53
54
|
// Store references in the context for other hooks
|
|
54
55
|
this.#context.setResource('wrapperHtmlEl', this.#wrapperEl);
|
|
55
56
|
this.#context.setResource('htmlEl', this.#htmlEl);
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const background = componentData.type === 'TEXT' ? componentData.appearance.background : undefined;
|
|
58
|
+
const animateWrapper = background &&
|
|
59
|
+
typeof background === 'object' &&
|
|
60
|
+
'enabled' in background &&
|
|
61
|
+
background.enabled &&
|
|
62
|
+
background.target === 'wrapper';
|
|
63
|
+
// Card/background animations must target the wrapper. Animating only the text leaves the
|
|
64
|
+
// wrapper background fully opaque, which can create blank transition frames.
|
|
65
|
+
this.#context.setResource('animationTarget', animateWrapper ? this.#wrapperEl : this.#htmlEl);
|
|
58
66
|
// Add to DOM if not already present
|
|
59
67
|
const hasEl = this.domManager.htmlContainer.querySelector('#' + this.#wrapperEl.id);
|
|
60
68
|
if (!hasEl) {
|
|
@@ -65,7 +73,7 @@ export class HtmlTextHook {
|
|
|
65
73
|
if (this.#currentId !== this.#context.contextData.id) {
|
|
66
74
|
await this.#handleRefresh();
|
|
67
75
|
}
|
|
68
|
-
const isActive = this.#context.isActive;
|
|
76
|
+
const isActive = this.#context.isActive && this.#context.data.visible !== false;
|
|
69
77
|
if (this.#wrapperEl) {
|
|
70
78
|
if (isActive) {
|
|
71
79
|
this.#wrapperEl.style.display = 'flex';
|
|
@@ -67,30 +67,8 @@ export class HtmlToCanvasHook {
|
|
|
67
67
|
}
|
|
68
68
|
const { width, height } = this.state;
|
|
69
69
|
if (!this.svgBase) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const charsList = this.state.getCharactersList().join('');
|
|
74
|
-
encodedChars = encodeURIComponent(charsList);
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
// If encoding fails (e.g., unpaired surrogates), filter to only safe characters
|
|
78
|
-
console.warn('Failed to encode characters list, filtering to safe characters:', error);
|
|
79
|
-
const safeChars = this.state
|
|
80
|
-
.getCharactersList()
|
|
81
|
-
.filter((char) => {
|
|
82
|
-
try {
|
|
83
|
-
encodeURIComponent(char);
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
.join('');
|
|
91
|
-
encodedChars = encodeURIComponent(safeChars);
|
|
92
|
-
}
|
|
93
|
-
const { base, content, end } = await svgGenerator.generateSVG(this.#htmlEl, this.#context.data.appearance.text, width, height, 'svg-' + this.#context.contextData.id, encodedChars);
|
|
70
|
+
const { base, content, end } = await svgGenerator.generateSVG(this.#htmlEl, this.#context.data.appearance.text, width, height, 'svg-' + this.#context.contextData.id, encodeURIComponent(this.state.getCharactersList().join('')) // current component text encoded
|
|
71
|
+
);
|
|
94
72
|
this.svgBase = base;
|
|
95
73
|
this.svgEnd = end;
|
|
96
74
|
this.svg = base + content + end;
|
|
@@ -149,8 +127,13 @@ export class HtmlToCanvasHook {
|
|
|
149
127
|
if (this.svg && rerendered) {
|
|
150
128
|
const texture = await this.#svgToTexture(this.svg);
|
|
151
129
|
if (texture) {
|
|
152
|
-
this.#sprite.texture
|
|
130
|
+
const previousTexture = this.#sprite.texture;
|
|
153
131
|
this.#sprite.texture = texture;
|
|
132
|
+
// Texture.EMPTY is a shared Pixi singleton. Destroying it corrupts later
|
|
133
|
+
// text/subtitle sprite construction in sparse seek sequences.
|
|
134
|
+
if (previousTexture !== Texture.EMPTY && previousTexture !== Texture.WHITE) {
|
|
135
|
+
previousTexture.destroy(true);
|
|
136
|
+
}
|
|
154
137
|
}
|
|
155
138
|
}
|
|
156
139
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { MediaManager } from '../../managers/MediaManager.js';
|
|
2
|
-
import { z } from 'zod';
|
|
3
2
|
import { StateManager } from '../../managers/StateManager.svelte.js';
|
|
3
|
+
import { shouldPrepareMediaAtTime } from '../../utils/mediaWindow.js';
|
|
4
4
|
export class MediaHook {
|
|
5
|
-
types = ['setup', 'update', 'destroy'];
|
|
5
|
+
types = ['setup', 'update', 'destroy', 'refresh:content'];
|
|
6
6
|
priority = 1;
|
|
7
7
|
#context;
|
|
8
8
|
#mediaElement;
|
|
@@ -15,16 +15,49 @@ export class MediaHook {
|
|
|
15
15
|
#playRequested = false;
|
|
16
16
|
#lastControllerCheck = 0;
|
|
17
17
|
#CONTROLLER_CHECK_INTERVAL = 100; // ms
|
|
18
|
+
#seekVersion = 0;
|
|
19
|
+
#cleanupPendingSeek;
|
|
18
20
|
constructor(cradle) {
|
|
19
21
|
this.mediaManager = cradle.mediaManager;
|
|
20
22
|
this.state = cradle.stateManager;
|
|
21
23
|
}
|
|
22
|
-
|
|
24
|
+
#shouldPrepareMedia() {
|
|
25
|
+
return shouldPrepareMediaAtTime(this.#context.contextData, this.#context.sceneState.currentTime);
|
|
26
|
+
}
|
|
27
|
+
async #releaseMediaElement(markDestroyed = false) {
|
|
28
|
+
this.#seekVersion += 1;
|
|
29
|
+
this.#cleanupPendingSeek?.();
|
|
30
|
+
this.#cleanupPendingSeek = undefined;
|
|
31
|
+
const source = this.#context.contextData.source;
|
|
32
|
+
const mediaType = this.#context.type === 'VIDEO' ? 'video' : 'audio';
|
|
33
|
+
if (source?.url) {
|
|
34
|
+
const isController = this.mediaManager.getMediaController(source.url, mediaType) ===
|
|
35
|
+
this.#context.contextData.id;
|
|
36
|
+
if (isController) {
|
|
37
|
+
await this.#pause('releasing media element');
|
|
38
|
+
this.mediaManager.setMediaController(source.url, undefined, mediaType);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (this.#mediaElement) {
|
|
42
|
+
if (source?.url) {
|
|
43
|
+
this.mediaManager.releaseMediaElement(source.url, mediaType);
|
|
44
|
+
}
|
|
45
|
+
this.#context.removeResource(mediaType === 'video' ? 'videoElement' : 'audioElement');
|
|
46
|
+
}
|
|
47
|
+
this.#mediaElement = undefined;
|
|
48
|
+
this.#lastTargetTime = null;
|
|
49
|
+
this.#destroyed = markDestroyed;
|
|
50
|
+
}
|
|
51
|
+
async #handleSetup(force = false) {
|
|
23
52
|
if (this.#mediaElement && !this.#destroyed) {
|
|
53
|
+
this.#context.setResource(this.#context.type === 'VIDEO' ? 'videoElement' : 'audioElement', this.#mediaElement);
|
|
24
54
|
return;
|
|
25
55
|
}
|
|
26
56
|
if (this.#context.contextData.type !== 'VIDEO' && this.#context.contextData.type !== 'AUDIO')
|
|
27
57
|
return;
|
|
58
|
+
if (!force && !this.#shouldPrepareMedia()) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
28
61
|
const mediaType = this.#context.type === 'VIDEO' ? 'video' : 'audio';
|
|
29
62
|
const source = this.#context.contextData.source; // TODO: remove as any
|
|
30
63
|
if (!source || !source.url) {
|
|
@@ -62,10 +95,28 @@ export class MediaHook {
|
|
|
62
95
|
this.#lastTargetTime = target;
|
|
63
96
|
await this.#seek(target);
|
|
64
97
|
}
|
|
98
|
+
#refreshPixiTexture() {
|
|
99
|
+
try {
|
|
100
|
+
const texture = this.#context.getResource('pixiTexture');
|
|
101
|
+
const baseTexture = texture?.baseTexture;
|
|
102
|
+
if (baseTexture?.resource && typeof baseTexture.resource.update === 'function') {
|
|
103
|
+
baseTexture.resource.update();
|
|
104
|
+
}
|
|
105
|
+
else if (typeof baseTexture?.update === 'function') {
|
|
106
|
+
baseTexture.update();
|
|
107
|
+
}
|
|
108
|
+
else if (typeof texture?.update === 'function') {
|
|
109
|
+
texture.update();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch { }
|
|
113
|
+
}
|
|
65
114
|
async #seek(time) {
|
|
66
115
|
if (!this.#mediaElement) {
|
|
67
116
|
return;
|
|
68
117
|
}
|
|
118
|
+
this.#cleanupPendingSeek?.();
|
|
119
|
+
this.#cleanupPendingSeek = undefined;
|
|
69
120
|
const element = this.#mediaElement;
|
|
70
121
|
const frameDuration = 1 / (this.state.data.settings.fps || 30);
|
|
71
122
|
// Use fastSeek for larger jumps when supported
|
|
@@ -81,25 +132,98 @@ export class MediaHook {
|
|
|
81
132
|
catch {
|
|
82
133
|
element.currentTime = time;
|
|
83
134
|
}
|
|
84
|
-
|
|
135
|
+
const seekVersion = ++this.#seekVersion;
|
|
136
|
+
const shouldRerenderWhenReady = this.state.environment !== 'server' &&
|
|
137
|
+
this.#context.sceneState.state !== 'playing' &&
|
|
138
|
+
this.#context.sceneState.state !== 'loading';
|
|
139
|
+
let settleSeek;
|
|
140
|
+
let settleResolved = false;
|
|
141
|
+
let settleTimeout;
|
|
142
|
+
let onSeeked;
|
|
143
|
+
let frameCallbackId;
|
|
85
144
|
const anyElement = element;
|
|
145
|
+
let readinessCleanup = () => { };
|
|
146
|
+
const resolveSettled = () => {
|
|
147
|
+
if (settleResolved)
|
|
148
|
+
return;
|
|
149
|
+
settleResolved = true;
|
|
150
|
+
settleSeek?.();
|
|
151
|
+
};
|
|
152
|
+
const cleanupPendingSeek = () => {
|
|
153
|
+
readinessCleanup();
|
|
154
|
+
if (settleTimeout !== undefined) {
|
|
155
|
+
clearTimeout(settleTimeout);
|
|
156
|
+
settleTimeout = undefined;
|
|
157
|
+
}
|
|
158
|
+
if (onSeeked) {
|
|
159
|
+
element.removeEventListener('seeked', onSeeked);
|
|
160
|
+
onSeeked = undefined;
|
|
161
|
+
}
|
|
162
|
+
if (frameCallbackId !== undefined &&
|
|
163
|
+
typeof anyElement.cancelVideoFrameCallback === 'function') {
|
|
164
|
+
anyElement.cancelVideoFrameCallback(frameCallbackId);
|
|
165
|
+
frameCallbackId = undefined;
|
|
166
|
+
}
|
|
167
|
+
resolveSettled();
|
|
168
|
+
};
|
|
169
|
+
this.#cleanupPendingSeek = cleanupPendingSeek;
|
|
170
|
+
if (shouldRerenderWhenReady) {
|
|
171
|
+
let cleanedUp = false;
|
|
172
|
+
readinessCleanup = () => {
|
|
173
|
+
if (cleanedUp)
|
|
174
|
+
return;
|
|
175
|
+
cleanedUp = true;
|
|
176
|
+
element.removeEventListener('seeked', handleReady);
|
|
177
|
+
element.removeEventListener('canplay', handleReady);
|
|
178
|
+
};
|
|
179
|
+
const handleReady = () => {
|
|
180
|
+
if (seekVersion !== this.#seekVersion) {
|
|
181
|
+
readinessCleanup();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.#refreshPixiTexture();
|
|
185
|
+
this.#context.eventManager.emit('rerender');
|
|
186
|
+
readinessCleanup();
|
|
187
|
+
};
|
|
188
|
+
element.addEventListener('seeked', handleReady);
|
|
189
|
+
element.addEventListener('canplay', handleReady);
|
|
190
|
+
}
|
|
191
|
+
let settled;
|
|
192
|
+
// Prefer requestVideoFrameCallback when available (videos only)
|
|
86
193
|
if (typeof anyElement.requestVideoFrameCallback === 'function') {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
194
|
+
settled = new Promise((resolve) => {
|
|
195
|
+
settleSeek = () => resolve(true);
|
|
196
|
+
settleTimeout = setTimeout(() => resolveSettled(), 120);
|
|
197
|
+
frameCallbackId = anyElement.requestVideoFrameCallback((_now, _metadata) => {
|
|
198
|
+
if (settleTimeout !== undefined) {
|
|
199
|
+
clearTimeout(settleTimeout);
|
|
200
|
+
settleTimeout = undefined;
|
|
201
|
+
}
|
|
202
|
+
frameCallbackId = undefined;
|
|
203
|
+
resolveSettled();
|
|
92
204
|
});
|
|
93
205
|
});
|
|
94
206
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
207
|
+
else {
|
|
208
|
+
// Fallback to seeked event
|
|
209
|
+
settled = new Promise((resolve) => {
|
|
210
|
+
settleSeek = () => resolve(true);
|
|
211
|
+
onSeeked = () => {
|
|
212
|
+
element.removeEventListener('seeked', onSeeked);
|
|
213
|
+
onSeeked = undefined;
|
|
214
|
+
resolveSettled();
|
|
215
|
+
};
|
|
216
|
+
element.addEventListener('seeked', onSeeked);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
await settled;
|
|
220
|
+
if (this.#cleanupPendingSeek === cleanupPendingSeek) {
|
|
221
|
+
this.#cleanupPendingSeek = undefined;
|
|
222
|
+
}
|
|
223
|
+
if (seekVersion !== this.#seekVersion || this.#mediaElement !== element) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.#refreshPixiTexture();
|
|
103
227
|
}
|
|
104
228
|
#isOutOfSync() {
|
|
105
229
|
if (!this.#mediaElement) {
|
|
@@ -159,9 +283,6 @@ export class MediaHook {
|
|
|
159
283
|
}
|
|
160
284
|
}
|
|
161
285
|
async #handleUpdate() {
|
|
162
|
-
// On server, seeking is handled by MediaSeekingHook. Avoid duplicate logic/races.
|
|
163
|
-
if (this.state.environment === 'server')
|
|
164
|
-
return;
|
|
165
286
|
if (this.#context.contextData.type !== 'VIDEO' && this.#context.contextData.type !== 'AUDIO')
|
|
166
287
|
return;
|
|
167
288
|
const isActive = this.#context.isActive;
|
|
@@ -175,6 +296,14 @@ export class MediaHook {
|
|
|
175
296
|
const now = performance.now();
|
|
176
297
|
const shouldCheckController = now - this.#lastControllerCheck > this.#CONTROLLER_CHECK_INTERVAL;
|
|
177
298
|
const isController = this.mediaManager.getMediaController(source.url, mediaType) === this.#context.contextData.id;
|
|
299
|
+
const shouldPrepareMedia = this.#shouldPrepareMedia();
|
|
300
|
+
if (!shouldPrepareMedia) {
|
|
301
|
+
await this.#releaseMediaElement();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!this.#mediaElement) {
|
|
305
|
+
await this.#handleSetup(true);
|
|
306
|
+
}
|
|
178
307
|
if (!isActive) {
|
|
179
308
|
if (isController) {
|
|
180
309
|
await this.#pause('!isActive isController');
|
|
@@ -183,20 +312,25 @@ export class MediaHook {
|
|
|
183
312
|
return;
|
|
184
313
|
}
|
|
185
314
|
if (!this.#mediaElement) {
|
|
186
|
-
|
|
315
|
+
return;
|
|
187
316
|
}
|
|
188
|
-
//
|
|
189
|
-
if (
|
|
190
|
-
|
|
317
|
+
// On server, seeking is handled by MediaSeekingHook. Avoid duplicate logic/races.
|
|
318
|
+
if (this.state.environment === 'server')
|
|
319
|
+
return;
|
|
320
|
+
// Make sure we're still marked as the controller. If nobody owns the shared media,
|
|
321
|
+
// claim it immediately; otherwise keep the debounce to avoid controller thrash.
|
|
322
|
+
if (!isController) {
|
|
191
323
|
const currentController = this.mediaManager.getMediaController(source.url, mediaType);
|
|
192
|
-
if (!currentController) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
this.#mediaElement
|
|
199
|
-
|
|
324
|
+
if (!currentController || shouldCheckController) {
|
|
325
|
+
if (!currentController) {
|
|
326
|
+
this.mediaManager.setMediaController(source.url, this.#context.contextData.id, mediaType);
|
|
327
|
+
await this.#autoSeek();
|
|
328
|
+
this.#lastControllerCheck = now;
|
|
329
|
+
// Safari-specific: Ensure audio is properly restored when taking control
|
|
330
|
+
if (this.#mediaElement && !isMuted) {
|
|
331
|
+
this.#mediaElement.muted = false;
|
|
332
|
+
this.#mediaElement.volume = componentVolume;
|
|
333
|
+
}
|
|
200
334
|
}
|
|
201
335
|
}
|
|
202
336
|
}
|
|
@@ -242,21 +376,10 @@ export class MediaHook {
|
|
|
242
376
|
}
|
|
243
377
|
async #handleRefresh() {
|
|
244
378
|
await this.#handleDestroy();
|
|
245
|
-
await this.#handleSetup();
|
|
379
|
+
await this.#handleSetup(true);
|
|
246
380
|
}
|
|
247
381
|
async #handleDestroy() {
|
|
248
|
-
this.#
|
|
249
|
-
this.#lastTargetTime = null;
|
|
250
|
-
// Release the media element back to the MediaManager
|
|
251
|
-
if (this.#mediaElement) {
|
|
252
|
-
const mediaType = this.#context.type === 'VIDEO' ? 'video' : 'audio';
|
|
253
|
-
const source = this.#context.contextData.source;
|
|
254
|
-
if (source && source.url) {
|
|
255
|
-
this.mediaManager.releaseMediaElement(source.url, mediaType);
|
|
256
|
-
}
|
|
257
|
-
this.#context.removeResource(mediaType === 'video' ? 'videoElement' : 'audioElement');
|
|
258
|
-
}
|
|
259
|
-
this.#mediaElement = undefined;
|
|
382
|
+
await this.#releaseMediaElement(true);
|
|
260
383
|
}
|
|
261
384
|
async handle(type, context) {
|
|
262
385
|
this.#context = context;
|
|
@@ -272,9 +395,11 @@ export class MediaHook {
|
|
|
272
395
|
else if (type === 'destroy') {
|
|
273
396
|
return await this.#handleDestroy();
|
|
274
397
|
}
|
|
275
|
-
else if (type === 'refresh') {
|
|
398
|
+
else if (type === 'refresh:content') {
|
|
276
399
|
return await this.#handleRefresh();
|
|
277
400
|
}
|
|
278
|
-
|
|
401
|
+
else if (type === 'update') {
|
|
402
|
+
await this.#handleUpdate();
|
|
403
|
+
}
|
|
279
404
|
}
|
|
280
405
|
}
|