@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.
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/inline-native/commit.d.ts +39 -0
- package/dist/inline-native/commit.js +94 -0
- package/dist/inline-native/elements.d.ts +104 -0
- package/dist/inline-native/elements.js +749 -0
- package/dist/inline-native/errors.d.ts +8 -0
- package/dist/inline-native/errors.js +23 -0
- package/dist/inline-native/events.d.ts +10 -0
- package/dist/inline-native/events.js +16 -0
- package/dist/inline-native/geometry.d.ts +78 -0
- package/dist/inline-native/geometry.js +40 -0
- package/dist/inline-native/identity.d.ts +28 -0
- package/dist/inline-native/identity.js +51 -0
- package/dist/inline-native/index.d.ts +14 -0
- package/dist/inline-native/index.js +19 -0
- package/dist/inline-native/layout.d.ts +2 -0
- package/dist/inline-native/layout.js +58 -0
- package/dist/inline-native/lease.d.ts +13 -0
- package/dist/inline-native/lease.js +31 -0
- package/dist/inline-native/runtime.d.ts +64 -0
- package/dist/inline-native/runtime.js +119 -0
- package/dist/inline-native/schema.d.ts +111 -0
- package/dist/inline-native/schema.js +210 -0
- package/dist/inline-native/structure.d.ts +22 -0
- package/dist/inline-native/structure.js +706 -0
- package/dist/inline-native/style.d.ts +7 -0
- package/dist/inline-native/style.js +119 -0
- package/dist/inline-native/types.d.ts +82 -0
- package/dist/inline-native/types.js +7 -0
- package/dist/inline-native/unwrap.d.ts +10 -0
- package/dist/inline-native/unwrap.js +32 -0
- package/dist/inline-native/video.d.ts +65 -0
- package/dist/inline-native/video.js +69 -0
- package/dist/native_component_wrapper_shared.d.ts +3 -2
- package/dist/native_component_wrapper_shared.js +6 -3
- package/dist/nativecomponent.d.ts +1 -1
- package/dist/nativecomponent.js +8 -11
- package/dist/navigator.d.ts +2 -2
- package/dist/navigator.js +5 -5
- package/dist/video-events.d.ts +2 -0
- package/dist/video-events.js +18 -0
- package/dist/video.d.ts +98 -79
- package/dist/video.js +182 -757
- package/package.json +2 -1
- package/src/index.ts +58 -1
- package/src/inline-native/commit.ts +141 -0
- package/src/inline-native/elements.ts +829 -0
- package/src/inline-native/errors.ts +35 -0
- package/src/inline-native/events.ts +31 -0
- package/src/inline-native/geometry.ts +86 -0
- package/src/inline-native/identity.ts +102 -0
- package/src/inline-native/index.ts +103 -0
- package/src/inline-native/layout.ts +58 -0
- package/src/inline-native/lease.ts +48 -0
- package/src/inline-native/runtime.ts +185 -0
- package/src/inline-native/schema.ts +247 -0
- package/src/inline-native/structure.ts +823 -0
- package/src/inline-native/style.ts +111 -0
- package/src/inline-native/types.ts +105 -0
- package/src/inline-native/unwrap.ts +34 -0
- package/src/inline-native/video.ts +125 -0
- package/src/native_component_wrapper_shared.ts +8 -6
- package/src/nativecomponent.ts +8 -13
- package/src/navigator.ts +9 -9
- package/src/video-events.ts +16 -0
- package/src/video.ts +245 -839
package/src/video.ts
CHANGED
|
@@ -1,17 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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[];
|
|
30
|
-
playbackRates?: number[];
|
|
54
|
+
qualities?: LxVideoQuality[];
|
|
55
|
+
playbackRates?: number[];
|
|
31
56
|
className?: string;
|
|
32
|
-
style?:
|
|
33
|
-
ref?:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
85
|
-
private updateState = new NativeComponentUpdateState();
|
|
113
|
+
private islandPlaybackIdle = false;
|
|
86
114
|
private unregister?: () => void;
|
|
87
|
-
private
|
|
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
|
|
117
|
+
private bindings: Record<string, string> = {};
|
|
125
118
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
159
|
-
this.
|
|
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
|
|
166
|
-
return this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
338
|
-
this.mounted = false;
|
|
339
|
-
this.updateState.reset();
|
|
340
|
-
}
|
|
229
|
+
if (previous !== this.componentId) this.registerEventBridge();
|
|
341
230
|
}
|
|
342
|
-
if (
|
|
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
|
|
356
|
-
|
|
357
|
-
if (!
|
|
358
|
-
|
|
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
|
|
362
|
-
const
|
|
363
|
-
|
|
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
|
|
382
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
423
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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";
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|