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