@lingxia/elements 0.15.0 → 0.17.0

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 (67) hide show
  1. package/dist/index.d.ts +3 -2
  2. package/dist/index.js +1 -0
  3. package/dist/inline-native/commit.d.ts +39 -0
  4. package/dist/inline-native/commit.js +94 -0
  5. package/dist/inline-native/elements.d.ts +104 -0
  6. package/dist/inline-native/elements.js +749 -0
  7. package/dist/inline-native/errors.d.ts +8 -0
  8. package/dist/inline-native/errors.js +23 -0
  9. package/dist/inline-native/events.d.ts +10 -0
  10. package/dist/inline-native/events.js +16 -0
  11. package/dist/inline-native/geometry.d.ts +78 -0
  12. package/dist/inline-native/geometry.js +40 -0
  13. package/dist/inline-native/identity.d.ts +28 -0
  14. package/dist/inline-native/identity.js +51 -0
  15. package/dist/inline-native/index.d.ts +14 -0
  16. package/dist/inline-native/index.js +19 -0
  17. package/dist/inline-native/layout.d.ts +2 -0
  18. package/dist/inline-native/layout.js +58 -0
  19. package/dist/inline-native/lease.d.ts +13 -0
  20. package/dist/inline-native/lease.js +31 -0
  21. package/dist/inline-native/runtime.d.ts +64 -0
  22. package/dist/inline-native/runtime.js +119 -0
  23. package/dist/inline-native/schema.d.ts +111 -0
  24. package/dist/inline-native/schema.js +210 -0
  25. package/dist/inline-native/structure.d.ts +22 -0
  26. package/dist/inline-native/structure.js +706 -0
  27. package/dist/inline-native/style.d.ts +7 -0
  28. package/dist/inline-native/style.js +119 -0
  29. package/dist/inline-native/types.d.ts +82 -0
  30. package/dist/inline-native/types.js +7 -0
  31. package/dist/inline-native/unwrap.d.ts +10 -0
  32. package/dist/inline-native/unwrap.js +32 -0
  33. package/dist/inline-native/video.d.ts +65 -0
  34. package/dist/inline-native/video.js +69 -0
  35. package/dist/native_component_wrapper_shared.d.ts +3 -2
  36. package/dist/native_component_wrapper_shared.js +6 -3
  37. package/dist/nativecomponent.d.ts +1 -1
  38. package/dist/nativecomponent.js +8 -11
  39. package/dist/navigator.d.ts +2 -2
  40. package/dist/navigator.js +5 -5
  41. package/dist/video-events.d.ts +2 -0
  42. package/dist/video-events.js +18 -0
  43. package/dist/video.d.ts +98 -79
  44. package/dist/video.js +182 -757
  45. package/package.json +2 -1
  46. package/src/index.ts +58 -1
  47. package/src/inline-native/commit.ts +141 -0
  48. package/src/inline-native/elements.ts +829 -0
  49. package/src/inline-native/errors.ts +35 -0
  50. package/src/inline-native/events.ts +31 -0
  51. package/src/inline-native/geometry.ts +86 -0
  52. package/src/inline-native/identity.ts +102 -0
  53. package/src/inline-native/index.ts +103 -0
  54. package/src/inline-native/layout.ts +58 -0
  55. package/src/inline-native/lease.ts +48 -0
  56. package/src/inline-native/runtime.ts +185 -0
  57. package/src/inline-native/schema.ts +247 -0
  58. package/src/inline-native/structure.ts +823 -0
  59. package/src/inline-native/style.ts +111 -0
  60. package/src/inline-native/types.ts +105 -0
  61. package/src/inline-native/unwrap.ts +34 -0
  62. package/src/inline-native/video.ts +125 -0
  63. package/src/native_component_wrapper_shared.ts +8 -6
  64. package/src/nativecomponent.ts +8 -13
  65. package/src/navigator.ts +9 -9
  66. package/src/video-events.ts +16 -0
  67. package/src/video.ts +245 -839
package/src/video.ts CHANGED
@@ -1,17 +1,42 @@
1
- import {
2
- sendNativeComponentMessage,
3
- registerNativeComponentHandler,
4
- addNativeComponentLayoutInvalidationListener
5
- } from "./nativecomponent.js";
6
- import { measureElement } from "./dom.js";
7
- import { ensureComponentId, NativeComponentUpdateState, iOSNativeComponentHelper } from "./component.js";
8
- import { isAndroid, isHarmony, isIOS, isWindows } from "./platform.js";
9
-
10
- const HARMONY_PROPS_PREFIX = "data:application/json,";
11
-
12
- // Type definitions
1
+ import { normalizeVideoEventDetail } from "./video-events.js";
2
+ import { ensureComponentId } from "./component.js";
3
+ import { registerNativeComponentHandler } from "./nativecomponent.js";
4
+ import { findInlineNativeRoot } from "./inline-native/structure.js";
5
+
13
6
  export type LxVideoQuality = { label: string; url?: string };
14
- type LxVideoViewEventHandler = (e: Event) => void;
7
+ export interface LxVideoEventPayloads {
8
+ onPlayRequest: Record<string, never>;
9
+ onPlay: Record<string, never>;
10
+ onPlaying: Record<string, never>;
11
+ onPause: Record<string, never>;
12
+ onStop: Record<string, never>;
13
+ onEnded: Record<string, never>;
14
+ onWaiting: Record<string, never>;
15
+ onTimeUpdate: { currentTime: number; duration?: number };
16
+ onError: { code: string; message: string; recoverable?: boolean };
17
+ onLoadedMetadata: { duration: number; width?: number; height?: number };
18
+ onFullscreenChange: { fullscreen: boolean; direction?: string };
19
+ onQualityChange: { quality: string; url?: string };
20
+ onRateChange: { rate: number };
21
+ onVolumeChange: { volume: number; muted?: boolean };
22
+ }
23
+
24
+ export interface LxVideoEventHandlers {
25
+ onPlayRequest?: (payload: LxVideoEventPayloads["onPlayRequest"]) => void;
26
+ onPlay?: (payload: LxVideoEventPayloads["onPlay"]) => void;
27
+ onPlaying?: (payload: LxVideoEventPayloads["onPlaying"]) => void;
28
+ onPause?: (payload: LxVideoEventPayloads["onPause"]) => void;
29
+ onStop?: (payload: LxVideoEventPayloads["onStop"]) => void;
30
+ onEnded?: (payload: LxVideoEventPayloads["onEnded"]) => void;
31
+ onWaiting?: (payload: LxVideoEventPayloads["onWaiting"]) => void;
32
+ onTimeUpdate?: (payload: LxVideoEventPayloads["onTimeUpdate"]) => void;
33
+ onError?: (payload: LxVideoEventPayloads["onError"]) => void;
34
+ onLoadedMetadata?: (payload: LxVideoEventPayloads["onLoadedMetadata"]) => void;
35
+ onFullscreenChange?: (payload: LxVideoEventPayloads["onFullscreenChange"]) => void;
36
+ onQualityChange?: (payload: LxVideoEventPayloads["onQualityChange"]) => void;
37
+ onRateChange?: (payload: LxVideoEventPayloads["onRateChange"]) => void;
38
+ onVolumeChange?: (payload: LxVideoEventPayloads["onVolumeChange"]) => void;
39
+ }
15
40
 
16
41
  export type LxVideoAttributes = {
17
42
  id?: string;
@@ -26,26 +51,25 @@ export type LxVideoAttributes = {
26
51
  progressBar?: boolean;
27
52
  live?: boolean;
28
53
  volume?: string | number;
29
- qualities?: LxVideoQuality[]; // First is default
30
- playbackRates?: number[]; // First is default
54
+ qualities?: LxVideoQuality[];
55
+ playbackRates?: number[];
31
56
  className?: string;
32
- style?: any;
33
- ref?: any;
34
- // Event handlers (framework-native syntax)
35
- onPlayRequest?: LxVideoViewEventHandler;
36
- onPlay?: LxVideoViewEventHandler;
37
- onPlaying?: LxVideoViewEventHandler;
38
- onPause?: LxVideoViewEventHandler;
39
- onStop?: LxVideoViewEventHandler;
40
- onEnded?: LxVideoViewEventHandler;
41
- onTimeUpdate?: LxVideoViewEventHandler;
42
- onError?: LxVideoViewEventHandler;
43
- onLoadedMetadata?: LxVideoViewEventHandler;
44
- onFullscreenChange?: LxVideoViewEventHandler;
45
- onWaiting?: LxVideoViewEventHandler;
46
- onQualityChange?: LxVideoViewEventHandler;
47
- onRateChange?: LxVideoViewEventHandler;
48
- // Logic bindings (CLI-generated, maps event name → Logic function name)
57
+ style?: unknown;
58
+ ref?: unknown;
59
+ onPlayRequest?: (event: CustomEvent<LxVideoEventPayloads["onPlayRequest"]>) => void;
60
+ onPlay?: (event: CustomEvent<LxVideoEventPayloads["onPlay"]>) => void;
61
+ onPlaying?: (event: CustomEvent<LxVideoEventPayloads["onPlaying"]>) => void;
62
+ onPause?: (event: CustomEvent<LxVideoEventPayloads["onPause"]>) => void;
63
+ onStop?: (event: CustomEvent<LxVideoEventPayloads["onStop"]>) => void;
64
+ onEnded?: (event: CustomEvent<LxVideoEventPayloads["onEnded"]>) => void;
65
+ onTimeUpdate?: (event: CustomEvent<LxVideoEventPayloads["onTimeUpdate"]>) => void;
66
+ onError?: (event: CustomEvent<LxVideoEventPayloads["onError"]>) => void;
67
+ onLoadedMetadata?: (event: CustomEvent<LxVideoEventPayloads["onLoadedMetadata"]>) => void;
68
+ onFullscreenChange?: (event: CustomEvent<LxVideoEventPayloads["onFullscreenChange"]>) => void;
69
+ onWaiting?: (event: CustomEvent<LxVideoEventPayloads["onWaiting"]>) => void;
70
+ onQualityChange?: (event: CustomEvent<LxVideoEventPayloads["onQualityChange"]>) => void;
71
+ onRateChange?: (event: CustomEvent<LxVideoEventPayloads["onRateChange"]>) => void;
72
+ onVolumeChange?: (event: CustomEvent<LxVideoEventPayloads["onVolumeChange"]>) => void;
49
73
  pageBindings?: Record<string, string>;
50
74
  };
51
75
 
@@ -59,9 +83,14 @@ declare global {
59
83
  }
60
84
  }
61
85
 
62
- // Component implementation
86
+ /**
87
+ * The DOM leaf for an inline-native video.
88
+ *
89
+ * Rendering, geometry and lifecycle are owned by the nearest LxNativeRoot.
90
+ * A bare lx-video is invalid and deliberately has no legacy overlay path.
91
+ */
63
92
  export class LxVideoElement extends HTMLElement {
64
- static get observedAttributes() {
93
+ static get observedAttributes(): string[] {
65
94
  return [
66
95
  "id",
67
96
  "src",
@@ -76,893 +105,270 @@ export class LxVideoElement extends HTMLElement {
76
105
  "live",
77
106
  "volume",
78
107
  "qualities",
79
- "playback-rates"
108
+ "playback-rates",
80
109
  ];
81
110
  }
82
111
 
83
112
  private componentId: string | null = null;
84
- private mounted = false;
85
- private updateState = new NativeComponentUpdateState();
113
+ private islandPlaybackIdle = false;
86
114
  private unregister?: () => void;
87
- private resizeObserver?: ResizeObserver;
88
- private winSettleFrame: number | null = null;
89
- private pendingLayoutFrame: number | null = null;
90
- private boundUpdatePosition = this.updatePosition.bind(this);
91
- private _handlers: Record<string, EventListenerOrEventListenerObject> = {};
92
- private harmonyEmbed?: HTMLEmbedElement;
93
- private lastHarmonyProps?: string;
94
- private pendingHarmonyProps?: Record<string, unknown>;
95
- private pendingHarmonyRetryTimer: number | null = null;
96
- private pendingHarmonyRetryCount: number = 0;
97
- private iOSBootstrapFrame: number | null = null;
98
- private iOSBootstrapRemaining: number = 0;
99
- private iOSHelper?: iOSNativeComponentHelper;
100
- private removeLayoutInvalidationListener?: () => void;
101
- private forceHarmonyEmbedRecreate: boolean = false;
102
- private attrObserver?: MutationObserver;
103
- private resizeEventListener: EventListenerObject = {
104
- handleEvent: () => this.boundUpdatePosition(),
105
- };
106
- private iOSScrollEventListener: EventListenerObject = {
107
- handleEvent: () => this.boundUpdatePosition(),
108
- };
109
- private scrollSettleFrame: number | null = null;
110
- // Scroll events do not bubble, so a plain window listener misses inner
111
- // scroll containers; the capture phase sees them all. Any ancestor
112
- // scrolling moves the element's document rect, so re-measure (coalesced
113
- // to one update per frame; decide() dedups unchanged rects).
114
- private containerScrollListener: EventListenerObject = {
115
- handleEvent: () => {
116
- if (this.scrollSettleFrame !== null) return;
117
- this.scrollSettleFrame = requestAnimationFrame(() => {
118
- this.scrollSettleFrame = null;
119
- this.updatePosition();
120
- });
121
- },
122
- };
115
+ private handlers: Record<string, EventListenerOrEventListenerObject> = {};
123
116
  private rawHandlers: Record<string, EventListenerOrEventListenerObject> = {};
124
- private _pageBindings: Record<string, string> = {};
117
+ private bindings: Record<string, string> = {};
125
118
 
126
- private parseRotateValue(value: unknown): 0 | 90 | 180 | 270 | undefined {
127
- const parsed = (() => {
128
- if (typeof value === "number") {
129
- return Number.isInteger(value) ? value : NaN;
130
- }
131
- if (typeof value === "string") {
132
- const normalized = value.trim();
133
- if (!/^(0|90|180|270)$/.test(normalized)) return NaN;
134
- return Number(normalized);
135
- }
136
- return NaN;
137
- })();
138
- if (parsed === 0 || parsed === 90 || parsed === 180 || parsed === 270) {
139
- return parsed;
140
- }
141
- return undefined;
119
+ set pageBindings(bindings: Record<string, string>) {
120
+ this.bindings = bindings ?? {};
121
+ if (this.isConnected) this.requestRootCompile();
142
122
  }
143
123
 
144
- private parseObjectFitValue(value: unknown): LxObjectFit | undefined {
145
- if (typeof value !== "string") return undefined;
146
- const normalized = value.trim().toLowerCase();
147
- if (
148
- normalized === "cover" ||
149
- normalized === "contain" ||
150
- normalized === "fill" ||
151
- normalized === "fit"
152
- ) {
153
- return normalized;
154
- }
155
- return undefined;
124
+ get pageBindings(): Record<string, string> {
125
+ return this.bindings;
156
126
  }
157
127
 
158
- set pageBindings(bindings: Record<string, string>) {
159
- this._pageBindings = bindings ?? {};
160
- if (this.isConnected) {
161
- this.mountOrUpdate(true);
162
- }
128
+ get pageFuncBindings(): Record<string, string> {
129
+ return this.bindings;
163
130
  }
164
131
 
165
- get pageBindings(): Record<string, string> {
166
- return this._pageBindings;
132
+ get pageFuncBindingsJson(): string {
133
+ return JSON.stringify(this.bindings);
167
134
  }
168
135
 
169
136
  set src(value: string | null | undefined) {
170
137
  if (value == null) {
171
138
  this.removeAttribute("src");
172
- return;
139
+ } else {
140
+ this.setAttribute("src", String(value));
173
141
  }
174
- this.setAttribute("src", String(value));
175
142
  }
176
143
 
177
144
  get src(): string | null {
178
145
  return this.getAttribute("src");
179
146
  }
180
147
 
181
- private dataAttrToDatasetKey(attr: string): string {
182
- const raw = attr.slice(5).trim();
183
- if (!raw) return "";
184
- const parts = raw.split("-").filter(Boolean);
185
- if (parts.length === 0) return "";
186
- return parts
187
- .map((segment, index) => {
188
- if (index === 0) return segment.toLowerCase();
189
- return segment.charAt(0).toUpperCase() + segment.slice(1);
190
- })
191
- .join("");
192
- }
193
-
194
- private collectDataset(): Record<string, string> {
195
- const dataset: Record<string, string> = {};
196
- const attrs = this.getAttributeNames();
197
- for (const attr of attrs) {
198
- if (!attr.startsWith("data-")) continue;
199
- const key = this.dataAttrToDatasetKey(attr);
200
- if (!key) continue;
201
- const value = this.getAttribute(attr);
202
- if (value == null) continue;
203
- dataset[key] = value;
204
- }
205
- return dataset;
206
- }
207
-
208
- private upgradeProperty(propName: string): void {
209
- const self = this as unknown as Record<string, unknown>;
210
- if (!Object.prototype.hasOwnProperty.call(self, propName)) return;
211
- const value = self[propName];
212
- delete self[propName];
213
- self[propName] = value;
214
- }
215
-
216
148
  set contentRotate(value: unknown) {
217
149
  const normalized = this.parseRotateValue(value);
218
150
  if (normalized === undefined) {
219
151
  this.removeAttribute("content-rotate");
220
- return;
152
+ } else {
153
+ this.setAttribute("content-rotate", String(normalized));
221
154
  }
222
- this.setAttribute("content-rotate", String(normalized));
223
155
  }
224
156
 
225
157
  get contentRotate(): 0 | 90 | 180 | 270 | undefined {
226
158
  return this.parseRotateValue(this.getAttribute("content-rotate"));
227
159
  }
228
160
 
229
- connectedCallback() {
230
- // React may set custom-element properties before upgrade; replay through setters.
231
- this.upgradeProperty("pageBindings");
232
- this.upgradeProperty("contentRotate");
233
- this.upgradeProperty("src");
234
- this.upgradeProperty("onplayrequest");
235
- this.upgradeProperty("onplay");
236
- this.upgradeProperty("onplaying");
237
- this.upgradeProperty("onpause");
238
- this.upgradeProperty("onstop");
239
- this.upgradeProperty("onended");
240
- this.upgradeProperty("ontimeupdate");
241
- this.upgradeProperty("onloadedmetadata");
242
- this.upgradeProperty("onfullscreenchange");
243
- this.upgradeProperty("onwaiting");
244
- this.upgradeProperty("onqualitychange");
245
- this.upgradeProperty("onratechange");
161
+ connectedCallback(): void {
162
+ for (const property of [
163
+ "pageBindings",
164
+ "contentRotate",
165
+ "src",
166
+ "onplayrequest",
167
+ "onplay",
168
+ "onplaying",
169
+ "onpause",
170
+ "onstop",
171
+ "onended",
172
+ "ontimeupdate",
173
+ "onloadedmetadata",
174
+ "onfullscreenchange",
175
+ "onwaiting",
176
+ "onqualitychange",
177
+ "onratechange",
178
+ ]) {
179
+ this.upgradeProperty(property);
180
+ }
246
181
 
247
182
  this.componentId = ensureComponentId(this, "lx-video", this.componentId);
248
- if (!this.componentId) {
183
+ this.registerEventBridge();
184
+
185
+ if (!findInlineNativeRoot(this)) {
186
+ queueMicrotask(() => {
187
+ if (!this.isConnected || findInlineNativeRoot(this)) return;
188
+ this.dispatchEvent(
189
+ new CustomEvent("error", {
190
+ detail: {
191
+ code: "NATIVE_ROOT_INVALID_STRUCTURE",
192
+ message: "LxVideo must be a direct child of LxNativeRoot",
193
+ },
194
+ })
195
+ );
196
+ });
197
+ this.ensurePlaceholderStyle();
249
198
  return;
250
199
  }
251
- this.unregister = registerNativeComponentHandler(this.componentId!, (message) => {
252
- // Handle component events from native
253
- if (message.event) {
254
- // Normalize detail based on event type
255
- let detail = message.detail || message.payload || {};
256
-
257
- // Ensure common state events have empty detail if not provided
258
- if (['playrequest', 'play', 'playing', 'pause', 'stop', 'ended', 'waiting'].includes(message.event)) {
259
- if (Object.keys(detail).length === 0) detail = {};
260
- }
261
-
262
- const ev = new CustomEvent(message.event, {
263
- detail: detail,
264
- bubbles: true,
265
- cancelable: false
266
- });
267
- this.dispatchEvent(ev);
268
- }
269
- });
270
- this.ensurePlaceholderStyle();
271
200
 
272
- // Setup iOS native component rendering helper (no-op on other platforms)
273
- this.iOSHelper = new iOSNativeComponentHelper(this, this.componentId);
274
- this.iOSHelper.setup();
275
- this.startAttrObserver();
276
-
277
- // iOS bootstrap:
278
- // avoid hardcoded timeout; run an immediate mount plus a few frame-based refreshes.
279
- if (isIOS()) {
280
- this.mountOrUpdate(true);
281
- this.startTracking();
282
- this.scheduleIOSBootstrapFrames();
283
- } else {
284
- this.mountOrUpdate();
285
- this.startTracking();
286
- // Windows only: the WebView2 surface is created at an intermediate size
287
- // and resized to its final content bounds shortly after this element
288
- // mounts, which reflows the page and moves the element. Unlike the other
289
- // platforms' webviews, WebView2 doesn't reliably fire the resize /
290
- // ResizeObserver that would make us re-report, and an early update can be
291
- // lost in the bridge transport during page load — so the native overlay
292
- // can stay pinned at the mount-time rect. Re-measure + re-report across the
293
- // settle window to land the final position. No-op on every other platform.
294
- if (isWindows()) {
295
- this.scheduleWindowsMountSettle();
201
+ if (this.hasAttribute("autoplay")) {
202
+ const latchAutoplay = () => {
203
+ if (!this.isConnected || this.islandPlaybackIdle || !this.hasAttribute("autoplay")) return;
204
+ this.setAttribute("data-lx-playing", "true");
205
+ };
206
+ if (typeof requestAnimationFrame === "function") {
207
+ requestAnimationFrame(latchAutoplay);
208
+ } else {
209
+ queueMicrotask(latchAutoplay);
296
210
  }
297
211
  }
212
+ this.ensurePlaceholderStyle();
298
213
  }
299
214
 
300
- disconnectedCallback() {
301
- this.updateState.reset();
302
-
303
- if (this.componentId && !isHarmony()) {
304
- sendNativeComponentMessage({
305
- action: "component.unmount",
306
- id: this.componentId
307
- });
308
- }
309
- this.stopTracking();
310
- if (this.unregister) {
311
- this.unregister();
312
- this.unregister = undefined;
215
+ disconnectedCallback(): void {
216
+ this.unregister?.();
217
+ this.unregister = undefined;
218
+ for (const [name, handler] of Object.entries(this.handlers)) {
219
+ this.removeEventListener(name, handler);
313
220
  }
314
- if (this.harmonyEmbed && this.contains(this.harmonyEmbed)) {
315
- this.removeChild(this.harmonyEmbed);
316
- }
317
- this.harmonyEmbed = undefined;
318
- this.lastHarmonyProps = undefined;
319
-
320
- // Cleanup iOS helper
321
- this.iOSHelper?.cleanup();
322
- this.iOSHelper = undefined;
323
- this.stopAttrObserver();
324
-
325
- // Cleanup manually attached property handlers
326
- Object.keys(this._handlers).forEach(name => {
327
- this.removeEventListener(name, this._handlers[name]);
328
- });
329
- this._handlers = {};
221
+ this.handlers = {};
330
222
  this.rawHandlers = {};
331
223
  }
332
224
 
333
- attributeChangedCallback(name: string) {
334
- if (name === "id") {
335
- const prev = this.componentId;
225
+ attributeChangedCallback(name: string): void {
226
+ if (name === "id" && this.isConnected) {
227
+ const previous = this.componentId;
336
228
  this.componentId = ensureComponentId(this, "lx-video", this.componentId);
337
- if (prev && this.componentId !== prev) {
338
- this.mounted = false;
339
- this.updateState.reset();
340
- }
229
+ if (previous !== this.componentId) this.registerEventBridge();
341
230
  }
342
- if (!this.isConnected) return;
343
- if (name === "poster" || name === "object-fit" || name === "objectfit") {
231
+ if (name === "poster" || name === "object-fit") {
344
232
  this.syncPosterPlaceholder();
345
233
  }
346
- // Keep content-rotate/object-fit updates immediate across all native platforms.
347
- const forcePropsUpdate = name === "content-rotate" || name === "object-fit" || name === "objectfit";
348
- if (isHarmony() && forcePropsUpdate) {
349
- // Bridge callbacks can race on Harmony; force a deterministic embed recreate for visual props.
350
- this.forceHarmonyEmbedRecreate = true;
351
- }
352
- this.mountOrUpdate(forcePropsUpdate);
353
234
  }
354
235
 
355
- private isEventListener(value: unknown): value is EventListenerOrEventListenerObject {
356
- if (typeof value === "function") return true;
357
- if (!value || typeof value !== "object") return false;
358
- return typeof (value as EventListenerObject).handleEvent === "function";
236
+ private registerEventBridge(): void {
237
+ this.unregister?.();
238
+ if (!this.componentId) return;
239
+ this.unregister = registerNativeComponentHandler(this.componentId, (message) => {
240
+ if (!message.event) return;
241
+ const detail = normalizeVideoEventDetail(message.event, message.detail || message.payload || {});
242
+ if (message.event === "playing" || message.event === "play") {
243
+ this.islandPlaybackIdle = false;
244
+ this.setAttribute("data-lx-playing", "true");
245
+ } else if (["pause", "stop", "ended"].includes(message.event)) {
246
+ this.islandPlaybackIdle = true;
247
+ this.removeAttribute("data-lx-playing");
248
+ }
249
+ this.dispatchEvent(
250
+ new CustomEvent(message.event, {
251
+ detail,
252
+ bubbles: true,
253
+ cancelable: false,
254
+ })
255
+ );
256
+ });
359
257
  }
360
258
 
361
- private setEventHandler(name: string, value: unknown) {
362
- const current = this._handlers[name];
363
- if (current) {
364
- this.removeEventListener(name, current);
365
- }
366
-
367
- if (this.isEventListener(value)) {
368
- const listener =
369
- typeof value === "function"
370
- ? ({ handleEvent: value } as EventListenerObject)
371
- : value;
372
- this._handlers[name] = listener;
373
- this.rawHandlers[name] = value;
374
- this.addEventListener(name, listener);
375
- } else {
376
- delete this._handlers[name];
377
- delete this.rawHandlers[name];
378
- }
259
+ private requestRootCompile(): void {
260
+ const root = findInlineNativeRoot(this) as (HTMLElement & { retry?: () => Promise<void> }) | null;
261
+ void root?.retry?.();
379
262
  }
380
263
 
381
- private getEventHandler(name: string) {
382
- return (this.rawHandlers[name] || null) as any;
264
+ private upgradeProperty(property: string): void {
265
+ const self = this as unknown as Record<string, unknown>;
266
+ if (!Object.prototype.hasOwnProperty.call(self, property)) return;
267
+ const value = self[property];
268
+ delete self[property];
269
+ self[property] = value;
383
270
  }
384
271
 
385
- // Standard Media Events for React Props (e.g. <lx-video onPlay={...} />)
386
- set onplayrequest(cb: EventListener) { this.setEventHandler('playrequest', cb); }
387
- get onplayrequest() { return this.getEventHandler('playrequest'); }
388
-
389
- set onplay(cb: EventListener) { this.setEventHandler('play', cb); }
390
- get onplay() { return this.getEventHandler('play'); }
391
-
392
- set onplaying(cb: EventListener) { this.setEventHandler('playing', cb); }
393
- get onplaying() { return this.getEventHandler('playing'); }
394
-
395
- set onpause(cb: EventListener) { this.setEventHandler('pause', cb); }
396
- get onpause() { return this.getEventHandler('pause'); }
397
-
398
- set onstop(cb: EventListener) { this.setEventHandler('stop', cb); }
399
- get onstop() { return this.getEventHandler('stop'); }
400
-
401
- set onended(cb: EventListener) { this.setEventHandler('ended', cb); }
402
- get onended() { return this.getEventHandler('ended'); }
403
-
404
- set ontimeupdate(cb: EventListener) { this.setEventHandler('timeupdate', cb); }
405
- get ontimeupdate() { return this.getEventHandler('timeupdate'); }
406
-
407
- // Note: onerror is inherited from HTMLElement with OnErrorEventHandler type.
408
- // Use addEventListener('error', handler) for error events instead.
409
-
410
- set onloadedmetadata(cb: EventListener) { this.setEventHandler('loadedmetadata', cb); }
411
- get onloadedmetadata() { return this.getEventHandler('loadedmetadata'); }
412
-
413
- set onfullscreenchange(cb: EventListener) { this.setEventHandler('fullscreenchange', cb); }
414
- get onfullscreenchange() { return this.getEventHandler('fullscreenchange'); }
415
-
416
- set onwaiting(cb: EventListener) { this.setEventHandler('waiting', cb); }
417
- get onwaiting() { return this.getEventHandler('waiting'); }
272
+ private parseRotateValue(value: unknown): 0 | 90 | 180 | 270 | undefined {
273
+ const parsed =
274
+ typeof value === "number"
275
+ ? value
276
+ : typeof value === "string" && /^(0|90|180|270)$/.test(value.trim())
277
+ ? Number(value)
278
+ : Number.NaN;
279
+ return parsed === 0 || parsed === 90 || parsed === 180 || parsed === 270
280
+ ? parsed
281
+ : undefined;
282
+ }
418
283
 
419
- set onqualitychange(cb: EventListener) { this.setEventHandler('qualitychange', cb); }
420
- get onqualitychange() { return this.getEventHandler('qualitychange'); }
284
+ private parseObjectFitValue(value: unknown): LxObjectFit | undefined {
285
+ if (typeof value !== "string") return undefined;
286
+ const normalized = value.trim().toLowerCase();
287
+ return normalized === "cover" ||
288
+ normalized === "contain" ||
289
+ normalized === "fill" ||
290
+ normalized === "fit"
291
+ ? normalized
292
+ : undefined;
293
+ }
421
294
 
422
- set onratechange(cb: EventListener) { this.setEventHandler('ratechange', cb); }
423
- get onratechange() { return this.getEventHandler('ratechange'); }
295
+ private isEventListener(value: unknown): value is EventListenerOrEventListenerObject {
296
+ return (
297
+ typeof value === "function" ||
298
+ (!!value &&
299
+ typeof value === "object" &&
300
+ typeof (value as EventListenerObject).handleEvent === "function")
301
+ );
302
+ }
424
303
 
425
- // Internal
426
- private ensurePlaceholderStyle() {
304
+ private setEventHandler(name: string, value: unknown): void {
305
+ const current = this.handlers[name];
306
+ if (current) this.removeEventListener(name, current);
307
+ if (!this.isEventListener(value)) {
308
+ delete this.handlers[name];
309
+ delete this.rawHandlers[name];
310
+ return;
311
+ }
312
+ const listener =
313
+ typeof value === "function" ? ({ handleEvent: value } as EventListenerObject) : value;
314
+ this.handlers[name] = listener;
315
+ this.rawHandlers[name] = value;
316
+ this.addEventListener(name, listener);
317
+ }
318
+
319
+ private getEventHandler(name: string): any {
320
+ return this.rawHandlers[name] || null;
321
+ }
322
+
323
+ set onplayrequest(value: EventListener) { this.setEventHandler("playrequest", value); }
324
+ get onplayrequest() { return this.getEventHandler("playrequest"); }
325
+ set onplay(value: EventListener) { this.setEventHandler("play", value); }
326
+ get onplay() { return this.getEventHandler("play"); }
327
+ set onplaying(value: EventListener) { this.setEventHandler("playing", value); }
328
+ get onplaying() { return this.getEventHandler("playing"); }
329
+ set onpause(value: EventListener) { this.setEventHandler("pause", value); }
330
+ get onpause() { return this.getEventHandler("pause"); }
331
+ set onstop(value: EventListener) { this.setEventHandler("stop", value); }
332
+ get onstop() { return this.getEventHandler("stop"); }
333
+ set onended(value: EventListener) { this.setEventHandler("ended", value); }
334
+ get onended() { return this.getEventHandler("ended"); }
335
+ set ontimeupdate(value: EventListener) { this.setEventHandler("timeupdate", value); }
336
+ get ontimeupdate() { return this.getEventHandler("timeupdate"); }
337
+ set onloadedmetadata(value: EventListener) { this.setEventHandler("loadedmetadata", value); }
338
+ get onloadedmetadata() { return this.getEventHandler("loadedmetadata"); }
339
+ set onfullscreenchange(value: EventListener) { this.setEventHandler("fullscreenchange", value); }
340
+ get onfullscreenchange() { return this.getEventHandler("fullscreenchange"); }
341
+ set onwaiting(value: EventListener) { this.setEventHandler("waiting", value); }
342
+ get onwaiting() { return this.getEventHandler("waiting"); }
343
+ set onqualitychange(value: EventListener) { this.setEventHandler("qualitychange", value); }
344
+ get onqualitychange() { return this.getEventHandler("qualitychange"); }
345
+ set onratechange(value: EventListener) { this.setEventHandler("ratechange", value); }
346
+ get onratechange() { return this.getEventHandler("ratechange"); }
347
+
348
+ private ensurePlaceholderStyle(): void {
427
349
  if (!this.style.display) this.style.display = "block";
428
- if (!this.style.position) this.style.position = "relative"; // Needed for iOS scroll container
350
+ if (!this.style.position) this.style.position = "relative";
429
351
  if (!this.style.backgroundColor) this.style.backgroundColor = "black";
430
352
  if (!this.style.aspectRatio) this.style.aspectRatio = "16 / 9";
431
353
  this.syncPosterPlaceholder();
432
354
  }
433
355
 
434
- /// The poster doubles as the element's CSS background: on platforms
435
- /// where the native layer hides while no media is presented (Windows:
436
- /// initial state, after stop, on error), the placeholder shows it.
437
- private syncPosterPlaceholder() {
356
+ private syncPosterPlaceholder(): void {
438
357
  const poster = this.getAttribute("poster");
439
358
  if (!poster) {
440
359
  this.style.backgroundImage = "";
441
360
  return;
442
361
  }
443
- const objectFit = this.parseObjectFitValue(
444
- this.getAttribute("object-fit") ?? this.getAttribute("objectfit")
445
- );
446
- const size =
447
- objectFit === "cover" ? "cover" : objectFit === "fill" ? "100% 100%" : "contain";
362
+ const objectFit = this.parseObjectFitValue(this.getAttribute("object-fit"));
363
+ const size = objectFit === "cover" ? "cover" : objectFit === "fill" ? "100% 100%" : "contain";
448
364
  this.style.backgroundImage = `url("${poster.replace(/"/g, '\\"')}")`;
449
365
  this.style.backgroundSize = size;
450
366
  this.style.backgroundPosition = "center";
451
367
  this.style.backgroundRepeat = "no-repeat";
452
368
  }
453
-
454
- private collectProps() {
455
- const volumeAttr = this.getAttribute("volume");
456
- const volume =
457
- volumeAttr != null ? parseFloat(volumeAttr) : undefined;
458
- const rawObjectFit =
459
- this.getAttribute("object-fit") ??
460
- this.getAttribute("objectfit");
461
- const objectFit = this.parseObjectFitValue(rawObjectFit);
462
-
463
- // JSON-encoded arrays (React wrapper sets these via JSON.stringify)
464
- const qualitiesAttr = this.getAttribute("qualities");
465
- let qualities: LxVideoQuality[] | undefined;
466
- if (qualitiesAttr) {
467
- try {
468
- const parsed = JSON.parse(qualitiesAttr);
469
- if (Array.isArray(parsed)) {
470
- qualities = parsed.filter((q): q is LxVideoQuality =>
471
- q && typeof q === "object" && typeof (q as { label?: unknown }).label === "string"
472
- );
473
- }
474
- } catch {}
475
- }
476
-
477
- // Parse playbackRates array from JSON attribute
478
- const playbackRatesAttr = this.getAttribute("playback-rates");
479
- let playbackRates: number[] | undefined;
480
- if (playbackRatesAttr) {
481
- try {
482
- const parsed = JSON.parse(playbackRatesAttr);
483
- if (Array.isArray(parsed)) {
484
- playbackRates = parsed.filter((n): n is number => typeof n === "number");
485
- }
486
- } catch {}
487
- }
488
-
489
- const isLive = this.hasAttribute("live");
490
- // When live, default progressBar to false unless explicitly enabled.
491
- const progressBarAttr = this.getAttribute("progress-bar");
492
- const progressBar = isLive
493
- ? (progressBarAttr !== null && progressBarAttr !== "false")
494
- : (progressBarAttr !== "false");
495
-
496
- const rawContentRotate = this.getAttribute("content-rotate");
497
- const validContentRotate = this.parseRotateValue(rawContentRotate);
498
- const pageFuncBindings = this._pageBindings;
499
- const dataset = this.collectDataset();
500
- const clearProps: string[] = [];
501
- if (rawContentRotate === null || validContentRotate === undefined) {
502
- clearProps.push("contentRotate");
503
- }
504
- if (rawObjectFit == null || objectFit === undefined) {
505
- clearProps.push("objectFit");
506
- }
507
-
508
- return {
509
- src: this.getAttribute("src") || undefined,
510
- poster: this.getAttribute("poster") || undefined,
511
- autoplay: this.hasAttribute("autoplay"),
512
- loop: this.hasAttribute("loop"),
513
- muted: this.hasAttribute("muted"),
514
- controls: this.hasAttribute("controls"),
515
- progressBar,
516
- live: isLive,
517
- volume: !Number.isNaN(volume ?? NaN) ? volume : undefined,
518
- objectFit,
519
- contentRotate: validContentRotate,
520
- ...(clearProps.length > 0 ? { __clearProps: clearProps } : {}),
521
- qualities,
522
- playbackRates,
523
- dataset,
524
- datasetJson: JSON.stringify(dataset),
525
- pageFuncBindings,
526
- pageFuncBindingsJson: JSON.stringify(pageFuncBindings)
527
- };
528
- }
529
-
530
- private shouldRefreshForAttribute(name: string): boolean {
531
- const normalized = name.trim().toLowerCase();
532
- return normalized.startsWith("data-");
533
- }
534
-
535
- private startAttrObserver(): void {
536
- if (typeof MutationObserver === "undefined" || this.attrObserver) {
537
- return;
538
- }
539
- this.attrObserver = new MutationObserver((mutations) => {
540
- for (const mutation of mutations) {
541
- const attrName = mutation.attributeName;
542
- if (!attrName || !this.shouldRefreshForAttribute(attrName)) {
543
- continue;
544
- }
545
- this.mountOrUpdate(true);
546
- break;
547
- }
548
- });
549
- this.attrObserver.observe(this, { attributes: true });
550
- }
551
-
552
- private stopAttrObserver(): void {
553
- if (!this.attrObserver) {
554
- return;
555
- }
556
- this.attrObserver.disconnect();
557
- this.attrObserver = undefined;
558
- }
559
-
560
- private measureForNative() {
561
- const measured = measureElement(this);
562
- if (!isIOS()) {
563
- return measured;
564
- }
565
- const rect = this.getBoundingClientRect();
566
- return {
567
- rect: {
568
- x: rect.left,
569
- y: rect.top,
570
- width: rect.width,
571
- height: rect.height
572
- },
573
- cornerRadius: measured.cornerRadius
574
- };
575
- }
576
-
577
- private mountOrUpdate(forceUpdate = false) {
578
- if (isHarmony()) {
579
- this.mountOrUpdateHarmony(forceUpdate);
580
- return;
581
- }
582
- if (!this.componentId) return;
583
- const { rect, cornerRadius } = this.measureForNative();
584
- const hasSize = rect.width > 0 && rect.height > 0;
585
- if (!hasSize) {
586
- // Defer and try again once layout stabilizes; only needed before first mount
587
- if (!this.mounted) {
588
- this.scheduleMountRetry();
589
- }
590
- return;
591
- }
592
- const props = this.collectProps();
593
- if (cornerRadius !== undefined) {
594
- (props as any).cornerRadius = cornerRadius;
595
- }
596
- const zIndex = parseFloat(this.style.zIndex || "0") || 0;
597
- const decision = this.updateState.decide(rect, props, zIndex, !this.mounted || forceUpdate);
598
- if (!decision.shouldSend) return;
599
-
600
- const payload: any = {
601
- action: this.mounted ? "component.update" : "component.mount",
602
- id: this.componentId,
603
- type: "video.native",
604
- rect,
605
- zIndex,
606
- ...(cornerRadius !== undefined ? { cornerRadius } : {})
607
- };
608
-
609
- // Enhance payload with platform-specific data (iOS scroll container rect)
610
- this.iOSHelper?.enhancePayload(payload);
611
-
612
- // Only include props when they changed or on first mount
613
- if (decision.propsChanged || !this.mounted) {
614
- payload.props = props;
615
- }
616
-
617
- sendNativeComponentMessage(payload);
618
- this.mounted = true;
619
- }
620
-
621
- private updatePosition() {
622
- if (isHarmony()) {
623
- this.mountOrUpdateHarmony();
624
- return;
625
- }
626
- if (!this.mounted || !this.componentId) return;
627
- const { rect, cornerRadius } = this.measureForNative();
628
- if (!rect.width || !rect.height) return;
629
- const zIndex = parseFloat(this.style.zIndex || "0") || 0;
630
- const decision = this.updateState.decide(rect, null, zIndex);
631
- if (!decision.shouldSend) return;
632
-
633
- const payload: any = {
634
- action: "component.update",
635
- id: this.componentId,
636
- rect,
637
- zIndex
638
- };
639
-
640
- // Keep iOS scroll-container metadata on update so native side can still
641
- // promote fallback overlay into WKChildScrollView when it becomes available.
642
- this.iOSHelper?.enhancePayload(payload);
643
-
644
- if (cornerRadius !== undefined) {
645
- payload.cornerRadius = cornerRadius;
646
- }
647
-
648
- sendNativeComponentMessage(payload);
649
- }
650
-
651
- /**
652
- * Start tracking component size changes.
653
- */
654
- private startTracking() {
655
- window.addEventListener("resize", this.resizeEventListener);
656
- if (isIOS()) {
657
- window.addEventListener("scroll", this.iOSScrollEventListener, { passive: true });
658
- } else if (isWindows()) {
659
- window.addEventListener("scroll", this.containerScrollListener, {
660
- capture: true,
661
- passive: true,
662
- });
663
- }
664
- if (isAndroid()) {
665
- this.removeLayoutInvalidationListener = addNativeComponentLayoutInvalidationListener(this.resizeEventListener);
666
- }
667
- this.startSizeObserver();
668
- this.updatePosition();
669
- }
670
-
671
- private stopTracking() {
672
- window.removeEventListener("resize", this.resizeEventListener);
673
- if (isIOS()) {
674
- window.removeEventListener("scroll", this.iOSScrollEventListener);
675
- } else if (isWindows()) {
676
- window.removeEventListener("scroll", this.containerScrollListener, { capture: true });
677
- }
678
- this.removeLayoutInvalidationListener?.();
679
- this.removeLayoutInvalidationListener = undefined;
680
- this.stopSizeObserver();
681
- if (this.scrollSettleFrame !== null) {
682
- cancelAnimationFrame(this.scrollSettleFrame);
683
- this.scrollSettleFrame = null;
684
- }
685
- if (this.winSettleFrame !== null) {
686
- cancelAnimationFrame(this.winSettleFrame);
687
- this.winSettleFrame = null;
688
- }
689
- if (this.pendingLayoutFrame !== null) {
690
- cancelAnimationFrame(this.pendingLayoutFrame);
691
- this.pendingLayoutFrame = null;
692
- }
693
- if (this.pendingHarmonyRetryTimer !== null) {
694
- clearTimeout(this.pendingHarmonyRetryTimer);
695
- this.pendingHarmonyRetryTimer = null;
696
- }
697
- if (this.iOSBootstrapFrame !== null) {
698
- cancelAnimationFrame(this.iOSBootstrapFrame);
699
- this.iOSBootstrapFrame = null;
700
- }
701
- this.iOSBootstrapRemaining = 0;
702
- this.pendingHarmonyProps = undefined;
703
- this.pendingHarmonyRetryCount = 0;
704
- }
705
-
706
- private startSizeObserver() {
707
- if (typeof ResizeObserver === "undefined") {
708
- // ResizeObserver unavailable: retry mount on next frame.
709
- this.scheduleMountRetry();
710
- return;
711
- }
712
- if (this.resizeObserver) return;
713
- this.resizeObserver = new ResizeObserver((entries) => {
714
- for (const entry of entries) {
715
- if (entry.target !== this) continue;
716
- const { width, height } = entry.contentRect;
717
- if (!width || !height) continue;
718
- if (this.mounted) {
719
- this.updatePosition();
720
- } else {
721
- this.mountOrUpdate();
722
- }
723
- }
724
- });
725
- this.resizeObserver.observe(this);
726
- }
727
-
728
- private stopSizeObserver() {
729
- if (this.resizeObserver) {
730
- this.resizeObserver.disconnect();
731
- this.resizeObserver = undefined;
732
- }
733
- }
734
-
735
- private scheduleMountRetry() {
736
- if (this.pendingLayoutFrame !== null || this.mounted) return;
737
- this.pendingLayoutFrame = requestAnimationFrame(() => {
738
- this.pendingLayoutFrame = null;
739
- this.mountOrUpdate();
740
- });
741
- }
742
-
743
- private scheduleWindowsMountSettle(): void {
744
- if (typeof requestAnimationFrame === "undefined") return;
745
- if (this.winSettleFrame !== null) return;
746
- // Re-measure across the page-load / WebView2-resize settle window. The
747
- // reflow that moves this element can land well after mount, so keep checking
748
- // until the rect has been stable for a while; decide() dedups, so updates
749
- // only fire when the rect changes.
750
- let lastKey = "";
751
- let stable = 0;
752
- let elapsed = 0;
753
- const tick = () => {
754
- this.winSettleFrame = null;
755
- if (!this.isConnected) return;
756
- // Force the re-send (don't dedup): an early update can be lost in the
757
- // bridge transport during page load, and once the element has settled a
758
- // dedup'd re-measure sees "no change" and never re-delivers it. Forcing
759
- // each frame re-delivers the current rect until it lands.
760
- this.mountOrUpdate(true);
761
- const r = this.getBoundingClientRect();
762
- const key = `${Math.round(r.top)}:${Math.round(r.width)}`;
763
- if (key === lastKey) {
764
- stable += 1;
765
- } else {
766
- stable = 0;
767
- lastKey = key;
768
- }
769
- elapsed += 1;
770
- // Keep re-sending until the layout has held steady for a while (so a late
771
- // settle is still caught), capped so it never runs unbounded.
772
- if (stable < 12 && elapsed < 180) {
773
- this.winSettleFrame = requestAnimationFrame(tick);
774
- }
775
- };
776
- this.winSettleFrame = requestAnimationFrame(tick);
777
- }
778
-
779
- private scheduleIOSBootstrapFrames(): void {
780
- if (!isIOS()) return;
781
- // Keep a short frame-based bootstrap window so late-created WKChildScrollView
782
- // can still receive mount/update without relying on hardcoded millisecond delay.
783
- this.iOSBootstrapRemaining = 8;
784
- const tick = () => {
785
- if (!this.isConnected) {
786
- this.iOSBootstrapFrame = null;
787
- this.iOSBootstrapRemaining = 0;
788
- return;
789
- }
790
- this.mountOrUpdate(true);
791
- this.iOSBootstrapRemaining -= 1;
792
- if (this.iOSBootstrapRemaining <= 0) {
793
- this.iOSBootstrapFrame = null;
794
- return;
795
- }
796
- this.iOSBootstrapFrame = requestAnimationFrame(tick);
797
- };
798
- this.iOSBootstrapFrame = requestAnimationFrame(tick);
799
- }
800
-
801
- private mountOrUpdateHarmony(forceUpdate = false) {
802
- if (!this.componentId) return;
803
- const { rect, cornerRadius } = measureElement(this);
804
- const hasSize = rect.width > 0 && rect.height > 0;
805
- if (!hasSize) {
806
- if (!this.mounted) {
807
- this.scheduleMountRetry();
808
- }
809
- return;
810
- }
811
- const props = this.collectProps();
812
- if (cornerRadius !== undefined) {
813
- (props as any).cornerRadius = cornerRadius;
814
- }
815
- const zIndex = parseFloat(this.style.zIndex || "0") || 0;
816
- const decision = this.updateState.decide(rect, props, zIndex, !this.mounted || forceUpdate);
817
- if (!decision.shouldSend) return;
818
- this.ensureHarmonyEmbed(
819
- rect,
820
- props,
821
- cornerRadius,
822
- this.forceHarmonyEmbedRecreate
823
- );
824
- this.forceHarmonyEmbedRecreate = false;
825
- this.mounted = true;
826
- }
827
-
828
- private ensureHarmonyEmbed(
829
- rect: { width: number; height: number },
830
- props: Record<string, unknown>,
831
- cornerRadius?: number,
832
- forceRecreate = false
833
- ) {
834
- const createEmbed = (): HTMLEmbedElement => {
835
- const embed = document.createElement("embed");
836
- embed.setAttribute("type", "native/video");
837
- embed.setAttribute("width", `${rect.width}`);
838
- embed.setAttribute("height", `${rect.height}`);
839
- embed.setAttribute("id", this.componentId!);
840
- embed.setAttribute("data-lx-component-id", this.componentId!);
841
- embed.style.display = "block";
842
- embed.style.width = "100%";
843
- embed.style.height = "100%";
844
- embed.style.border = "none";
845
- if (cornerRadius !== undefined) {
846
- embed.style.borderRadius = `${cornerRadius}px`;
847
- embed.style.overflow = "hidden";
848
- }
849
- const encodedProps = this.encodeHarmonyProps(props);
850
- embed.setAttribute("src", encodedProps);
851
- this.lastHarmonyProps = encodedProps;
852
- return embed;
853
- };
854
-
855
- if (forceRecreate && this.harmonyEmbed) {
856
- const oldEmbed = this.harmonyEmbed;
857
- this.harmonyEmbed = undefined;
858
- if (this.contains(oldEmbed)) {
859
- this.removeChild(oldEmbed);
860
- }
861
- this.lastHarmonyProps = undefined;
862
- const recreated = createEmbed();
863
- this.appendChild(recreated);
864
- this.harmonyEmbed = recreated;
865
- this.pushHarmonyPropsUpdate(props);
866
- this.pendingHarmonyRetryCount = 0;
867
- return;
868
- }
869
-
870
- // Create embed element only once - ArkWeb triggers DESTROY+CREATE on attribute changes
871
- if (!this.harmonyEmbed) {
872
- const embed = createEmbed();
873
- this.pushHarmonyPropsUpdate(props);
874
-
875
- this.appendChild(embed);
876
- this.harmonyEmbed = embed;
877
- return;
878
- }
879
-
880
- // Only update props when they changed.
881
- const encodedProps = this.encodeHarmonyProps(props);
882
- if (encodedProps !== this.lastHarmonyProps) {
883
- const pushed = this.pushHarmonyPropsUpdate(props);
884
- if (pushed) {
885
- this.lastHarmonyProps = encodedProps;
886
- this.pendingHarmonyProps = undefined;
887
- this.pendingHarmonyRetryCount = 0;
888
- } else {
889
- this.scheduleHarmonyPropsRetry(props);
890
- }
891
- }
892
-
893
- // Ensure embed element matches latest corner radius for clipping
894
- if (cornerRadius !== undefined && this.harmonyEmbed.style.borderRadius !== `${cornerRadius}px`) {
895
- this.harmonyEmbed.style.borderRadius = `${cornerRadius}px`;
896
- this.harmonyEmbed.style.overflow = "hidden";
897
- }
898
- }
899
-
900
- private encodeHarmonyProps(props: Record<string, unknown>): string {
901
- try {
902
- const payload = { componentId: this.componentId, ...props };
903
- return `${HARMONY_PROPS_PREFIX}${encodeURIComponent(
904
- JSON.stringify(payload)
905
- )}`;
906
- } catch (_e) {
907
- return `${HARMONY_PROPS_PREFIX}%7B%7D`;
908
- }
909
- }
910
-
911
- private pushHarmonyPropsUpdate(props: Record<string, unknown>): boolean {
912
- if (!this.componentId || !isHarmony()) return false;
913
- try {
914
- const proxy = (window as unknown as Record<string, unknown>).LingXiaProxy as
915
- | { nativeComponentUpdate?: ((componentId: string, payload: string) => void) | ((payload: string) => void) }
916
- | undefined;
917
- const updateFn = proxy?.nativeComponentUpdate;
918
- if (typeof updateFn !== "function") {
919
- return false;
920
- }
921
- const payload = JSON.stringify({ componentId: this.componentId, ...props });
922
- try {
923
- (updateFn as (payload: string) => void).call(proxy, payload);
924
- return true;
925
- } catch {
926
- try {
927
- (updateFn as (componentId: string, payload: string) => void).call(proxy, this.componentId, payload);
928
- return true;
929
- } catch {
930
- return false;
931
- }
932
- }
933
- } catch (error) {
934
- return false;
935
- }
936
- }
937
-
938
- private scheduleHarmonyPropsRetry(props: Record<string, unknown>): void {
939
- this.pendingHarmonyProps = { ...props };
940
- if (this.pendingHarmonyRetryTimer !== null) {
941
- return;
942
- }
943
- this.pendingHarmonyRetryTimer = window.setTimeout(() => {
944
- this.pendingHarmonyRetryTimer = null;
945
- if (!this.isConnected || !this.pendingHarmonyProps) {
946
- return;
947
- }
948
- const pendingProps = this.pendingHarmonyProps;
949
- const pushed = this.pushHarmonyPropsUpdate(pendingProps);
950
- if (pushed) {
951
- this.lastHarmonyProps = this.encodeHarmonyProps(pendingProps);
952
- this.pendingHarmonyProps = undefined;
953
- this.pendingHarmonyRetryCount = 0;
954
- } else {
955
- this.pendingHarmonyRetryCount += 1;
956
- if (this.pendingHarmonyRetryCount < 20) {
957
- this.scheduleHarmonyPropsRetry(pendingProps);
958
- }
959
- }
960
- }, 100);
961
- }
962
-
963
369
  }
964
370
 
965
- export function registerVideoComponent() {
371
+ export function registerVideoComponent(): void {
966
372
  if (!customElements.get("lx-video")) {
967
373
  customElements.define("lx-video", LxVideoElement);
968
374
  }