visualfries 0.1.10120 → 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.
Files changed (133) hide show
  1. package/README.md +97 -0
  2. package/bin/visualfries.js +1570 -0
  3. package/dist/DIContainer.d.ts +2 -1
  4. package/dist/DIContainer.js +16 -5
  5. package/dist/SceneBuilder.svelte.d.ts +70 -1
  6. package/dist/SceneBuilder.svelte.js +96 -29
  7. package/dist/agent/audioMixer.d.ts +34 -0
  8. package/dist/agent/audioMixer.js +308 -0
  9. package/dist/agent/broll.d.ts +35 -0
  10. package/dist/agent/broll.js +226 -0
  11. package/dist/agent/capabilities.d.ts +34 -0
  12. package/dist/agent/capabilities.js +246 -0
  13. package/dist/agent/captionScene.d.ts +23 -0
  14. package/dist/agent/captionScene.js +204 -0
  15. package/dist/agent/catalog.d.ts +24 -0
  16. package/dist/agent/catalog.js +67 -0
  17. package/dist/agent/cueFiles.d.ts +54 -0
  18. package/dist/agent/cueFiles.js +219 -0
  19. package/dist/agent/cuePresets.d.ts +7 -0
  20. package/dist/agent/cuePresets.js +50 -0
  21. package/dist/agent/deterministicMedia.d.ts +61 -0
  22. package/dist/agent/deterministicMedia.js +303 -0
  23. package/dist/agent/index.d.ts +18 -0
  24. package/dist/agent/index.js +18 -0
  25. package/dist/agent/inspectScene.d.ts +36 -0
  26. package/dist/agent/inspectScene.js +139 -0
  27. package/dist/agent/localRender.d.ts +82 -0
  28. package/dist/agent/localRender.js +702 -0
  29. package/dist/agent/overlays.d.ts +40 -0
  30. package/dist/agent/overlays.js +325 -0
  31. package/dist/agent/productionPlan.d.ts +131 -0
  32. package/dist/agent/productionPlan.js +322 -0
  33. package/dist/agent/renderPlan.d.ts +27 -0
  34. package/dist/agent/renderPlan.js +68 -0
  35. package/dist/agent/renderRanges.d.ts +24 -0
  36. package/dist/agent/renderRanges.js +73 -0
  37. package/dist/agent/runtimeSupport.d.ts +19 -0
  38. package/dist/agent/runtimeSupport.js +192 -0
  39. package/dist/agent/streamingEncoder.d.ts +31 -0
  40. package/dist/agent/streamingEncoder.js +208 -0
  41. package/dist/agent/transcripts.d.ts +29 -0
  42. package/dist/agent/transcripts.js +146 -0
  43. package/dist/agent/transitions.d.ts +20 -0
  44. package/dist/agent/transitions.js +169 -0
  45. package/dist/animations/PixiAnimationTarget.d.ts +29 -0
  46. package/dist/animations/PixiAnimationTarget.js +63 -0
  47. package/dist/animations/builders/LineHighlighterAnimationBuilder.js +20 -3
  48. package/dist/animations/builders/WordHighlighterAnimationBuilder.js +15 -2
  49. package/dist/browser/browserExport.d.ts +73 -0
  50. package/dist/browser/browserExport.js +180 -0
  51. package/dist/browser/glTransitions.d.ts +37 -0
  52. package/dist/browser/glTransitions.js +261 -0
  53. package/dist/browser/index.d.ts +3 -0
  54. package/dist/browser/index.js +3 -0
  55. package/dist/browser/mediaDecode.d.ts +31 -0
  56. package/dist/browser/mediaDecode.js +152 -0
  57. package/dist/builders/PixiComponentBuilder.d.ts +6 -0
  58. package/dist/builders/PixiComponentBuilder.js +12 -0
  59. package/dist/builders/_ComponentState.svelte.d.ts +7 -5
  60. package/dist/builders/_ComponentState.svelte.js +29 -15
  61. package/dist/builders/html/StyleBuilder.js +9 -8
  62. package/dist/builders/html/TextComponentHtmlBuilder.js +4 -2
  63. package/dist/builders/html/processors/AppearanceStyleProcessor.js +1 -1
  64. package/dist/builders/html/processors/TextAppearanceStyleProcessor.js +7 -0
  65. package/dist/builders/html/processors/TextEffectsStyleProcessor.js +32 -13
  66. package/dist/commands/ReplaceSourceOnTimeCommand.js +9 -1
  67. package/dist/commands/SeekCommand.js +19 -6
  68. package/dist/components/AnimatedGIF.d.ts +3 -0
  69. package/dist/components/AnimatedGIF.js +28 -1
  70. package/dist/components/Component.svelte.d.ts +5 -5
  71. package/dist/components/Component.svelte.js +29 -13
  72. package/dist/components/ComponentContext.svelte.js +68 -23
  73. package/dist/components/hooks/AnimationHook.js +1 -1
  74. package/dist/components/hooks/CanvasFillHook.d.ts +12 -0
  75. package/dist/components/hooks/CanvasFillHook.js +75 -0
  76. package/dist/components/hooks/DeterministicMediaFrameHook.js +3 -0
  77. package/dist/components/hooks/HtmlTextHook.js +11 -3
  78. package/dist/components/hooks/HtmlToCanvasHook.js +8 -25
  79. package/dist/components/hooks/MediaHook.js +172 -47
  80. package/dist/components/hooks/MediaSeekingHook.js +138 -88
  81. package/dist/components/hooks/PixiGifHook.js +4 -4
  82. package/dist/components/hooks/PixiSplitScreenDisplayObjectHook.js +73 -6
  83. package/dist/components/hooks/PixiVideoTextureHook.js +10 -4
  84. package/dist/components/hooks/PixiVisualTransformHook.d.ts +16 -0
  85. package/dist/components/hooks/PixiVisualTransformHook.js +102 -0
  86. package/dist/components/hooks/SubtitlesHook.js +48 -24
  87. package/dist/components/hooks/VerifyMediaHook.js +2 -2
  88. package/dist/directors/ComponentDirector.d.ts +1 -0
  89. package/dist/directors/ComponentDirector.js +41 -8
  90. package/dist/fonts/fontDiscovery.js +12 -1
  91. package/dist/layers/Layer.svelte.d.ts +54 -0
  92. package/dist/layers/Layer.svelte.js +2 -25
  93. package/dist/managers/AppManager.svelte.js +8 -5
  94. package/dist/managers/ComponentsManager.svelte.d.ts +7 -7
  95. package/dist/managers/ComponentsManager.svelte.js +29 -15
  96. package/dist/managers/DeterministicMediaManager.js +38 -3
  97. package/dist/managers/EventManager.js +3 -1
  98. package/dist/managers/LayersManager.svelte.d.ts +54 -0
  99. package/dist/managers/RenderManager.d.ts +5 -0
  100. package/dist/managers/RenderManager.js +67 -21
  101. package/dist/managers/StateManager.svelte.d.ts +56 -0
  102. package/dist/managers/StateManager.svelte.js +60 -58
  103. package/dist/managers/SubtitlesManager.svelte.js +1 -3
  104. package/dist/managers/TimeManager.svelte.d.ts +4 -0
  105. package/dist/managers/TimeManager.svelte.js +23 -7
  106. package/dist/schemas/runtime/deterministic.js +5 -1
  107. package/dist/schemas/runtime/types.d.ts +36 -15
  108. package/dist/schemas/scene/components.d.ts +126 -0
  109. package/dist/schemas/scene/components.js +11 -0
  110. package/dist/schemas/scene/core.d.ts +109 -10
  111. package/dist/schemas/scene/core.js +1 -1
  112. package/dist/utils/mediaWindow.d.ts +13 -0
  113. package/dist/utils/mediaWindow.js +25 -0
  114. package/dist/utils/svgGenerator.js +1 -1
  115. package/dist/utils/timelineWindow.d.ts +2 -0
  116. package/dist/utils/timelineWindow.js +7 -0
  117. package/dist/utils/utils.js +2 -4
  118. package/docs/AGENT_PATTERNS.md +243 -0
  119. package/docs/AGENT_WORKFLOW.md +364 -0
  120. package/docs/AUTHORING_BEST_PRACTICES.md +188 -0
  121. package/docs/AUTHORING_SYSTEM_AUDIT.md +98 -0
  122. package/docs/BROWSER_EXPORT_SPIKE.md +55 -0
  123. package/docs/DETERMINISTIC_RENDERER.md +83 -0
  124. package/docs/NEXT_RELEASE.md +28 -0
  125. package/docs/PROPOSED_VISUALFRIES_SKILL.md +68 -0
  126. package/docs/examples/agent-cues.json +49 -0
  127. package/docs/examples/agent-transcript.json +23 -0
  128. package/docs/examples/agent-transcript.srt +7 -0
  129. package/docs/examples/agent-transcript.vtt +7 -0
  130. package/docs/examples/image-animation.json +87 -0
  131. package/docs/examples/native-text-metric-badge.json +109 -0
  132. package/package.json +133 -100
  133. 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
- // for texts we need to target this, but if we animate scale, position etc. we should target parent
57
- this.#context.setResource('animationTarget', this.#htmlEl);
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
- // Safely encode characters list, filtering out any problematic characters
71
- let encodedChars = '';
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.destroy();
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
- async #handleSetup() {
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
- // Prefer requestVideoFrameCallback when available (videos only)
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
- return new Promise((resolve) => {
88
- const timeout = setTimeout(() => resolve(true), 120);
89
- anyElement.requestVideoFrameCallback((_now, _metadata) => {
90
- clearTimeout(timeout);
91
- resolve(true);
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
- // Fallback to seeked event
96
- return new Promise((resolve) => {
97
- const onSeeked = () => {
98
- element.removeEventListener('seeked', onSeeked);
99
- resolve(true);
100
- };
101
- element.addEventListener('seeked', onSeeked);
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
- await this.#handleRefresh();
315
+ return;
187
316
  }
188
- // Make sure we're still marked as the controller (debounced)
189
- if (!isController && shouldCheckController) {
190
- // Only set controller if no other component is currently controlling this media
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
- this.mediaManager.setMediaController(source.url, this.#context.contextData.id, mediaType);
194
- await this.#autoSeek();
195
- this.#lastControllerCheck = now;
196
- // Safari-specific: Ensure audio is properly restored when taking control
197
- if (this.#mediaElement && !isMuted) {
198
- this.#mediaElement.muted = false;
199
- this.#mediaElement.volume = componentVolume;
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.#destroyed = true;
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
- await this.#handleUpdate();
401
+ else if (type === 'update') {
402
+ await this.#handleUpdate();
403
+ }
279
404
  }
280
405
  }