@xmaxai/sdk 0.1.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 (38) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +201 -0
  4. package/dist/chunk-67XSD3QJ.js +2673 -0
  5. package/dist/chunk-67XSD3QJ.js.map +1 -0
  6. package/dist/chunk-74ZZWUPI.js +992 -0
  7. package/dist/chunk-74ZZWUPI.js.map +1 -0
  8. package/dist/chunk-EGNRAZ5U.js +432 -0
  9. package/dist/chunk-EGNRAZ5U.js.map +1 -0
  10. package/dist/chunk-PLU2YVAS.js +414 -0
  11. package/dist/chunk-PLU2YVAS.js.map +1 -0
  12. package/dist/experimental/index.cjs +3897 -0
  13. package/dist/experimental/index.cjs.map +1 -0
  14. package/dist/experimental/index.d.cts +2 -0
  15. package/dist/experimental/index.d.ts +2 -0
  16. package/dist/experimental/index.js +5 -0
  17. package/dist/experimental/index.js.map +1 -0
  18. package/dist/index-BIUk_kvb.d.ts +350 -0
  19. package/dist/index-Dai62Sos.d.cts +350 -0
  20. package/dist/index.cjs +5109 -0
  21. package/dist/index.cjs.map +1 -0
  22. package/dist/index.d.cts +235 -0
  23. package/dist/index.d.ts +235 -0
  24. package/dist/index.js +359 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/local-publish-diagnostics-ABLL56IT.js +79 -0
  27. package/dist/local-publish-diagnostics-ABLL56IT.js.map +1 -0
  28. package/dist/react/index.cjs +4304 -0
  29. package/dist/react/index.cjs.map +1 -0
  30. package/dist/react/index.d.cts +112 -0
  31. package/dist/react/index.d.ts +112 -0
  32. package/dist/react/index.js +817 -0
  33. package/dist/react/index.js.map +1 -0
  34. package/dist/rtc-manager-gYDkADGw.d.cts +412 -0
  35. package/dist/rtc-manager-gYDkADGw.d.ts +412 -0
  36. package/dist/video-file-stream-UXJ6TNIB.js +4 -0
  37. package/dist/video-file-stream-UXJ6TNIB.js.map +1 -0
  38. package/package.json +82 -0
@@ -0,0 +1,2673 @@
1
+ import { createSDKI18n, resolveApiErrorMessage, resolvePlaybackDrivenEncoderFps, mountCaptureVideo } from './chunk-PLU2YVAS.js';
2
+ import COS from 'cos-js-sdk-v5';
3
+ import VERTC, { VideoSourceType, MediaType, StreamIndex, RoomProfileType, MirrorType, VideoCodecType, VideoRenderMode } from '@volcengine/rtc';
4
+
5
+ // src/shared/browser-toast.ts
6
+ var TOAST_ROOT_ID = "Xmax-sdk-toast-root";
7
+ var TOAST_DURATION_MS = 3e3;
8
+ var toastSeed = 0;
9
+ var timeoutMap = /* @__PURE__ */ new Map();
10
+ var elementMap = /* @__PURE__ */ new Map();
11
+ function ensureToastRoot() {
12
+ if (typeof document === "undefined") {
13
+ return null;
14
+ }
15
+ let root = document.getElementById(TOAST_ROOT_ID);
16
+ if (root) {
17
+ return root;
18
+ }
19
+ root = document.createElement("div");
20
+ root.id = TOAST_ROOT_ID;
21
+ root.setAttribute("aria-live", "polite");
22
+ Object.assign(root.style, {
23
+ position: "fixed",
24
+ left: "50%",
25
+ top: "24px",
26
+ transform: "translateX(-50%)",
27
+ zIndex: "3000",
28
+ display: "flex",
29
+ flexDirection: "column",
30
+ alignItems: "center",
31
+ gap: "12px",
32
+ pointerEvents: "none"
33
+ });
34
+ document.body.appendChild(root);
35
+ return root;
36
+ }
37
+ function createIcon(type) {
38
+ const icon = document.createElement("span");
39
+ icon.setAttribute("aria-hidden", "true");
40
+ icon.innerHTML = type === "success" ? '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M10 3L4.5 8.5 2 6" stroke="#4ade80" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none"><circle cx="6" cy="6" r="5" stroke="#f87171" stroke-width="1.5"/><path d="M4 4l4 4M8 4L4 8" stroke="#f87171" stroke-width="1.5" stroke-linecap="round"/></svg>';
41
+ Object.assign(icon.style, {
42
+ display: "inline-flex",
43
+ width: "12px",
44
+ height: "12px",
45
+ flexShrink: "0"
46
+ });
47
+ return icon;
48
+ }
49
+ function clearToastTimer(id) {
50
+ const timer = timeoutMap.get(id);
51
+ if (timer) {
52
+ window.clearTimeout(timer);
53
+ timeoutMap.delete(id);
54
+ }
55
+ }
56
+ function destroyToast(id) {
57
+ if (!id) {
58
+ timeoutMap.forEach((timer) => window.clearTimeout(timer));
59
+ timeoutMap.clear();
60
+ elementMap.forEach((element2) => element2.remove());
61
+ elementMap.clear();
62
+ const root2 = typeof document !== "undefined" ? document.getElementById(TOAST_ROOT_ID) : null;
63
+ root2?.remove();
64
+ return;
65
+ }
66
+ clearToastTimer(id);
67
+ const element = elementMap.get(id);
68
+ element?.remove();
69
+ elementMap.delete(id);
70
+ const root = typeof document !== "undefined" ? document.getElementById(TOAST_ROOT_ID) : null;
71
+ if (root && !root.childElementCount) {
72
+ root.remove();
73
+ }
74
+ }
75
+ function showToast(type, message, options) {
76
+ const root = ensureToastRoot();
77
+ if (!root) {
78
+ return "";
79
+ }
80
+ const id = options?.key || `toast-${Date.now()}-${++toastSeed}`;
81
+ clearToastTimer(id);
82
+ elementMap.get(id)?.remove();
83
+ const isMobile = typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches;
84
+ const textSize = isMobile ? "12px" : "14px";
85
+ const toast = document.createElement("div");
86
+ toast.setAttribute("role", "status");
87
+ Object.assign(toast.style, {
88
+ pointerEvents: "auto",
89
+ display: "flex",
90
+ alignItems: "center",
91
+ gap: "12px",
92
+ minWidth: "240px",
93
+ padding: "16px",
94
+ borderRadius: "12px",
95
+ border: "1px solid rgba(255, 255, 255, 0.20)",
96
+ background: "rgba(0, 0, 0, 0.80)",
97
+ boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
98
+ fontFamily: "Rubik, ui-sans-serif, system-ui, sans-serif",
99
+ fontSize: textSize,
100
+ fontWeight: "400",
101
+ lineHeight: "14px"
102
+ });
103
+ const content = document.createElement("span");
104
+ content.textContent = message;
105
+ Object.assign(content.style, {
106
+ paddingRight: "0px",
107
+ textAlign: "center",
108
+ color: "#ffffff",
109
+ fontFamily: "Rubik, ui-sans-serif, system-ui, sans-serif",
110
+ fontSize: textSize,
111
+ fontWeight: "400",
112
+ lineHeight: "14px"
113
+ });
114
+ toast.appendChild(createIcon(type));
115
+ toast.appendChild(content);
116
+ root.appendChild(toast);
117
+ elementMap.set(id, toast);
118
+ const timer = window.setTimeout(() => {
119
+ destroyToast(id);
120
+ }, options?.duration ?? TOAST_DURATION_MS);
121
+ timeoutMap.set(id, timer);
122
+ return id;
123
+ }
124
+ var browserToast = {
125
+ success(message, options) {
126
+ return showToast("success", message, options);
127
+ },
128
+ error(message, options) {
129
+ return showToast("error", message, options);
130
+ },
131
+ destroy(id) {
132
+ destroyToast(id);
133
+ }
134
+ };
135
+
136
+ // src/shared/error-utils.ts
137
+ function notifyError(error, notifier, fallbackMessage) {
138
+ const resolved = error instanceof Error ? error : Object.assign(new Error(fallbackMessage), { cause: error });
139
+ if (!resolved.__XmaxNotified) {
140
+ resolved.__XmaxNotified = true;
141
+ const message = resolved.message || fallbackMessage;
142
+ if (notifier) {
143
+ notifier(message, error);
144
+ } else if (typeof window !== "undefined" && typeof document !== "undefined") {
145
+ browserToast.error(message);
146
+ }
147
+ }
148
+ return resolved;
149
+ }
150
+
151
+ // src/sdk/client.ts
152
+ var XMAX_OPEN_API_BASE_URLS = {
153
+ // 国内 — @xmaxai/sdk
154
+ cn: "https://cloud.xmax.22duck.cn/open/api/v1",
155
+ // 海外 — @xmaxai/sdk-global
156
+ global: "https://api.xmax.cloud/open/api/v1"
157
+ };
158
+ var XMAX_SDK_BUILD_REGION = "cn";
159
+ var XMAX_OPEN_API_PRODUCTION_BASE_URL = XMAX_OPEN_API_BASE_URLS[XMAX_SDK_BUILD_REGION];
160
+ var ACTIVE_STATUS = "ACTIVE";
161
+ var STS_TTL_MS = 30 * 60 * 1e3;
162
+ var STS_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
163
+ function sanitizeFileName(fileName) {
164
+ return fileName.trim().replace(/[^\w.\-一-龥]/g, "_").replace(/_+/g, "_");
165
+ }
166
+ function buildCosFileUrl(endpoint, bucket, key) {
167
+ const trimmed = endpoint.trim();
168
+ const url = new URL(
169
+ /^https?:\/\//i.test(trimmed) ? trimmed.endsWith("/") ? trimmed : `${trimmed}/` : `https://${trimmed.replace(/^\/+/, "")}${trimmed.endsWith("/") ? "" : "/"}`
170
+ );
171
+ if (url.hostname.startsWith("cos.") && !url.hostname.startsWith(`${bucket}.`)) {
172
+ url.hostname = `${bucket}.${url.hostname}`;
173
+ }
174
+ return new URL(key.replace(/^\/+/, ""), url.toString()).toString();
175
+ }
176
+ function resolveCosUploadUrl(raw, sts, key) {
177
+ const location = raw?.Location?.trim();
178
+ if (location) {
179
+ return /^https?:\/\//i.test(location) ? location : `https://${location.replace(/^\/+/, "")}`;
180
+ }
181
+ const endpoint = sts.endpoint?.trim();
182
+ if (endpoint) {
183
+ return buildCosFileUrl(endpoint, sts.bucket, key);
184
+ }
185
+ return `https://${sts.bucket}.cos.${sts.region}.myqcloud.com/${key.replace(/^\/+/, "")}`;
186
+ }
187
+ var XmaxOpenClient = class {
188
+ constructor(options) {
189
+ this.stsCache = null;
190
+ this.heartbeatIntervalId = null;
191
+ this.suppressErrorNotification = false;
192
+ this.teardownAbortController = null;
193
+ this.pendingRequestControllers = /* @__PURE__ */ new Set();
194
+ const i18n = options.i18n ?? createSDKI18n({ locale: options.locale });
195
+ if (!options.apiKey?.trim()) {
196
+ throw new Error(i18n.t("errors.apiKeyEmpty"));
197
+ }
198
+ if (!options.baseUrl?.trim()) {
199
+ throw new Error(i18n.t("errors.baseUrlEmpty"));
200
+ }
201
+ this.apiKey = options.apiKey;
202
+ this.authToken = options.authToken;
203
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
204
+ this.timeoutMs = options.timeoutMs ?? 1e4;
205
+ this.i18n = i18n;
206
+ this.onError = options.onError;
207
+ }
208
+ throwError(message, cause) {
209
+ const error = new Error(message);
210
+ if (cause !== void 0) {
211
+ error.cause = cause;
212
+ }
213
+ if (this.suppressErrorNotification) {
214
+ throw error;
215
+ }
216
+ throw notifyError(error, this.onError, message);
217
+ }
218
+ /** Stop heartbeat, abort in-flight API requests, and suppress error toasts during teardown. */
219
+ beginTeardown() {
220
+ this.suppressErrorNotification = true;
221
+ this.stopHeartbeat();
222
+ if (this.teardownAbortController) {
223
+ this.teardownAbortController.abort();
224
+ }
225
+ this.teardownAbortController = new AbortController();
226
+ this.pendingRequestControllers.forEach((controller) => {
227
+ controller.abort();
228
+ });
229
+ this.pendingRequestControllers.clear();
230
+ }
231
+ endTeardown() {
232
+ this.suppressErrorNotification = false;
233
+ this.teardownAbortController = null;
234
+ }
235
+ isAbortError(error) {
236
+ return error instanceof DOMException && error.name === "AbortError";
237
+ }
238
+ async request(method, path, body) {
239
+ const controller = new AbortController();
240
+ this.pendingRequestControllers.add(controller);
241
+ const onAbort = () => controller.abort();
242
+ this.teardownAbortController?.signal.addEventListener("abort", onAbort);
243
+ const timeoutId = window.setTimeout(() => controller.abort(), this.timeoutMs);
244
+ const headers = {
245
+ "Content-Type": "application/json",
246
+ Accept: "application/json",
247
+ "X-Api-Key": this.apiKey
248
+ };
249
+ if (this.authToken) {
250
+ headers["Authorization"] = `Bearer ${this.authToken}`;
251
+ }
252
+ try {
253
+ const response = await fetch(`${this.baseUrl}${path}`, {
254
+ method,
255
+ headers,
256
+ body: body === void 0 ? void 0 : JSON.stringify(body),
257
+ signal: controller.signal
258
+ });
259
+ const text = await response.text();
260
+ let payload;
261
+ try {
262
+ payload = text ? JSON.parse(text) : null;
263
+ } catch (parseError) {
264
+ throw this.throwError(this.i18n.t("errors.invalidJson"), parseError);
265
+ }
266
+ if (typeof payload !== "object" || payload === null) {
267
+ throw this.throwError(this.i18n.t("errors.invalidPayload"));
268
+ }
269
+ const p = payload;
270
+ if (response.ok && p.success === true) {
271
+ return p.data;
272
+ }
273
+ const mappedMessage = resolveApiErrorMessage(p.code, this.i18n.locale);
274
+ const errorMessage = mappedMessage ?? this.i18n.t("errors.requestRetry");
275
+ throw new Error(errorMessage);
276
+ } catch (error) {
277
+ if (this.isAbortError(error)) {
278
+ if (this.suppressErrorNotification) {
279
+ throw new Error(this.i18n.t("errors.requestTimeout"));
280
+ }
281
+ throw this.throwError(this.i18n.t("errors.requestTimeout"), error);
282
+ } else if (error instanceof Error) {
283
+ if (this.suppressErrorNotification) {
284
+ throw error;
285
+ }
286
+ throw notifyError(error, this.onError, error.message || this.i18n.t("errors.requestFailed"));
287
+ } else {
288
+ throw this.throwError(this.i18n.t("errors.requestFailed"), error);
289
+ }
290
+ } finally {
291
+ this.teardownAbortController?.signal.removeEventListener("abort", onAbort);
292
+ this.pendingRequestControllers.delete(controller);
293
+ window.clearTimeout(timeoutId);
294
+ }
295
+ }
296
+ async createSession(model) {
297
+ if (!model.trim()) {
298
+ this.throwError(this.i18n.t("errors.modelEmpty"));
299
+ }
300
+ return this.request("POST", "/session", { model });
301
+ }
302
+ async getSession(sessionUid) {
303
+ return this.request("GET", `/session/${sessionUid}`);
304
+ }
305
+ async heartbeatSession(sessionUid) {
306
+ return this.request("PUT", `/session/${sessionUid}/heartbeat`);
307
+ }
308
+ async closeSession(sessionUid) {
309
+ const previousSuppress = this.suppressErrorNotification;
310
+ this.suppressErrorNotification = true;
311
+ try {
312
+ return await this.request("DELETE", `/session/${sessionUid}`);
313
+ } catch {
314
+ return null;
315
+ } finally {
316
+ this.suppressErrorNotification = previousSuppress;
317
+ }
318
+ }
319
+ async getCosSts(forceRefresh = false) {
320
+ const now = Date.now();
321
+ if (!forceRefresh && this.stsCache && now - this.stsCache.fetchedAt < STS_TTL_MS - STS_REFRESH_BUFFER_MS) {
322
+ return this.stsCache.value;
323
+ }
324
+ const sts = await this.request("GET", "/cos/sts");
325
+ this.stsCache = {
326
+ value: sts,
327
+ fetchedAt: now
328
+ };
329
+ return sts;
330
+ }
331
+ async uploadObject(file, expectedTypePrefix) {
332
+ if (!(file instanceof File)) {
333
+ this.throwError(this.i18n.t("errors.invalidFile"));
334
+ }
335
+ if (!file.type.startsWith(expectedTypePrefix)) {
336
+ this.throwError(this.i18n.t("errors.invalidFileType", { type: file.type }));
337
+ }
338
+ const sts = await this.getCosSts();
339
+ const safeFileName = sanitizeFileName(file.name) || "image";
340
+ const key = `${sts.prefix}${Date.now()}_${safeFileName}`;
341
+ const cos = new COS({
342
+ SecretId: sts.credentials.accessKeyId,
343
+ SecretKey: sts.credentials.secretAccessKey,
344
+ SecurityToken: sts.credentials.sessionToken
345
+ });
346
+ const raw = await new Promise((resolve, reject) => {
347
+ cos.putObject(
348
+ {
349
+ Bucket: sts.bucket,
350
+ Region: sts.region,
351
+ Key: key,
352
+ Body: file,
353
+ ContentType: file.type
354
+ },
355
+ (error, data) => {
356
+ if (error) {
357
+ reject(error);
358
+ return;
359
+ }
360
+ resolve(data);
361
+ }
362
+ );
363
+ });
364
+ return {
365
+ key,
366
+ url: resolveCosUploadUrl(raw, sts, key),
367
+ raw,
368
+ sts
369
+ };
370
+ }
371
+ async uploadImage(file) {
372
+ return this.uploadObject(file, "image/");
373
+ }
374
+ async uploadVideo(file) {
375
+ return this.uploadObject(file, "video/");
376
+ }
377
+ startHeartbeat(sessionUid, intervalMs = 1e4, onHeartbeat, onError) {
378
+ this.stopHeartbeat();
379
+ const heartbeat = async () => {
380
+ try {
381
+ const session = await this.heartbeatSession(sessionUid);
382
+ onHeartbeat?.(session);
383
+ if (session.status !== ACTIVE_STATUS) {
384
+ this.stopHeartbeat();
385
+ }
386
+ } catch (error) {
387
+ onError?.(error);
388
+ this.stopHeartbeat();
389
+ }
390
+ };
391
+ heartbeat();
392
+ this.heartbeatIntervalId = window.setInterval(heartbeat, intervalMs);
393
+ }
394
+ stopHeartbeat() {
395
+ if (this.heartbeatIntervalId !== null) {
396
+ clearInterval(this.heartbeatIntervalId);
397
+ this.heartbeatIntervalId = null;
398
+ }
399
+ }
400
+ getRtcJoinInfo(session) {
401
+ const roomId = session.modelExtra?.room_id?.trim();
402
+ const appId = session.modelExtra?.rtc_app_id?.trim();
403
+ const token = session.modelExtra?.room_token?.trim();
404
+ const userId = session.modelExtra?.user_id?.trim() || session.userUid?.trim() || "";
405
+ if (!roomId || !appId || !token || !userId) {
406
+ this.throwError(this.i18n.t("errors.sessionJoinInfoIncomplete"));
407
+ }
408
+ return {
409
+ appId,
410
+ roomId,
411
+ userId,
412
+ token,
413
+ botName: session.modelExtra?.bot_name
414
+ };
415
+ }
416
+ };
417
+
418
+ // src/sdk/rtc-events.ts
419
+ function createTaskUid() {
420
+ return `task-${Date.now()}`;
421
+ }
422
+ function createStartRtcRoomEvent(input) {
423
+ const resolvedPrompt = input.prompt?.trim() ?? "";
424
+ const refImagePath = input.refImagePath?.trim() || input.refImage?.trim() || void 0;
425
+ const useRefImage = !!refImagePath;
426
+ return {
427
+ event: "start",
428
+ user_id: input.userId?.trim() || void 0,
429
+ uid: input.uid?.trim() || void 0,
430
+ session_uid: input.sessionUid?.trim() || void 0,
431
+ params: {
432
+ model: input.model.trim(),
433
+ size: input.size,
434
+ prompt: resolvedPrompt,
435
+ ref_image_path: useRefImage ? refImagePath : void 0,
436
+ ref_image: useRefImage ? refImagePath : void 0,
437
+ static_generate: input.staticGenerate === void 0 ? void 0 : input.staticGenerate,
438
+ ...input.extraParams ?? {}
439
+ }
440
+ };
441
+ }
442
+ function createStopRtcRoomEvent(input) {
443
+ return {
444
+ event: "stop",
445
+ user_id: input.userId?.trim() || void 0,
446
+ session_uid: input.sessionUid?.trim() || void 0
447
+ };
448
+ }
449
+ function createTracksRtcRoomEvent(input) {
450
+ return {
451
+ event: "tracks",
452
+ user_id: input.userId?.trim() || void 0,
453
+ uid: input.uid?.trim() || void 0,
454
+ session_uid: input.sessionUid?.trim() || void 0,
455
+ tracks: input.tracks
456
+ };
457
+ }
458
+
459
+ // src/rtc/preview-media-layout.ts
460
+ var PREVIEW_UPLOAD_LAYOUT_ATTR = "data-preview-upload-layout";
461
+ var PREVIEW_UPLOAD_FIT_ATTR = "data-preview-upload-fit";
462
+ var PREVIEW_OUTPUT_LAYOUT_ATTR = "data-preview-output-layout";
463
+ var PREVIEW_CONTAIN_ATTR = "data-preview-contain";
464
+ var PREVIEW_COVER_ATTR = "data-preview-cover";
465
+ function hasFitPreviewLayout(container) {
466
+ return container.hasAttribute(PREVIEW_UPLOAD_LAYOUT_ATTR) || container.hasAttribute(PREVIEW_OUTPUT_LAYOUT_ATTR) || container.hasAttribute(PREVIEW_CONTAIN_ATTR) || container.closest(`[${PREVIEW_UPLOAD_LAYOUT_ATTR}]`) !== null || container.closest(`[${PREVIEW_OUTPUT_LAYOUT_ATTR}]`) !== null || container.closest(`[${PREVIEW_CONTAIN_ATTR}]`) !== null;
467
+ }
468
+ function hasCoverPreviewLayout(container) {
469
+ return container.hasAttribute(PREVIEW_COVER_ATTR) || container.closest(`[${PREVIEW_COVER_ATTR}]`) !== null;
470
+ }
471
+ function resolveUploadFitMode(container) {
472
+ const node = container.hasAttribute(PREVIEW_UPLOAD_FIT_ATTR) ? container : container.closest(`[${PREVIEW_UPLOAD_FIT_ATTR}]`);
473
+ const fit = node?.getAttribute(PREVIEW_UPLOAD_FIT_ATTR);
474
+ return fit === "landscape" ? "landscape" : "portrait";
475
+ }
476
+ function applyUploadPreviewMediaLayout(container) {
477
+ if (hasCoverPreviewLayout(container)) {
478
+ return;
479
+ }
480
+ if (!hasFitPreviewLayout(container)) {
481
+ return;
482
+ }
483
+ const fitMode = resolveUploadFitMode(container);
484
+ container.style.overflow = "hidden";
485
+ container.style.display = "flex";
486
+ container.style.alignItems = "center";
487
+ container.style.justifyContent = "center";
488
+ container.querySelectorAll("div").forEach((node) => {
489
+ if (node === container) {
490
+ return;
491
+ }
492
+ const el = node;
493
+ el.style.width = "100%";
494
+ el.style.height = "100%";
495
+ if (fitMode === "landscape") {
496
+ el.style.display = "flex";
497
+ el.style.alignItems = "center";
498
+ el.style.justifyContent = "center";
499
+ el.style.overflow = "hidden";
500
+ }
501
+ });
502
+ container.querySelectorAll("video, canvas").forEach((node) => {
503
+ const el = node;
504
+ el.style.display = "block";
505
+ el.style.objectFit = "contain";
506
+ el.style.objectPosition = "center";
507
+ el.style.position = "relative";
508
+ el.style.left = "auto";
509
+ el.style.right = "auto";
510
+ el.style.top = "auto";
511
+ el.style.bottom = "auto";
512
+ el.style.transform = "none";
513
+ if (fitMode === "landscape") {
514
+ el.style.width = "100%";
515
+ el.style.height = "100%";
516
+ el.style.maxWidth = "none";
517
+ el.style.maxHeight = "none";
518
+ el.style.margin = "0";
519
+ return;
520
+ }
521
+ el.style.height = "100%";
522
+ el.style.width = "auto";
523
+ el.style.maxWidth = "none";
524
+ el.style.maxHeight = "none";
525
+ el.style.marginLeft = "auto";
526
+ el.style.marginRight = "auto";
527
+ el.style.marginTop = "auto";
528
+ el.style.marginBottom = "auto";
529
+ });
530
+ }
531
+ function observeUploadPreviewMediaLayout(container) {
532
+ if (!hasFitPreviewLayout(container) && !hasCoverPreviewLayout(container)) {
533
+ return () => {
534
+ };
535
+ }
536
+ let rafId = null;
537
+ const apply = () => applyUploadPreviewMediaLayout(container);
538
+ const scheduleApply = () => {
539
+ if (rafId !== null) {
540
+ return;
541
+ }
542
+ rafId = requestAnimationFrame(() => {
543
+ rafId = null;
544
+ apply();
545
+ });
546
+ };
547
+ apply();
548
+ const observer = new MutationObserver(() => scheduleApply());
549
+ observer.observe(container, {
550
+ childList: true,
551
+ subtree: true,
552
+ attributes: true
553
+ });
554
+ return () => {
555
+ if (rafId !== null) {
556
+ cancelAnimationFrame(rafId);
557
+ }
558
+ observer.disconnect();
559
+ };
560
+ }
561
+ function resolveVideoRenderMode(container) {
562
+ if (container.hasAttribute(PREVIEW_UPLOAD_LAYOUT_ATTR) || container.hasAttribute(PREVIEW_OUTPUT_LAYOUT_ATTR) || container.hasAttribute("data-preview-contain") || container.hasAttribute(PREVIEW_UPLOAD_FIT_ATTR) || container.closest(`[${PREVIEW_UPLOAD_LAYOUT_ATTR}]`) || container.closest(`[${PREVIEW_OUTPUT_LAYOUT_ATTR}]`) || container.closest("[data-preview-contain]") || container.closest(`[${PREVIEW_UPLOAD_FIT_ATTR}]`)) {
563
+ return VideoRenderMode.RENDER_MODE_FIT;
564
+ }
565
+ if (container.hasAttribute("data-preview-cover") || container.hasAttribute("data-upload-preview-cover") || container.closest("[data-preview-cover]") || container.closest("[data-upload-preview-cover]")) {
566
+ return VideoRenderMode.RENDER_MODE_HIDDEN;
567
+ }
568
+ const rect = container.getBoundingClientRect();
569
+ if (rect.width <= 0 || rect.height <= 0) {
570
+ return VideoRenderMode.RENDER_MODE_FIT;
571
+ }
572
+ const isCompactPreview = rect.width <= 160;
573
+ const isPortraitFrame = rect.height > rect.width;
574
+ return isCompactPreview || isPortraitFrame ? VideoRenderMode.RENDER_MODE_HIDDEN : VideoRenderMode.RENDER_MODE_FIT;
575
+ }
576
+ var REMOTE_OUTPUT_STALL_MS = 2e4;
577
+ var REMOTE_OUTPUT_RECOVERY_COOLDOWN_MS = 6e4;
578
+ var SESSION_SEI_INTERVAL_MS = 500;
579
+ var DEFAULT_SEI_REPEAT_COUNT = 1;
580
+ var PUBLISH_START_MIN_FRAMES = 5;
581
+ var PUBLISH_START_FRAME_TIMEOUT_MS = 800;
582
+ var PUBLISH_COLD_MIN_FRAMES = 12;
583
+ var PUBLISH_COLD_FRAME_TIMEOUT_MS = 1500;
584
+ var PUBLISH_COLD_WARMUP_MS = 600;
585
+ var PUBLISH_WARM_SKIP_MS = 800;
586
+ var COLD_START_MIN_FRAMES = 3;
587
+ var WARM_START_MIN_FRAMES = 1;
588
+ var WARM_START_FRAME_TIMEOUT_MS = 200;
589
+ var COLD_START_SETTLE_MS = 50;
590
+ var COLD_START_OUTBOUND_TIMEOUT_MS = 1500;
591
+ var COLD_START_OUTBOUND_STABLE_MS = 200;
592
+ var COLD_START_MIN_OUTBOUND_FPS = 5;
593
+ var PUBLISH_READY_BUFFER_MS = 300;
594
+ var PUBLISH_READY_TIMEOUT_MS = 5e3;
595
+ var _RtcManager = class _RtcManager {
596
+ constructor(options = {}) {
597
+ this.engine = null;
598
+ this.leaveTask = null;
599
+ this.users = /* @__PURE__ */ new Set();
600
+ this.currentJoinInfo = null;
601
+ this.joined = false;
602
+ this.preserveRemoteDom = false;
603
+ this.localVideoPublished = false;
604
+ this.localVideoSourceType = VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL;
605
+ this.shouldMirrorLocalPreview = false;
606
+ this.externalVideoTrack = null;
607
+ this.remoteVideoUserId = null;
608
+ this.pendingRemoteVideoUserId = null;
609
+ this.pendingRemoteVideoMediaType = MediaType.VIDEO;
610
+ /** Subscribed to RTC media, but player may still be gated by SEI / container. */
611
+ this.subscribedRemoteUserId = null;
612
+ this.subscribedRemoteMediaType = MediaType.VIDEO;
613
+ this.localVideoContainer = null;
614
+ this.remoteVideoContainer = null;
615
+ this.expectedRemoteSei = null;
616
+ this.lastRemoteSei = null;
617
+ this.lastRemoteSeiUserId = null;
618
+ this.localSeiMessage = null;
619
+ this.localSeiRepeatCount = DEFAULT_SEI_REPEAT_COUNT;
620
+ this.seiSendInterval = null;
621
+ this.lastSeiSentAtMs = 0;
622
+ this.hasLoggedSeiSendFailure = false;
623
+ this.seiSupport = "unknown";
624
+ this.hasLoggedSeiUnsupported = false;
625
+ this.lastDebugSeiMatched = null;
626
+ this.lastDebugRemoteSei = null;
627
+ this.remoteFrameWatchStop = null;
628
+ this.uploadPreviewLayoutObserverStop = null;
629
+ this.remoteOutputRecoveryCooldownUntil = 0;
630
+ this.joinedAtMs = 0;
631
+ this.publishStartedAtMs = 0;
632
+ this.lastRoomStartEventAtMs = 0;
633
+ this.localStreamStatsCount = 0;
634
+ this.firstNonZeroSentFrameRateAtMs = 0;
635
+ this.lastObservedSentFrameRate = 0;
636
+ this.publishReadyWaiters = [];
637
+ this.stablePublishWaiters = [];
638
+ this.publishReadyBufferTimer = null;
639
+ this.seiGateFallbackTimer = null;
640
+ this.seiMatchedDelayTimer = null;
641
+ this.lastSentResolution = "";
642
+ this.lastReceivedResolution = "";
643
+ this.onLog = options.onLog;
644
+ this.onStateChange = options.onStateChange;
645
+ this.onSeiGateChange = options.onSeiGateChange;
646
+ this.onRemoteSeiReceived = options.onRemoteSeiReceived;
647
+ this.onRemoteOutputStalled = options.onRemoteOutputStalled;
648
+ this.debug = options.debug ?? false;
649
+ this.i18n = options.i18n ?? createSDKI18n();
650
+ }
651
+ emitSeiGateState() {
652
+ const matched = this.isSeiMatched();
653
+ this.onSeiGateChange?.({
654
+ expectedSei: this.expectedRemoteSei,
655
+ sei: this.lastRemoteSei,
656
+ userId: this.lastRemoteSeiUserId,
657
+ matched
658
+ });
659
+ }
660
+ setPreserveRemoteDom(preserve) {
661
+ this.preserveRemoteDom = preserve;
662
+ }
663
+ setOnRemotePlayerMounted(callback) {
664
+ this.onRemotePlayerMounted = callback;
665
+ }
666
+ setLocalVideoContainer(container) {
667
+ const previous = this.localVideoContainer;
668
+ const changed = previous !== container;
669
+ this.localVideoContainer = container;
670
+ if (previous && changed) {
671
+ previous.replaceChildren();
672
+ }
673
+ if (container && changed) {
674
+ container.replaceChildren();
675
+ }
676
+ if (container && this.localVideoPublished && this.engine) {
677
+ this.bindLocalVideoPlayer();
678
+ }
679
+ }
680
+ setRemoteVideoContainer(container) {
681
+ const previous = this.remoteVideoContainer;
682
+ if (previous === container) {
683
+ if (this.debug && container && container.childElementCount === 0 && (this.remoteVideoUserId || this.pendingRemoteVideoUserId)) {
684
+ this.debugLog("setRemoteVideoContainer skipped but container empty", {
685
+ remoteVideoUserId: this.remoteVideoUserId,
686
+ pendingRemoteVideoUserId: this.pendingRemoteVideoUserId
687
+ });
688
+ }
689
+ return;
690
+ }
691
+ this.remoteVideoContainer = container;
692
+ this.debugLog("setRemoteVideoContainer", {
693
+ hadPrevious: !!previous,
694
+ next: !!container,
695
+ remoteVideoUserId: this.remoteVideoUserId,
696
+ pendingRemoteVideoUserId: this.pendingRemoteVideoUserId
697
+ });
698
+ if (previous) {
699
+ previous.replaceChildren();
700
+ }
701
+ if (container && this.engine && this.joined && (this.remoteVideoUserId || this.pendingRemoteVideoUserId)) {
702
+ void this.refreshRemoteVideoBinding();
703
+ }
704
+ }
705
+ /** Display-only gate: expect bot output SEI to match task id — no local SEI send, no start blocking. */
706
+ configureSessionSei(sessionId, _options = {}) {
707
+ const normalizedSessionId = this.normalizeSei(sessionId);
708
+ const previousExpectedRemoteSei = this.expectedRemoteSei;
709
+ this.stopSeiSending();
710
+ this.clearSeiGateFallbackTimer();
711
+ this.clearSeiMatchedDelayTimer();
712
+ this.localSeiMessage = null;
713
+ this.expectedRemoteSei = this.seiSupport === "unsupported" ? null : normalizedSessionId;
714
+ this.lastRemoteSei = null;
715
+ this.lastRemoteSeiUserId = null;
716
+ this.hasLoggedSeiSendFailure = false;
717
+ if (previousExpectedRemoteSei !== this.expectedRemoteSei && this.remoteVideoUserId) {
718
+ this.pendingRemoteVideoUserId = this.remoteVideoUserId;
719
+ this.remoteVideoUserId = null;
720
+ }
721
+ this.emitSeiGateState();
722
+ if (previousExpectedRemoteSei !== this.expectedRemoteSei && !this.preserveRemoteDom) {
723
+ this.clearContainer(this.remoteVideoContainer);
724
+ }
725
+ this.scheduleSeiGateFallback(normalizedSessionId);
726
+ }
727
+ clearSeiGateFallbackTimer() {
728
+ if (this.seiGateFallbackTimer !== null) {
729
+ clearTimeout(this.seiGateFallbackTimer);
730
+ this.seiGateFallbackTimer = null;
731
+ }
732
+ }
733
+ clearSeiMatchedDelayTimer() {
734
+ if (this.seiMatchedDelayTimer !== null) {
735
+ clearTimeout(this.seiMatchedDelayTimer);
736
+ this.seiMatchedDelayTimer = null;
737
+ }
738
+ }
739
+ /** Show output if bot SEI never arrives — UI gate only, RTC bind is not blocked. */
740
+ scheduleSeiGateFallback(expectedSei) {
741
+ this.clearSeiGateFallbackTimer();
742
+ if (!expectedSei) {
743
+ return;
744
+ }
745
+ this.seiGateFallbackTimer = setTimeout(() => {
746
+ this.seiGateFallbackTimer = null;
747
+ if (this.expectedRemoteSei !== expectedSei || this.isSeiMatched()) {
748
+ return;
749
+ }
750
+ this.log("warn", this.i18n.t("rtc.seiGateFallback"), { expectedSei });
751
+ this.expectedRemoteSei = null;
752
+ this.emitSeiGateState();
753
+ void this.refreshRemoteVideoBinding({ force: true });
754
+ }, _RtcManager.SEI_GATE_FALLBACK_MS);
755
+ }
756
+ get snapshot() {
757
+ return {
758
+ roomId: this.currentJoinInfo?.roomId ?? null,
759
+ userId: this.currentJoinInfo?.userId ?? null,
760
+ appId: this.currentJoinInfo?.appId ?? null,
761
+ botUserId: this.currentJoinInfo?.botName ?? null,
762
+ joined: this.joined,
763
+ localVideoPublished: this.localVideoPublished,
764
+ remoteVideoUserId: this.remoteVideoUserId,
765
+ users: Array.from(this.users)
766
+ };
767
+ }
768
+ getLocalVideoSize() {
769
+ if (!this.engine || !this.joined) {
770
+ return null;
771
+ }
772
+ const track = this.engine.getLocalStreamTrack(StreamIndex.STREAM_INDEX_MAIN, "video");
773
+ if (!track) {
774
+ return null;
775
+ }
776
+ const settings = track.getSettings?.();
777
+ const width = typeof settings?.width === "number" ? settings.width : 0;
778
+ const height = typeof settings?.height === "number" ? settings.height : 0;
779
+ if (!width || !height) {
780
+ return null;
781
+ }
782
+ return { width, height };
783
+ }
784
+ async join(joinInfo) {
785
+ if (this.currentJoinInfo?.roomId === joinInfo.roomId && this.joined) {
786
+ this.log("info", this.i18n.t("rtc.alreadyInRoom"), joinInfo);
787
+ return;
788
+ }
789
+ await this.leave();
790
+ this.currentJoinInfo = joinInfo;
791
+ this.users.clear();
792
+ this.users.add(joinInfo.userId);
793
+ this.localVideoPublished = false;
794
+ this.remoteVideoUserId = null;
795
+ this.pendingRemoteVideoUserId = null;
796
+ this.pendingRemoteVideoMediaType = MediaType.VIDEO;
797
+ this.lastRemoteSei = null;
798
+ this.lastRemoteSeiUserId = null;
799
+ this.clearVideoContainers();
800
+ const engine = VERTC.createEngine(joinInfo.appId);
801
+ this.engine = engine;
802
+ this.bindEngineEvents(engine);
803
+ this.log("info", this.i18n.t("rtc.joiningRoom"), joinInfo);
804
+ await engine.joinRoom(
805
+ joinInfo.token,
806
+ joinInfo.roomId,
807
+ {
808
+ userId: joinInfo.userId
809
+ },
810
+ {
811
+ isAutoPublish: false,
812
+ isAutoSubscribeAudio: false,
813
+ isAutoSubscribeVideo: true,
814
+ roomProfileType: RoomProfileType.communication
815
+ }
816
+ );
817
+ this.joined = true;
818
+ this.joinedAtMs = performance.now();
819
+ this.ensureSeiSupport();
820
+ this.emitState();
821
+ this.log("success", this.i18n.t("rtc.joinSuccess"), this.snapshot);
822
+ this.log("info", this.i18n.t("rtc.roomUsers", { users: this.snapshot.users.join(", ") }), this.snapshot.users);
823
+ }
824
+ async startVideoPublishing(size) {
825
+ if (!this.engine || !this.currentJoinInfo || !this.joined) {
826
+ throw new Error(this.i18n.t("errors.rtcNotJoined"));
827
+ }
828
+ const publishStartedAtMs = performance.now();
829
+ await this.applyVideoEncoderPreference(size);
830
+ if (size) {
831
+ await this.applyLocalVideoCaptureConfig(size);
832
+ }
833
+ if (this.localVideoPublished) {
834
+ this.log("info", this.i18n.t("rtc.localVideoAlreadyPublished"));
835
+ return;
836
+ }
837
+ this.shouldMirrorLocalPreview = true;
838
+ this.bindLocalVideoPlayer();
839
+ await this.switchToInternalVideoSource();
840
+ await this.engine.startVideoCapture();
841
+ await this.engine.publishStream(MediaType.VIDEO);
842
+ this.localVideoPublished = true;
843
+ this.publishStartedAtMs = publishStartedAtMs;
844
+ this.localStreamStatsCount = 0;
845
+ this.firstNonZeroSentFrameRateAtMs = 0;
846
+ this.lastObservedSentFrameRate = 0;
847
+ this.ensureSeiSupport();
848
+ this.emitState();
849
+ this.log("success", this.i18n.t("rtc.localVideoPublished"));
850
+ }
851
+ async startExternalVideoPublishing(track, encoderSize, publishOptions) {
852
+ if (!this.engine || !this.currentJoinInfo || !this.joined) {
853
+ throw new Error(this.i18n.t("errors.rtcNotJoined"));
854
+ }
855
+ if (this.localVideoPublished) {
856
+ this.log("info", this.i18n.t("rtc.localVideoAlreadyPublished"));
857
+ return;
858
+ }
859
+ const publishStartedAtMs = performance.now();
860
+ const settings = track.getSettings?.();
861
+ const width = typeof settings?.width === "number" && settings.width > 0 ? settings.width : void 0;
862
+ const height = typeof settings?.height === "number" && settings.height > 0 ? settings.height : void 0;
863
+ const encoderOptions = publishOptions?.highQuality !== false ? {
864
+ ...publishOptions,
865
+ highQuality: true,
866
+ fps: publishOptions?.fps ?? resolvePlaybackDrivenEncoderFps(track)
867
+ } : publishOptions;
868
+ this.shouldMirrorLocalPreview = false;
869
+ this.bindLocalVideoPlayer();
870
+ await this.switchToExternalVideoSource(track);
871
+ await this.engine.publishStream(MediaType.VIDEO);
872
+ await this.applyVideoEncoderPreference(
873
+ encoderSize ?? (width && height ? [width, height] : void 0),
874
+ encoderOptions
875
+ );
876
+ this.localVideoPublished = true;
877
+ this.publishStartedAtMs = publishStartedAtMs;
878
+ this.localStreamStatsCount = 0;
879
+ this.firstNonZeroSentFrameRateAtMs = 0;
880
+ this.lastObservedSentFrameRate = 0;
881
+ this.ensureSeiSupport();
882
+ this.emitState();
883
+ this.log("success", this.i18n.t("rtc.externalVideoPublished"));
884
+ }
885
+ async stopVideoPublishing(options) {
886
+ if (!this.engine) {
887
+ return;
888
+ }
889
+ this.stopSeiSending();
890
+ this.clearPublishReadyWaiters();
891
+ if (this.localVideoPublished) {
892
+ await this.engine.unpublishStream(MediaType.VIDEO);
893
+ }
894
+ if (this.localVideoSourceType === VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL) {
895
+ await this.engine.stopVideoCapture();
896
+ }
897
+ if (this.externalVideoTrack) {
898
+ if (!options?.preserveExternalTrack) {
899
+ try {
900
+ this.externalVideoTrack.stop();
901
+ } catch {
902
+ }
903
+ }
904
+ this.externalVideoTrack = null;
905
+ }
906
+ await this.switchToInternalVideoSource(false);
907
+ this.clearUploadPreviewLayoutObserver();
908
+ this.clearContainer(this.localVideoContainer);
909
+ this.clearRemoteVideo();
910
+ this.localVideoPublished = false;
911
+ this.publishStartedAtMs = 0;
912
+ this.lastSentResolution = "";
913
+ this.emitState();
914
+ this.log("warn", this.i18n.t("rtc.localVideoStopped"));
915
+ }
916
+ async leave() {
917
+ if (this.leaveTask) {
918
+ await this.leaveTask;
919
+ return;
920
+ }
921
+ const engine = this.engine;
922
+ if (!engine) {
923
+ return;
924
+ }
925
+ this.leaveTask = (async () => {
926
+ try {
927
+ if (this.localVideoPublished) {
928
+ await this.stopVideoPublishing();
929
+ }
930
+ await engine.leaveRoom();
931
+ } catch (error) {
932
+ this.log("warn", this.i18n.t("rtc.leaveRoomError"), error);
933
+ } finally {
934
+ try {
935
+ engine.destroy();
936
+ } catch {
937
+ }
938
+ if (this.engine === engine) {
939
+ this.engine = null;
940
+ }
941
+ this.currentJoinInfo = null;
942
+ this.users.clear();
943
+ this.joined = false;
944
+ this.localVideoPublished = false;
945
+ this.localVideoSourceType = VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL;
946
+ this.shouldMirrorLocalPreview = false;
947
+ this.externalVideoTrack = null;
948
+ this.remoteVideoUserId = null;
949
+ this.pendingRemoteVideoUserId = null;
950
+ this.pendingRemoteVideoMediaType = MediaType.VIDEO;
951
+ this.subscribedRemoteUserId = null;
952
+ this.subscribedRemoteMediaType = MediaType.VIDEO;
953
+ this.stopSeiSending();
954
+ this.clearSeiGateFallbackTimer();
955
+ this.clearSeiMatchedDelayTimer();
956
+ this.stopRemoteFrameWatch();
957
+ this.clearPublishReadyWaiters();
958
+ this.joinedAtMs = 0;
959
+ this.publishStartedAtMs = 0;
960
+ this.lastRoomStartEventAtMs = 0;
961
+ this.localStreamStatsCount = 0;
962
+ this.firstNonZeroSentFrameRateAtMs = 0;
963
+ this.lastSentResolution = "";
964
+ this.lastReceivedResolution = "";
965
+ this.clearVideoContainers();
966
+ this.emitState();
967
+ this.log("info", this.i18n.t("rtc.engineDestroyed"));
968
+ }
969
+ })();
970
+ try {
971
+ await this.leaveTask;
972
+ } finally {
973
+ this.leaveTask = null;
974
+ }
975
+ }
976
+ /**
977
+ * Resolve whether to wait for input frames before start.
978
+ * Warm pipelines (restart / long-lived publish) skip — iOS sends start while frames already flow.
979
+ */
980
+ resolveInputFrameWaitOptions() {
981
+ if (!this.localVideoPublished || this.publishStartedAtMs <= 0) {
982
+ return {
983
+ minFrames: PUBLISH_COLD_MIN_FRAMES,
984
+ timeoutMs: PUBLISH_COLD_FRAME_TIMEOUT_MS,
985
+ minPublishMs: PUBLISH_COLD_WARMUP_MS
986
+ };
987
+ }
988
+ const elapsedMs = performance.now() - this.publishStartedAtMs;
989
+ if (elapsedMs >= PUBLISH_WARM_SKIP_MS) {
990
+ return null;
991
+ }
992
+ return {
993
+ minFrames: PUBLISH_COLD_MIN_FRAMES,
994
+ timeoutMs: Math.max(400, PUBLISH_COLD_FRAME_TIMEOUT_MS - Math.floor(elapsedMs)),
995
+ minPublishMs: Math.max(0, PUBLISH_COLD_WARMUP_MS - elapsedMs)
996
+ };
997
+ }
998
+ /**
999
+ * Wait until the published input track produces enough frames before sending start.
1000
+ * Uses actual frame delivery instead of sentFrameRate (~2s Volcengine stats window).
1001
+ */
1002
+ async waitForPublishedInputFrames(options = {}) {
1003
+ const track = this.externalVideoTrack ?? this.engine?.getLocalStreamTrack(StreamIndex.STREAM_INDEX_MAIN, "video") ?? null;
1004
+ if (!track || track.readyState === "ended") {
1005
+ return { ready: false, frameCount: 0, elapsedMs: 0 };
1006
+ }
1007
+ const { waitForMediaStreamTrackFrames } = await import('./local-publish-diagnostics-ABLL56IT.js');
1008
+ return waitForMediaStreamTrackFrames(track, {
1009
+ minFrames: options.minFrames ?? PUBLISH_START_MIN_FRAMES,
1010
+ timeoutMs: options.timeoutMs ?? PUBLISH_START_FRAME_TIMEOUT_MS
1011
+ });
1012
+ }
1013
+ /**
1014
+ * Before every start: ensure the publish track is producing frames.
1015
+ * Cold — longer warmup; warm restart — quick sync (~200ms) so backend gets steady input after start.
1016
+ */
1017
+ async ensurePublishedInputBeforeStart() {
1018
+ if (!this.localVideoPublished || this.publishStartedAtMs <= 0) {
1019
+ return;
1020
+ }
1021
+ const publishAgeMs = performance.now() - this.publishStartedAtMs;
1022
+ const isCold = publishAgeMs < PUBLISH_WARM_SKIP_MS;
1023
+ if (!isCold && this.lastObservedSentFrameRate >= 15) {
1024
+ this.debugLog("ensurePublishedInputBeforeStart (warm, outbound steady)", {
1025
+ publishAgeMs: Number(publishAgeMs.toFixed(1)),
1026
+ sentFrameRate: this.lastObservedSentFrameRate
1027
+ });
1028
+ return;
1029
+ }
1030
+ if (isCold) {
1031
+ const [frameResult2] = await Promise.all([
1032
+ this.waitForPublishedInputFrames({
1033
+ minFrames: COLD_START_MIN_FRAMES,
1034
+ timeoutMs: PUBLISH_START_FRAME_TIMEOUT_MS
1035
+ }),
1036
+ this.waitForMinPublishDuration(PUBLISH_COLD_WARMUP_MS)
1037
+ ]);
1038
+ {
1039
+ await new Promise((resolve) => {
1040
+ setTimeout(resolve, COLD_START_SETTLE_MS);
1041
+ });
1042
+ }
1043
+ this.debugLog("ensurePublishedInputBeforeStart (cold)", {
1044
+ publishAgeMs: Number(publishAgeMs.toFixed(1)),
1045
+ frameReady: frameResult2.ready,
1046
+ frameCount: frameResult2.frameCount,
1047
+ sentFrameRate: this.lastObservedSentFrameRate
1048
+ });
1049
+ return;
1050
+ }
1051
+ const frameResult = await this.waitForPublishedInputFrames({
1052
+ minFrames: WARM_START_MIN_FRAMES,
1053
+ timeoutMs: WARM_START_FRAME_TIMEOUT_MS
1054
+ });
1055
+ if (!frameResult.ready && this.lastObservedSentFrameRate <= 0) {
1056
+ await this.waitForLocalPublishReady({ timeoutMs: 800 });
1057
+ }
1058
+ this.debugLog("ensurePublishedInputBeforeStart (warm sync)", {
1059
+ publishAgeMs: Number(publishAgeMs.toFixed(1)),
1060
+ frameReady: frameResult.ready,
1061
+ frameCount: frameResult.frameCount,
1062
+ sentFrameRate: this.lastObservedSentFrameRate
1063
+ });
1064
+ }
1065
+ /** @deprecated Use {@link ensurePublishedInputBeforeStart}. */
1066
+ async waitForRtcOutboundBeforeStart(options) {
1067
+ const timeoutMs = options?.timeoutMs ?? PUBLISH_READY_TIMEOUT_MS;
1068
+ const publishAgeMs = this.publishStartedAtMs > 0 ? performance.now() - this.publishStartedAtMs : 0;
1069
+ const localFramesPromise = publishAgeMs >= PUBLISH_WARM_SKIP_MS ? Promise.resolve({ ready: true, frameCount: 0, elapsedMs: 0 }) : this.waitForPublishedInputFrames({
1070
+ minFrames: PUBLISH_START_MIN_FRAMES,
1071
+ timeoutMs: PUBLISH_START_FRAME_TIMEOUT_MS
1072
+ });
1073
+ const [, rtcReady] = await Promise.all([
1074
+ localFramesPromise,
1075
+ this.waitForLocalPublishReady({ timeoutMs })
1076
+ ]);
1077
+ return rtcReady;
1078
+ }
1079
+ /** @deprecated Use {@link waitForRtcOutboundBeforeStart}. */
1080
+ async waitForPublishedInputFramesBeforeStart() {
1081
+ const waitOptions = this.resolveInputFrameWaitOptions();
1082
+ const waitTasks = [
1083
+ this.waitForLocalPublishReady({ timeoutMs: PUBLISH_READY_TIMEOUT_MS })
1084
+ ];
1085
+ if (waitOptions) {
1086
+ waitTasks.push(
1087
+ this.waitForMinPublishDuration(waitOptions.minPublishMs),
1088
+ this.waitForPublishedInputFrames({
1089
+ minFrames: waitOptions.minFrames,
1090
+ timeoutMs: waitOptions.timeoutMs
1091
+ })
1092
+ );
1093
+ }
1094
+ const [rtcResult, ...rest] = await Promise.all(waitTasks);
1095
+ const rtcSend = rtcResult;
1096
+ const frameResult = waitOptions ? rest[1] : { ready: true, frameCount: 0, elapsedMs: 0 };
1097
+ return {
1098
+ ...frameResult,
1099
+ rtcSendReady: rtcSend.ready,
1100
+ rtcSendReason: rtcSend.reason,
1101
+ rtcSendElapsedMs: rtcSend.elapsedSincePublishMs
1102
+ };
1103
+ }
1104
+ async waitForMinPublishDuration(minMs) {
1105
+ if (minMs <= 0 || this.publishStartedAtMs <= 0) {
1106
+ return;
1107
+ }
1108
+ const elapsedMs = performance.now() - this.publishStartedAtMs;
1109
+ const remainingMs = minMs - elapsedMs;
1110
+ if (remainingMs <= 0) {
1111
+ return;
1112
+ }
1113
+ await new Promise((resolve) => {
1114
+ setTimeout(resolve, remainingMs);
1115
+ });
1116
+ }
1117
+ /**
1118
+ * Subscribe to the bot output stream before start — iOS bindRemoteVideoRenderer runs pre-request.
1119
+ */
1120
+ async prepareRemoteVideoForGeneration() {
1121
+ const botUserId = this.currentJoinInfo?.botName?.trim();
1122
+ if (!botUserId || !this.engine || !this.joined) {
1123
+ return;
1124
+ }
1125
+ this.pendingRemoteVideoUserId = botUserId;
1126
+ this.pendingRemoteVideoMediaType = MediaType.VIDEO;
1127
+ await this.ensureRemoteStreamSubscribed(botUserId, MediaType.VIDEO);
1128
+ }
1129
+ /**
1130
+ * Wait until outbound sentFrameRate reaches minFps and stays there for stableMs.
1131
+ * Avoids starting while Volcengine encoder is still ramping (causes early dropped frames).
1132
+ */
1133
+ async waitForStableLocalPublishReady(options) {
1134
+ const minFps = options?.minFps ?? COLD_START_MIN_OUTBOUND_FPS;
1135
+ const timeoutMs = options?.timeoutMs ?? COLD_START_OUTBOUND_TIMEOUT_MS;
1136
+ const stableMs = options?.stableMs ?? COLD_START_OUTBOUND_STABLE_MS;
1137
+ const publishStartedAtMs = this.publishStartedAtMs;
1138
+ if (!this.localVideoPublished || publishStartedAtMs <= 0) {
1139
+ return {
1140
+ ready: false,
1141
+ reason: "not-published",
1142
+ elapsedSincePublishMs: 0
1143
+ };
1144
+ }
1145
+ return new Promise((resolve) => {
1146
+ let settled = false;
1147
+ let stableTimer = null;
1148
+ const finish = (result) => {
1149
+ if (settled) {
1150
+ return;
1151
+ }
1152
+ settled = true;
1153
+ clearTimeout(timeoutTimer);
1154
+ if (stableTimer !== null) {
1155
+ clearTimeout(stableTimer);
1156
+ }
1157
+ const index = this.stablePublishWaiters.indexOf(check);
1158
+ if (index >= 0) {
1159
+ this.stablePublishWaiters.splice(index, 1);
1160
+ }
1161
+ resolve(result);
1162
+ };
1163
+ const check = () => {
1164
+ if (this.lastObservedSentFrameRate >= minFps) {
1165
+ if (stableTimer === null) {
1166
+ stableTimer = setTimeout(() => {
1167
+ finish({
1168
+ ready: true,
1169
+ reason: "sent-frame-rate",
1170
+ elapsedSincePublishMs: Number((performance.now() - publishStartedAtMs).toFixed(1))
1171
+ });
1172
+ }, stableMs);
1173
+ }
1174
+ return;
1175
+ }
1176
+ if (stableTimer !== null) {
1177
+ clearTimeout(stableTimer);
1178
+ stableTimer = null;
1179
+ }
1180
+ };
1181
+ this.stablePublishWaiters.push(check);
1182
+ check();
1183
+ const timeoutTimer = setTimeout(() => {
1184
+ finish({
1185
+ ready: this.lastObservedSentFrameRate > 0,
1186
+ reason: "timeout",
1187
+ elapsedSincePublishMs: Number((performance.now() - publishStartedAtMs).toFixed(1))
1188
+ });
1189
+ }, timeoutMs);
1190
+ });
1191
+ }
1192
+ /**
1193
+ * Wait until Volcengine reports non-zero sentFrameRate (~2s stats window).
1194
+ * Prefer this over local track frame counts before start — backend only processes from start.
1195
+ */
1196
+ async waitForLocalPublishReady(options) {
1197
+ const timeoutMs = options?.timeoutMs ?? PUBLISH_READY_TIMEOUT_MS;
1198
+ const publishStartedAtMs = this.publishStartedAtMs;
1199
+ if (!this.localVideoPublished || publishStartedAtMs <= 0) {
1200
+ return {
1201
+ ready: false,
1202
+ reason: "not-published",
1203
+ elapsedSincePublishMs: 0
1204
+ };
1205
+ }
1206
+ if (this.firstNonZeroSentFrameRateAtMs > 0) {
1207
+ return {
1208
+ ready: true,
1209
+ reason: "already-ready",
1210
+ elapsedSincePublishMs: Number((this.firstNonZeroSentFrameRateAtMs - publishStartedAtMs).toFixed(1))
1211
+ };
1212
+ }
1213
+ return new Promise((resolve) => {
1214
+ let settled = false;
1215
+ const finish = (result) => {
1216
+ if (settled) {
1217
+ return;
1218
+ }
1219
+ settled = true;
1220
+ clearTimeout(timeoutTimer);
1221
+ const index = this.publishReadyWaiters.indexOf(onReady);
1222
+ if (index >= 0) {
1223
+ this.publishReadyWaiters.splice(index, 1);
1224
+ }
1225
+ resolve(result);
1226
+ };
1227
+ const onReady = (result) => finish(result);
1228
+ this.publishReadyWaiters.push(onReady);
1229
+ const timeoutTimer = setTimeout(() => {
1230
+ finish({
1231
+ ready: false,
1232
+ reason: "timeout",
1233
+ elapsedSincePublishMs: Number((performance.now() - publishStartedAtMs).toFixed(1))
1234
+ });
1235
+ }, timeoutMs);
1236
+ });
1237
+ }
1238
+ async sendRoomEvent(event) {
1239
+ this.ensureReadyForMessaging();
1240
+ const payload = JSON.stringify(event);
1241
+ const eventName = typeof event === "object" && event !== null && "event" in event ? String(event.event ?? "unknown") : "unknown";
1242
+ if (eventName === "start") {
1243
+ this.lastRoomStartEventAtMs = performance.now();
1244
+ }
1245
+ await this.engine?.sendRoomMessage(payload);
1246
+ this.log("success", this.i18n.t("rtc.roomEventSent"), event);
1247
+ }
1248
+ clearRemoteVideo(options) {
1249
+ this.stopRemoteFrameWatch();
1250
+ if (!options?.preserveDom && !this.preserveRemoteDom) {
1251
+ this.clearContainer(this.remoteVideoContainer);
1252
+ }
1253
+ if (this.remoteVideoUserId) {
1254
+ this.pendingRemoteVideoUserId = this.remoteVideoUserId;
1255
+ this.remoteVideoUserId = null;
1256
+ this.emitState();
1257
+ }
1258
+ }
1259
+ async refreshRemoteVideoBinding(options) {
1260
+ const targetUserId = this.remoteVideoUserId ?? this.pendingRemoteVideoUserId;
1261
+ if (!this.engine || !targetUserId || !this.remoteVideoContainer) {
1262
+ return;
1263
+ }
1264
+ await this.ensureRemoteStreamSubscribed(targetUserId, this.pendingRemoteVideoMediaType);
1265
+ if (!this.canRenderRemoteVideo(targetUserId)) {
1266
+ this.debugLog("refreshRemoteVideoBinding blocked by SEI gate", {
1267
+ targetUserId,
1268
+ expectedRemoteSei: this.expectedRemoteSei,
1269
+ lastRemoteSei: this.lastRemoteSei
1270
+ });
1271
+ return;
1272
+ }
1273
+ if (!options?.force && this.remoteVideoUserId === targetUserId && this.remoteVideoContainer && this.remoteVideoContainer.childElementCount > 0) {
1274
+ this.debugLog("refreshRemoteVideoBinding skipped (already mounted)", { targetUserId });
1275
+ return;
1276
+ }
1277
+ try {
1278
+ await this.mountRemoteVideoPlayer(
1279
+ targetUserId,
1280
+ this.remoteVideoContainer,
1281
+ this.pendingRemoteVideoMediaType,
1282
+ options?.force
1283
+ );
1284
+ this.remoteVideoUserId = targetUserId;
1285
+ this.pendingRemoteVideoUserId = null;
1286
+ this.emitState();
1287
+ this.startRemoteFrameWatch(targetUserId);
1288
+ this.emitRemotePlayerMounted();
1289
+ this.log("info", this.i18n.t("rtc.remotePlayerRefreshed", { userId: targetUserId }));
1290
+ } catch (error) {
1291
+ this.log("warn", this.i18n.t("rtc.remotePlayerRefreshFailed"), error);
1292
+ }
1293
+ }
1294
+ bindEngineEvents(engine) {
1295
+ engine.on(VERTC.events.onUserJoined, (event) => {
1296
+ this.users.add(event.userInfo.userId);
1297
+ this.emitState();
1298
+ this.log("success", this.i18n.t("rtc.userJoined", { userId: event.userInfo.userId }), event);
1299
+ this.log("info", this.i18n.t("rtc.roomUsers", { users: this.snapshot.users.join(", ") }), this.snapshot.users);
1300
+ });
1301
+ engine.on(VERTC.events.onUserLeave, (event) => {
1302
+ this.users.delete(event.userInfo.userId);
1303
+ if (this.remoteVideoUserId === event.userInfo.userId) {
1304
+ this.remoteVideoUserId = null;
1305
+ this.clearContainer(this.remoteVideoContainer);
1306
+ }
1307
+ if (this.pendingRemoteVideoUserId === event.userInfo.userId) {
1308
+ this.pendingRemoteVideoUserId = null;
1309
+ }
1310
+ this.emitState();
1311
+ this.log("warn", this.i18n.t("rtc.userLeft", { userId: event.userInfo.userId }), event);
1312
+ this.log("info", this.i18n.t("rtc.roomUsers", { users: this.snapshot.users.join(", ") }), this.snapshot.users);
1313
+ });
1314
+ engine.on(VERTC.events.onConnectionStateChanged, (event) => {
1315
+ this.log("info", this.i18n.t("rtc.connectionStateChanged"), event);
1316
+ });
1317
+ engine.on(VERTC.events.onUserPublishStream, (event) => {
1318
+ this.log("success", this.i18n.t("rtc.userPublished", { userId: event.userId }), event);
1319
+ if (this.supportsVideo(event.mediaType)) {
1320
+ void this.attachRemoteVideo(event.userId, event.mediaType);
1321
+ }
1322
+ });
1323
+ engine.on(VERTC.events.onUserUnpublishStream, (event) => {
1324
+ this.log("warn", this.i18n.t("rtc.userUnpublished", { userId: event.userId }), event);
1325
+ this.debugLog("onUserUnpublishStream", {
1326
+ userId: event.userId,
1327
+ mediaType: event.mediaType,
1328
+ remoteVideoUserId: this.remoteVideoUserId,
1329
+ expectedRemoteSei: this.expectedRemoteSei,
1330
+ lastRemoteSei: this.lastRemoteSei
1331
+ });
1332
+ if (this.remoteVideoUserId === event.userId && this.supportsVideo(event.mediaType)) {
1333
+ this.pendingRemoteVideoUserId = event.userId;
1334
+ this.pendingRemoteVideoMediaType = event.mediaType;
1335
+ this.remoteVideoUserId = null;
1336
+ this.stopRemoteFrameWatch();
1337
+ if (!this.preserveRemoteDom) {
1338
+ this.clearContainer(this.remoteVideoContainer);
1339
+ }
1340
+ this.emitState();
1341
+ return;
1342
+ }
1343
+ });
1344
+ engine.on(VERTC.events.onSEIMessageReceived, (event) => {
1345
+ const userId = event.remoteStreamKey.userId;
1346
+ const sei = this.normalizeSei(this.decodeSeiMessage(event.sei));
1347
+ if (sei && this.isRemoteSeiUser(userId)) {
1348
+ this.onRemoteSeiReceived?.({ sei, userId });
1349
+ }
1350
+ this.lastRemoteSei = sei;
1351
+ this.lastRemoteSeiUserId = userId;
1352
+ const matched = this.isSeiMatched();
1353
+ const seiStateChanged = this.lastDebugSeiMatched !== matched || this.lastDebugRemoteSei !== sei;
1354
+ if (!this.debug || seiStateChanged) {
1355
+ this.log("info", this.i18n.t("rtc.seiReceived", { userId }), { userId, sei, matched });
1356
+ }
1357
+ if (this.debug && seiStateChanged) {
1358
+ this.debugLog("onSEIMessageReceived", {
1359
+ userId,
1360
+ sei,
1361
+ matched,
1362
+ expectedRemoteSei: this.expectedRemoteSei
1363
+ });
1364
+ this.lastDebugSeiMatched = matched;
1365
+ this.lastDebugRemoteSei = sei;
1366
+ }
1367
+ if (!matched) {
1368
+ this.onSeiGateChange?.({
1369
+ expectedSei: this.expectedRemoteSei,
1370
+ sei,
1371
+ userId,
1372
+ matched: false
1373
+ });
1374
+ return;
1375
+ }
1376
+ if (this.seiMatchedDelayTimer !== null) {
1377
+ return;
1378
+ }
1379
+ this.clearSeiGateFallbackTimer();
1380
+ this.stopSeiSending();
1381
+ this.seiMatchedDelayTimer = setTimeout(() => {
1382
+ this.seiMatchedDelayTimer = null;
1383
+ if (!this.isSeiMatched()) {
1384
+ return;
1385
+ }
1386
+ this.onSeiGateChange?.({
1387
+ expectedSei: this.expectedRemoteSei,
1388
+ sei,
1389
+ userId,
1390
+ matched: true
1391
+ });
1392
+ if (this.remoteVideoUserId === userId) {
1393
+ return;
1394
+ }
1395
+ void this.attachRemoteVideo(userId, this.pendingRemoteVideoMediaType);
1396
+ }, _RtcManager.SEI_MATCHED_DISPLAY_DELAY_MS);
1397
+ });
1398
+ engine.on(VERTC.events.onError, (event) => {
1399
+ this.log("error", this.i18n.t("rtc.error", { code: String(event.errorCode) }), event);
1400
+ });
1401
+ engine.on(VERTC.events.onPublishResult, (event) => {
1402
+ this.debugLog("onPublishResult", {
1403
+ ...event,
1404
+ elapsedSincePublishMs: this.publishStartedAtMs > 0 ? Number((performance.now() - this.publishStartedAtMs).toFixed(1)) : null
1405
+ });
1406
+ });
1407
+ engine.on(VERTC.events.onLocalStreamStats, (stats) => {
1408
+ this.logLocalStreamStats(stats);
1409
+ if (stats.videoStats) {
1410
+ const width = stats.videoStats.sentResolutionWidth ?? stats.videoStats.encodedFrameWidth ?? 0;
1411
+ const height = stats.videoStats.sentResolutionHeight ?? stats.videoStats.encodedFrameHeight ?? 0;
1412
+ stats.videoStats.sentFrameRate ?? 0;
1413
+ const res = `${width}x${height}`;
1414
+ if (width > 0 && height > 0 && res !== this.lastSentResolution) {
1415
+ this.lastSentResolution = res;
1416
+ }
1417
+ }
1418
+ });
1419
+ engine.on(VERTC.events.onRemoteStreamStats, (stats) => {
1420
+ if (stats?.videoStats) {
1421
+ const { width, height, receivedFrameRate } = stats.videoStats;
1422
+ const w = width ?? 0;
1423
+ const h = height ?? 0;
1424
+ const res = `${w}x${h}`;
1425
+ if (w > 0 && h > 0 && res !== this.lastReceivedResolution) {
1426
+ this.lastReceivedResolution = res;
1427
+ }
1428
+ }
1429
+ });
1430
+ engine.on(VERTC.events.onLocalStatsException, (exception) => {
1431
+ this.debugLog("onLocalStatsException", {
1432
+ exception,
1433
+ elapsedSincePublishMs: this.publishStartedAtMs > 0 ? Number((performance.now() - this.publishStartedAtMs).toFixed(1)) : null
1434
+ });
1435
+ });
1436
+ engine.on(VERTC.events.onTrackMute, (event) => {
1437
+ this.debugLog("onTrackMute", event);
1438
+ });
1439
+ engine.on(VERTC.events.onTrackUnmute, (event) => {
1440
+ this.debugLog("onTrackUnmute", event);
1441
+ });
1442
+ }
1443
+ emitState() {
1444
+ this.onStateChange?.(this.snapshot);
1445
+ }
1446
+ ensureReadyForMessaging() {
1447
+ if (!this.engine || !this.joined) {
1448
+ throw new Error(this.i18n.t("errors.rtcNotJoinedForMessaging"));
1449
+ }
1450
+ }
1451
+ bindLocalVideoPlayer(force = false) {
1452
+ if (!this.localVideoContainer) {
1453
+ return;
1454
+ }
1455
+ const container = this.getRequiredContainer(this.localVideoContainer, "local");
1456
+ if (!force && container.childElementCount > 0) {
1457
+ this.bindUploadPreviewLayout(container);
1458
+ return;
1459
+ }
1460
+ container.replaceChildren();
1461
+ this.engine?.setLocalVideoMirrorType(
1462
+ this.shouldMirrorLocalPreview ? MirrorType.MIRROR_TYPE_RENDER : MirrorType.MIRROR_TYPE_NONE
1463
+ );
1464
+ this.engine?.setLocalVideoPlayer(StreamIndex.STREAM_INDEX_MAIN, {
1465
+ renderDom: container,
1466
+ renderMode: resolveVideoRenderMode(container)
1467
+ });
1468
+ this.bindUploadPreviewLayout(container);
1469
+ }
1470
+ bindUploadPreviewLayout(container) {
1471
+ this.uploadPreviewLayoutObserverStop?.();
1472
+ this.uploadPreviewLayoutObserverStop = null;
1473
+ applyUploadPreviewMediaLayout(container);
1474
+ this.uploadPreviewLayoutObserverStop = observeUploadPreviewMediaLayout(container);
1475
+ }
1476
+ clearUploadPreviewLayoutObserver() {
1477
+ this.uploadPreviewLayoutObserverStop?.();
1478
+ this.uploadPreviewLayoutObserverStop = null;
1479
+ }
1480
+ /** Re-bind local preview after the container becomes visible or is resized (mobile H5). */
1481
+ refreshLocalVideoPreview(options) {
1482
+ if (!this.engine || !this.localVideoPublished || !this.localVideoContainer) {
1483
+ return;
1484
+ }
1485
+ this.bindLocalVideoPlayer(options?.force);
1486
+ }
1487
+ async switchToExternalVideoSource(track) {
1488
+ if (!this.engine) {
1489
+ return;
1490
+ }
1491
+ this.externalVideoTrack = track;
1492
+ await this.engine.setVideoSourceType(StreamIndex.STREAM_INDEX_MAIN, VideoSourceType.VIDEO_SOURCE_TYPE_EXTERNAL);
1493
+ await this.engine.setExternalVideoTrack(StreamIndex.STREAM_INDEX_MAIN, track);
1494
+ this.localVideoSourceType = VideoSourceType.VIDEO_SOURCE_TYPE_EXTERNAL;
1495
+ }
1496
+ async switchToInternalVideoSource(shouldSetConfig = true) {
1497
+ if (!this.engine) {
1498
+ return;
1499
+ }
1500
+ if (this.localVideoSourceType === VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL) {
1501
+ return;
1502
+ }
1503
+ await this.engine.setVideoSourceType(StreamIndex.STREAM_INDEX_MAIN, VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL);
1504
+ this.localVideoSourceType = VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL;
1505
+ if (shouldSetConfig && this.externalVideoTrack) {
1506
+ this.externalVideoTrack = null;
1507
+ }
1508
+ }
1509
+ async applyLocalVideoCaptureConfig([width, height]) {
1510
+ if (!this.engine) {
1511
+ return;
1512
+ }
1513
+ await this.engine.setVideoCaptureConfig({
1514
+ width,
1515
+ height
1516
+ });
1517
+ this.log("info", this.i18n.t("rtc.captureSizeSet", { width, height }));
1518
+ }
1519
+ resolveEncoderMaxKbps(width, height, highQuality = true) {
1520
+ const pixels = width * height;
1521
+ if (highQuality) {
1522
+ if (pixels >= 2e6) {
1523
+ return 6e3;
1524
+ }
1525
+ if (pixels >= 1e6) {
1526
+ return 4e3;
1527
+ }
1528
+ return 3e3;
1529
+ }
1530
+ if (pixels >= 2e6) {
1531
+ return 5e3;
1532
+ }
1533
+ if (pixels >= 1e6) {
1534
+ return 3e3;
1535
+ }
1536
+ return 2e3;
1537
+ }
1538
+ async applyVideoEncoderPreference(size, publishOptions) {
1539
+ if (!this.engine) {
1540
+ return;
1541
+ }
1542
+ try {
1543
+ const width = size?.[0] ?? 1280;
1544
+ const height = size?.[1] ?? 720;
1545
+ if (publishOptions) {
1546
+ const fps = publishOptions.fps ?? 30;
1547
+ const maxKbps = this.resolveEncoderMaxKbps(width, height, publishOptions.highQuality);
1548
+ await this.engine.setVideoEncoderConfig({
1549
+ width,
1550
+ height,
1551
+ frameRate: fps,
1552
+ maxKbps,
1553
+ contentHint: "detail",
1554
+ preferCodecName: VideoCodecType.H264
1555
+ });
1556
+ } else {
1557
+ await this.engine.setVideoEncoderConfig({
1558
+ width,
1559
+ height,
1560
+ frameRate: 24,
1561
+ maxKbps: -1,
1562
+ contentHint: "detail",
1563
+ // @ts-expect-error: encodePreference is supported by ByteRTC SDK but may be missing from current typings
1564
+ encodePreference: 1,
1565
+ preferCodecName: VideoCodecType.H264
1566
+ });
1567
+ }
1568
+ this.log("info", this.i18n.t("rtc.encoderPreferenceSet"), size);
1569
+ } catch (error) {
1570
+ this.log("warn", this.i18n.t("rtc.encoderPreferenceFailed"), error);
1571
+ }
1572
+ }
1573
+ async attachRemoteVideo(userId, mediaType) {
1574
+ if (!this.engine || !this.currentJoinInfo) {
1575
+ return;
1576
+ }
1577
+ const preferredBotUserId = this.currentJoinInfo.botName?.trim() || null;
1578
+ const shouldReplaceCurrent = this.remoteVideoUserId === null || userId === preferredBotUserId || this.remoteVideoUserId !== preferredBotUserId;
1579
+ if (!shouldReplaceCurrent) {
1580
+ return;
1581
+ }
1582
+ this.pendingRemoteVideoUserId = userId;
1583
+ this.pendingRemoteVideoMediaType = mediaType;
1584
+ await this.ensureRemoteStreamSubscribed(userId, mediaType);
1585
+ if (!this.canRenderRemoteVideo(userId)) {
1586
+ this.log("info", this.i18n.t("rtc.remoteWaitSei", { userId }), {
1587
+ expectedRemoteSei: this.expectedRemoteSei,
1588
+ lastRemoteSei: this.lastRemoteSei,
1589
+ lastRemoteSeiUserId: this.lastRemoteSeiUserId
1590
+ });
1591
+ return;
1592
+ }
1593
+ const container = this.remoteVideoContainer;
1594
+ if (!container) {
1595
+ this.log("warn", this.i18n.t("rtc.remoteContainerNotReady"), { userId, mediaType });
1596
+ return;
1597
+ }
1598
+ await this.mountRemoteVideoPlayer(userId, container, mediaType);
1599
+ this.remoteVideoUserId = userId;
1600
+ this.pendingRemoteVideoUserId = null;
1601
+ this.emitState();
1602
+ this.startRemoteFrameWatch(userId);
1603
+ this.emitRemotePlayerMounted();
1604
+ this.log("success", this.i18n.t("rtc.remotePlayerBound", { userId }));
1605
+ }
1606
+ async ensureRemoteStreamSubscribed(userId, mediaType) {
1607
+ if (!this.engine) {
1608
+ return;
1609
+ }
1610
+ if (this.subscribedRemoteUserId === userId) {
1611
+ return;
1612
+ }
1613
+ await this.engine.subscribeStream(userId, mediaType);
1614
+ this.subscribedRemoteUserId = userId;
1615
+ this.subscribedRemoteMediaType = mediaType;
1616
+ this.debugLog("ensureRemoteStreamSubscribed", { userId, mediaType });
1617
+ }
1618
+ bindRemoteVideoPlayerToContainer(userId, container) {
1619
+ if (!this.engine) {
1620
+ return;
1621
+ }
1622
+ this.engine.setRemoteVideoPlayer(StreamIndex.STREAM_INDEX_MAIN, {
1623
+ userId,
1624
+ renderDom: container,
1625
+ renderMode: resolveVideoRenderMode(container)
1626
+ });
1627
+ this.bindUploadPreviewLayout(container);
1628
+ }
1629
+ async mountRemoteVideoPlayer(userId, container, mediaType, force = false) {
1630
+ if (!this.engine) {
1631
+ return;
1632
+ }
1633
+ if (!force && this.remoteVideoUserId === userId && container === this.remoteVideoContainer && container.childElementCount > 0) {
1634
+ this.debugLog("mountRemoteVideoPlayer skipped (already mounted)", { userId });
1635
+ return;
1636
+ }
1637
+ this.debugLog(force ? "mountRemoteVideoPlayer (force)" : "mountRemoteVideoPlayer", {
1638
+ userId,
1639
+ mediaType,
1640
+ childCountBefore: container.childElementCount
1641
+ });
1642
+ this.stopRemoteFrameWatch();
1643
+ container.replaceChildren();
1644
+ await this.ensureRemoteStreamSubscribed(userId, mediaType);
1645
+ this.bindRemoteVideoPlayerToContainer(userId, container);
1646
+ }
1647
+ startRemoteFrameWatch(userId) {
1648
+ this.stopRemoteFrameWatch();
1649
+ let lastFrameAt = 0;
1650
+ let hadFirstFrame = false;
1651
+ let rvfcHandle = null;
1652
+ let watchedVideo = null;
1653
+ const onFrame = () => {
1654
+ lastFrameAt = Date.now();
1655
+ hadFirstFrame = true;
1656
+ };
1657
+ const bindRvfc = (video) => {
1658
+ if (watchedVideo === video && rvfcHandle !== null) {
1659
+ return;
1660
+ }
1661
+ if (watchedVideo && watchedVideo !== video) {
1662
+ watchedVideo.removeEventListener("timeupdate", onFrame);
1663
+ }
1664
+ watchedVideo = video;
1665
+ const rvfcVideo = video;
1666
+ if (rvfcVideo.requestVideoFrameCallback) {
1667
+ const schedule = () => {
1668
+ if (this.remoteVideoUserId !== userId) {
1669
+ return;
1670
+ }
1671
+ rvfcHandle = rvfcVideo.requestVideoFrameCallback(() => {
1672
+ onFrame();
1673
+ schedule();
1674
+ });
1675
+ };
1676
+ schedule();
1677
+ return;
1678
+ }
1679
+ video.addEventListener("timeupdate", onFrame);
1680
+ };
1681
+ const pollTimer = setInterval(() => {
1682
+ if (this.remoteVideoUserId !== userId || !this.remoteVideoContainer) {
1683
+ return;
1684
+ }
1685
+ const video = this.remoteVideoContainer.querySelector("video");
1686
+ if (video) {
1687
+ bindRvfc(video);
1688
+ }
1689
+ if (!hadFirstFrame || lastFrameAt === 0) {
1690
+ return;
1691
+ }
1692
+ const stallMs = Date.now() - lastFrameAt;
1693
+ if (stallMs < REMOTE_OUTPUT_STALL_MS) {
1694
+ return;
1695
+ }
1696
+ if (Date.now() < this.remoteOutputRecoveryCooldownUntil) {
1697
+ return;
1698
+ }
1699
+ this.remoteOutputRecoveryCooldownUntil = Date.now() + REMOTE_OUTPUT_RECOVERY_COOLDOWN_MS;
1700
+ this.debugLog("remote output stalled", { userId, stallMs });
1701
+ this.log("warn", this.i18n.t("rtc.remoteOutputStalled"), { userId, stallMs });
1702
+ this.onRemoteOutputStalled?.({ userId, stallMs });
1703
+ hadFirstFrame = false;
1704
+ lastFrameAt = 0;
1705
+ }, 3e3);
1706
+ this.remoteFrameWatchStop = () => {
1707
+ clearInterval(pollTimer);
1708
+ if (watchedVideo) {
1709
+ watchedVideo.removeEventListener("timeupdate", onFrame);
1710
+ }
1711
+ if (rvfcHandle !== null && watchedVideo) {
1712
+ const rvfcVideo = watchedVideo;
1713
+ rvfcVideo.cancelVideoFrameCallback?.(rvfcHandle);
1714
+ }
1715
+ rvfcHandle = null;
1716
+ watchedVideo = null;
1717
+ };
1718
+ }
1719
+ stopRemoteFrameWatch() {
1720
+ this.remoteFrameWatchStop?.();
1721
+ this.remoteFrameWatchStop = null;
1722
+ }
1723
+ isRemoteSeiUser(userId) {
1724
+ const localUserId = this.currentJoinInfo?.userId?.trim();
1725
+ if (localUserId && userId === localUserId) {
1726
+ return false;
1727
+ }
1728
+ const botUserId = this.currentJoinInfo?.botName?.trim();
1729
+ if (botUserId) {
1730
+ return userId === botUserId;
1731
+ }
1732
+ return true;
1733
+ }
1734
+ /** Always mount remote player — SEI gate is UI-only (see deriveOutputVisibility). */
1735
+ canRenderRemoteVideo(_userId) {
1736
+ return true;
1737
+ }
1738
+ /** Whether remote SEI matches the active task (display gate). */
1739
+ isSeiMatched() {
1740
+ if (this.seiSupport === "unsupported") {
1741
+ return true;
1742
+ }
1743
+ if (!this.expectedRemoteSei) {
1744
+ return true;
1745
+ }
1746
+ return this.lastRemoteSei === this.expectedRemoteSei;
1747
+ }
1748
+ /** Low-rate SEI until remote output SEI matches — avoids Volcengine sendSEIMessage timeouts. */
1749
+ startContinuousSeiSending() {
1750
+ this.stopSeiSending();
1751
+ if (!this.localSeiMessage || !this.engine || !this.joined || !this.localVideoPublished || this.seiSupport === "unsupported") {
1752
+ return;
1753
+ }
1754
+ const tick = () => {
1755
+ if (this.isSeiMatched() && this.expectedRemoteSei) {
1756
+ this.stopSeiSending();
1757
+ return;
1758
+ }
1759
+ this.flushSeiMessage();
1760
+ };
1761
+ this.seiSendInterval = setInterval(tick, SESSION_SEI_INTERVAL_MS);
1762
+ }
1763
+ stopSeiSending() {
1764
+ if (this.seiSendInterval !== null) {
1765
+ this.debugLog("stopSeiSending");
1766
+ clearInterval(this.seiSendInterval);
1767
+ this.seiSendInterval = null;
1768
+ }
1769
+ this.lastSeiSentAtMs = 0;
1770
+ }
1771
+ canSendLocalSeiNow() {
1772
+ if (this.firstNonZeroSentFrameRateAtMs <= 0) {
1773
+ return false;
1774
+ }
1775
+ const now = performance.now();
1776
+ return now - this.lastSeiSentAtMs >= SESSION_SEI_INTERVAL_MS - 16;
1777
+ }
1778
+ flushSeiMessage() {
1779
+ if (!this.engine || !this.joined || !this.localVideoPublished || !this.localSeiMessage || this.seiSupport === "unsupported") {
1780
+ return;
1781
+ }
1782
+ if (!this.canSendLocalSeiNow()) {
1783
+ return;
1784
+ }
1785
+ this.lastSeiSentAtMs = performance.now();
1786
+ try {
1787
+ this.engine.sendSEIMessage(StreamIndex.STREAM_INDEX_MAIN, this.localSeiMessage, this.localSeiRepeatCount);
1788
+ } catch (error) {
1789
+ if (!this.hasLoggedSeiSendFailure) {
1790
+ this.log("warn", this.i18n.t("rtc.sendSeiFailed"), error);
1791
+ this.hasLoggedSeiSendFailure = true;
1792
+ }
1793
+ this.stopSeiSending();
1794
+ }
1795
+ }
1796
+ ensureSeiSupport() {
1797
+ if (this.seiSupport !== "unknown") {
1798
+ return;
1799
+ }
1800
+ void VERTC.getSupportedCodecs().then((codecs) => {
1801
+ const supported = codecs.some((c) => c.toLowerCase() === "h264" || c.toLowerCase() === "h.264");
1802
+ this.seiSupport = supported ? "supported" : "unsupported";
1803
+ if (this.seiSupport === "unsupported" && !this.hasLoggedSeiUnsupported) {
1804
+ this.hasLoggedSeiUnsupported = true;
1805
+ this.log("warn", this.i18n.t("rtc.seiUnsupported"), codecs);
1806
+ }
1807
+ if (this.seiSupport === "unsupported") {
1808
+ this.expectedRemoteSei = null;
1809
+ this.emitSeiGateState();
1810
+ void this.refreshRemoteVideoBinding();
1811
+ } else if (this.localSeiMessage) {
1812
+ this.startContinuousSeiSending();
1813
+ this.emitSeiGateState();
1814
+ void this.refreshRemoteVideoBinding();
1815
+ }
1816
+ }).catch((error) => {
1817
+ this.seiSupport = "unsupported";
1818
+ if (!this.hasLoggedSeiUnsupported) {
1819
+ this.hasLoggedSeiUnsupported = true;
1820
+ this.log("warn", this.i18n.t("rtc.getCodecsFailed"), error);
1821
+ }
1822
+ this.expectedRemoteSei = null;
1823
+ this.emitSeiGateState();
1824
+ void this.refreshRemoteVideoBinding();
1825
+ });
1826
+ }
1827
+ normalizeSei(value) {
1828
+ const normalized = value?.trim();
1829
+ if (!normalized) {
1830
+ return null;
1831
+ }
1832
+ return normalized.startsWith("task-") ? normalized.slice("task-".length) : normalized;
1833
+ }
1834
+ normalizeSeiRepeatCount(value) {
1835
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1836
+ return DEFAULT_SEI_REPEAT_COUNT;
1837
+ }
1838
+ return Math.max(0, Math.min(4, Math.floor(value)));
1839
+ }
1840
+ decodeSeiMessage(sei) {
1841
+ try {
1842
+ return new TextDecoder().decode(sei);
1843
+ } catch {
1844
+ return null;
1845
+ }
1846
+ }
1847
+ supportsVideo(mediaType) {
1848
+ return mediaType === MediaType.VIDEO || mediaType === MediaType.AUDIO_AND_VIDEO;
1849
+ }
1850
+ clearVideoContainers() {
1851
+ this.stopRemoteFrameWatch();
1852
+ this.clearContainer(this.localVideoContainer);
1853
+ this.clearContainer(this.remoteVideoContainer);
1854
+ }
1855
+ clearContainer(container) {
1856
+ container?.replaceChildren();
1857
+ }
1858
+ getRequiredContainer(container, containerType) {
1859
+ if (!container) {
1860
+ throw new Error(`missing ${containerType} video container`);
1861
+ }
1862
+ return container;
1863
+ }
1864
+ emitRemotePlayerMounted() {
1865
+ this.onRemotePlayerMounted?.();
1866
+ }
1867
+ log(level, message, data) {
1868
+ this.onLog?.({ level, message, data });
1869
+ }
1870
+ resolvePublishReadyWaiters(reason) {
1871
+ if (this.publishReadyWaiters.length === 0 || this.publishStartedAtMs <= 0) {
1872
+ return;
1873
+ }
1874
+ const result = {
1875
+ ready: true,
1876
+ reason,
1877
+ elapsedSincePublishMs: Number((performance.now() - this.publishStartedAtMs).toFixed(1))
1878
+ };
1879
+ const waiters = this.publishReadyWaiters.splice(0);
1880
+ for (const waiter of waiters) {
1881
+ waiter(result);
1882
+ }
1883
+ }
1884
+ clearPublishReadyWaiters() {
1885
+ if (this.publishReadyBufferTimer !== null) {
1886
+ clearTimeout(this.publishReadyBufferTimer);
1887
+ this.publishReadyBufferTimer = null;
1888
+ }
1889
+ this.publishReadyWaiters = [];
1890
+ this.stablePublishWaiters = [];
1891
+ }
1892
+ notifyStablePublishWaiters() {
1893
+ if (this.stablePublishWaiters.length === 0) {
1894
+ return;
1895
+ }
1896
+ const waiters = this.stablePublishWaiters.slice();
1897
+ for (const waiter of waiters) {
1898
+ waiter();
1899
+ }
1900
+ }
1901
+ schedulePublishReadyWhenStable(sentFrameRate) {
1902
+ if (this.publishReadyWaiters.length === 0 || this.publishReadyBufferTimer !== null) {
1903
+ return;
1904
+ }
1905
+ if (sentFrameRate <= 0) {
1906
+ return;
1907
+ }
1908
+ this.publishReadyBufferTimer = setTimeout(() => {
1909
+ this.publishReadyBufferTimer = null;
1910
+ this.resolvePublishReadyWaiters("sent-frame-rate");
1911
+ }, PUBLISH_READY_BUFFER_MS);
1912
+ }
1913
+ logLocalStreamStats(stats) {
1914
+ if (!this.localVideoPublished) {
1915
+ return;
1916
+ }
1917
+ this.localStreamStatsCount += 1;
1918
+ const nowMs = performance.now();
1919
+ const sentFrameRate = stats.videoStats?.sentFrameRate ?? 0;
1920
+ if (sentFrameRate > 0 && this.firstNonZeroSentFrameRateAtMs === 0) {
1921
+ this.firstNonZeroSentFrameRateAtMs = nowMs;
1922
+ }
1923
+ this.lastObservedSentFrameRate = sentFrameRate;
1924
+ this.schedulePublishReadyWhenStable(sentFrameRate);
1925
+ this.notifyStablePublishWaiters();
1926
+ }
1927
+ debugLog(message, data) {
1928
+ if (!this.debug) {
1929
+ return;
1930
+ }
1931
+ this.log("info", `[debug] ${message}`, data);
1932
+ }
1933
+ };
1934
+ _RtcManager.SEI_GATE_FALLBACK_MS = 6e3;
1935
+ _RtcManager.SEI_MATCHED_DISPLAY_DELAY_MS = 200;
1936
+ var RtcManager = _RtcManager;
1937
+
1938
+ // src/shared/camera-stream.ts
1939
+ var CAMERA_ACCESS_ERROR_NAMES = /* @__PURE__ */ new Set([
1940
+ "NotFoundError",
1941
+ "NotAllowedError",
1942
+ "PermissionDeniedError",
1943
+ "NotReadableError",
1944
+ "OverconstrainedError",
1945
+ "ConstraintNotSatisfiedError",
1946
+ "AbortError",
1947
+ "SecurityError"
1948
+ ]);
1949
+ function getCameraEnvironmentIssue() {
1950
+ if (typeof window === "undefined") {
1951
+ return "api-unavailable";
1952
+ }
1953
+ if (!window.isSecureContext) {
1954
+ return "insecure-context";
1955
+ }
1956
+ if (typeof navigator?.mediaDevices?.getUserMedia !== "function") {
1957
+ return "api-unavailable";
1958
+ }
1959
+ return null;
1960
+ }
1961
+ function isCameraAccessError(error) {
1962
+ return classifyCameraAccessError(error) !== null;
1963
+ }
1964
+ function classifyCameraAccessError(error) {
1965
+ if (error instanceof DOMException) {
1966
+ switch (error.name) {
1967
+ case "NotFoundError":
1968
+ return "not-found";
1969
+ case "NotAllowedError":
1970
+ return "not-allowed";
1971
+ case "PermissionDeniedError":
1972
+ return "permission-denied";
1973
+ case "NotReadableError":
1974
+ return "in-use";
1975
+ case "OverconstrainedError":
1976
+ case "ConstraintNotSatisfiedError":
1977
+ return "overconstrained";
1978
+ case "TypeError":
1979
+ return "generic";
1980
+ case "AbortError":
1981
+ case "SecurityError":
1982
+ return "generic";
1983
+ default:
1984
+ if (CAMERA_ACCESS_ERROR_NAMES.has(error.name)) {
1985
+ return "generic";
1986
+ }
1987
+ return null;
1988
+ }
1989
+ }
1990
+ if (error instanceof Error) {
1991
+ if (error.message.includes("Camera is not supported")) {
1992
+ return "unsupported";
1993
+ }
1994
+ if (error.message.includes("captureStream is not supported")) {
1995
+ return "capture-unsupported";
1996
+ }
1997
+ if (error.message.includes("No active video track")) {
1998
+ return "in-use";
1999
+ }
2000
+ }
2001
+ return null;
2002
+ }
2003
+ function buildVideoConstraints(options, resolution) {
2004
+ const size = resolution === "min" ? { width: { min: options.width, ideal: options.width }, height: { min: options.height, ideal: options.height } } : { width: { ideal: options.width }, height: { ideal: options.height } };
2005
+ return {
2006
+ ...size,
2007
+ frameRate: { ideal: options.frameRate ?? 30 },
2008
+ ...options.facingMode !== void 0 ? { facingMode: options.facingMode } : {}
2009
+ };
2010
+ }
2011
+ function isOverconstrainedCameraError(error) {
2012
+ if (!(error instanceof DOMException)) {
2013
+ return false;
2014
+ }
2015
+ return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
2016
+ }
2017
+ function isEnvironmentFacing(facingMode) {
2018
+ if (facingMode === "environment") {
2019
+ return true;
2020
+ }
2021
+ if (typeof facingMode === "object" && facingMode !== null && "exact" in facingMode) {
2022
+ return facingMode.exact === "environment";
2023
+ }
2024
+ return false;
2025
+ }
2026
+ function waitTwoAnimationFrames() {
2027
+ return new Promise((resolve) => {
2028
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
2029
+ });
2030
+ }
2031
+ async function stabilizeCameraVideoTrack(track, options) {
2032
+ if (!isEnvironmentFacing(options?.facingMode)) {
2033
+ return;
2034
+ }
2035
+ const caps = track.getCapabilities?.();
2036
+ if (!caps) {
2037
+ return;
2038
+ }
2039
+ const advanced = [];
2040
+ if (Array.isArray(caps.focusMode)) {
2041
+ if (caps.focusMode.includes("single-shot")) {
2042
+ advanced.push({ focusMode: "single-shot" });
2043
+ } else if (caps.focusMode.includes("manual") && caps.focusDistance) {
2044
+ const settings = track.getSettings?.();
2045
+ const { min, max } = caps.focusDistance;
2046
+ const distance = typeof settings?.focusDistance === "number" ? settings.focusDistance : Math.min(max, Math.max(min, 1));
2047
+ advanced.push({ focusMode: "manual", focusDistance: distance });
2048
+ }
2049
+ }
2050
+ if (caps.zoom && typeof caps.zoom.min === "number" && typeof caps.zoom.max === "number") {
2051
+ const zoom = 1;
2052
+ if (zoom >= caps.zoom.min && zoom <= caps.zoom.max) {
2053
+ advanced.push({ zoom });
2054
+ }
2055
+ }
2056
+ if (advanced.length === 0) {
2057
+ return;
2058
+ }
2059
+ try {
2060
+ await track.applyConstraints({ advanced });
2061
+ } catch {
2062
+ }
2063
+ }
2064
+ async function openCameraStream(options) {
2065
+ try {
2066
+ return await navigator.mediaDevices.getUserMedia({
2067
+ audio: false,
2068
+ video: buildVideoConstraints(options, "min")
2069
+ });
2070
+ } catch (error) {
2071
+ if (!isOverconstrainedCameraError(error)) {
2072
+ throw error;
2073
+ }
2074
+ return navigator.mediaDevices.getUserMedia({
2075
+ audio: false,
2076
+ video: buildVideoConstraints(options, "ideal")
2077
+ });
2078
+ }
2079
+ }
2080
+ async function requestCameraMediaStream(options) {
2081
+ const environmentIssue = getCameraEnvironmentIssue();
2082
+ if (environmentIssue === "insecure-context") {
2083
+ throw new DOMException("Camera requires a secure context (HTTPS).", "SecurityError");
2084
+ }
2085
+ if (environmentIssue === "api-unavailable") {
2086
+ throw new Error("Camera is not supported in this browser.");
2087
+ }
2088
+ const stream = await openCameraStream(options);
2089
+ const track = stream.getVideoTracks()[0];
2090
+ if (track && isEnvironmentFacing(options.facingMode)) {
2091
+ await waitTwoAnimationFrames();
2092
+ await stabilizeCameraVideoTrack(track, { facingMode: options.facingMode });
2093
+ }
2094
+ return stream;
2095
+ }
2096
+
2097
+ // src/rtc/camera-stream-v2.ts
2098
+ async function openNativeCameraStream(options = {}) {
2099
+ const i18n = options.i18n ?? createSDKI18n();
2100
+ const stream = await requestCameraMediaStream({
2101
+ width: options.width ?? 1280,
2102
+ height: options.height ?? 720,
2103
+ frameRate: options.frameRate ?? 30,
2104
+ facingMode: options.facingMode ?? "user"
2105
+ });
2106
+ return createNativeCameraStream(stream, { i18n });
2107
+ }
2108
+ async function createNativeCameraStream(sourceStream, options = {}) {
2109
+ const i18n = options.i18n ?? createSDKI18n();
2110
+ const sourceTrack = sourceStream.getVideoTracks()[0];
2111
+ if (!sourceTrack) {
2112
+ throw new Error(i18n.t("errors.videoTrackMissing"));
2113
+ }
2114
+ const videoEl = document.createElement("video");
2115
+ videoEl.playsInline = true;
2116
+ videoEl.muted = true;
2117
+ videoEl.autoplay = true;
2118
+ videoEl.srcObject = sourceStream;
2119
+ mountCaptureVideo(videoEl);
2120
+ await new Promise((resolve, reject) => {
2121
+ const onOk = () => {
2122
+ cleanup();
2123
+ resolve();
2124
+ };
2125
+ const onErr = () => {
2126
+ cleanup();
2127
+ reject(new Error(`failed to load video metadata`));
2128
+ };
2129
+ const cleanup = () => {
2130
+ videoEl.removeEventListener("loadedmetadata", onOk);
2131
+ videoEl.removeEventListener("error", onErr);
2132
+ };
2133
+ videoEl.addEventListener("loadedmetadata", onOk, { once: true });
2134
+ videoEl.addEventListener("error", onErr, { once: true });
2135
+ });
2136
+ const playResult = videoEl.play();
2137
+ if (playResult && typeof playResult.then === "function") {
2138
+ await playResult;
2139
+ }
2140
+ const width = videoEl.videoWidth || 0;
2141
+ const height = videoEl.videoHeight || 0;
2142
+ if (!width || !height) {
2143
+ throw new Error(i18n.t("errors.videoTrackMissing"));
2144
+ }
2145
+ const destroy = () => {
2146
+ try {
2147
+ videoEl.pause();
2148
+ } catch {
2149
+ }
2150
+ try {
2151
+ videoTrack.stop();
2152
+ } catch {
2153
+ }
2154
+ try {
2155
+ sourceStream.getTracks().forEach((track) => {
2156
+ if (track.readyState !== "ended") {
2157
+ track.stop();
2158
+ }
2159
+ });
2160
+ } catch {
2161
+ }
2162
+ try {
2163
+ videoEl.srcObject = null;
2164
+ videoEl.remove();
2165
+ } catch {
2166
+ }
2167
+ };
2168
+ const videoTrack = sourceTrack;
2169
+ return {
2170
+ sourceStream,
2171
+ previewStream: sourceStream,
2172
+ // No canvas mirror, preview is the raw stream
2173
+ videoTrack,
2174
+ videoEl,
2175
+ width,
2176
+ height,
2177
+ destroy
2178
+ };
2179
+ }
2180
+
2181
+ // src/drag/map-viewport.ts
2182
+ function clamp(n, min, max) {
2183
+ return Math.min(Math.max(n, min), max);
2184
+ }
2185
+ function getFitContentRect(rect, targetSize, fitMode = "contain") {
2186
+ const [targetWidth, targetHeight] = targetSize;
2187
+ if (!targetWidth || !targetHeight || !rect.width || !rect.height) {
2188
+ return {
2189
+ left: rect.left,
2190
+ top: rect.top,
2191
+ width: rect.width,
2192
+ height: rect.height
2193
+ };
2194
+ }
2195
+ const scale = fitMode === "cover" ? Math.max(rect.width / targetWidth, rect.height / targetHeight) : Math.min(rect.width / targetWidth, rect.height / targetHeight);
2196
+ const width = targetWidth * scale;
2197
+ const height = targetHeight * scale;
2198
+ return {
2199
+ left: rect.left + (rect.width - width) / 2,
2200
+ top: rect.top + (rect.height - height) / 2,
2201
+ width,
2202
+ height
2203
+ };
2204
+ }
2205
+ function getContentRect(rect, targetSize) {
2206
+ return getFitContentRect(rect, targetSize, "contain");
2207
+ }
2208
+ function findDragMediaElement(dragSurface) {
2209
+ const root = dragSurface.parentElement ?? dragSurface;
2210
+ if (typeof root.querySelector !== "function") {
2211
+ return null;
2212
+ }
2213
+ const videoHost = root.querySelector("[data-xmax-remote-video]") ?? root;
2214
+ if (typeof videoHost.querySelector !== "function") {
2215
+ return null;
2216
+ }
2217
+ const media = videoHost.querySelector("video, canvas");
2218
+ return media ?? null;
2219
+ }
2220
+ function toContentRect(rect) {
2221
+ return {
2222
+ left: rect.left,
2223
+ top: rect.top,
2224
+ width: rect.width,
2225
+ height: rect.height
2226
+ };
2227
+ }
2228
+ function isMediaInsetInContainer(mediaRect, containerRect) {
2229
+ if (!mediaRect.width || !mediaRect.height || !containerRect.width || !containerRect.height) {
2230
+ return false;
2231
+ }
2232
+ const widthRatio = mediaRect.width / containerRect.width;
2233
+ const heightRatio = mediaRect.height / containerRect.height;
2234
+ return widthRatio < 0.98 || heightRatio < 0.98;
2235
+ }
2236
+ function resolveMappingContext(dragSurface, targetSize, fitMode = "cover") {
2237
+ const containerRect = dragSurface.getBoundingClientRect();
2238
+ const media = findDragMediaElement(dragSurface);
2239
+ if (media) {
2240
+ const mediaRect = media.getBoundingClientRect();
2241
+ if (mediaRect.width > 0 && mediaRect.height > 0 && isMediaInsetInContainer(mediaRect, containerRect)) {
2242
+ const content2 = toContentRect(mediaRect);
2243
+ return { content: content2, hitRect: content2, source: "media" };
2244
+ }
2245
+ }
2246
+ const content = getFitContentRect(containerRect, targetSize, fitMode);
2247
+ return {
2248
+ content,
2249
+ hitRect: toContentRect(containerRect),
2250
+ source: "fit"
2251
+ };
2252
+ }
2253
+ function mapViewportPointToCoordinateSpace(e, container, targetSize, options = {}) {
2254
+ const fitMode = options.fitMode ?? "cover";
2255
+ const mirrored = options.mirrored ?? false;
2256
+ const [targetWidth, targetHeight] = targetSize;
2257
+ if (!targetWidth || !targetHeight) {
2258
+ return null;
2259
+ }
2260
+ const { content, hitRect } = resolveMappingContext(container, targetSize, fitMode);
2261
+ if (!content.width || !content.height) {
2262
+ return null;
2263
+ }
2264
+ if (e.clientX < hitRect.left || e.clientY < hitRect.top || e.clientX > hitRect.left + hitRect.width || e.clientY > hitRect.top + hitRect.height) {
2265
+ return null;
2266
+ }
2267
+ const x = Math.round((e.clientX - content.left) * targetWidth / content.width);
2268
+ const y = Math.round((e.clientY - content.top) * targetHeight / content.height);
2269
+ const normalizedX = mirrored ? targetWidth - 1 - x : x;
2270
+ return [
2271
+ clamp(normalizedX, 0, targetWidth - 1),
2272
+ clamp(y, 0, targetHeight - 1)
2273
+ ];
2274
+ }
2275
+ function mapTargetPointToCanvas(point, container, targetSize, dpr, options = {}) {
2276
+ const fitMode = options.fitMode ?? "cover";
2277
+ const mirrored = options.mirrored ?? false;
2278
+ const containerRect = container.getBoundingClientRect();
2279
+ const [targetWidth, targetHeight] = targetSize;
2280
+ const { content } = resolveMappingContext(container, targetSize, fitMode);
2281
+ if (!targetWidth || !targetHeight || !content.width || !content.height) {
2282
+ return null;
2283
+ }
2284
+ const displayX = mirrored ? targetWidth - 1 - point[0] : point[0];
2285
+ const originX = (content.left - containerRect.left) * dpr;
2286
+ const originY = (content.top - containerRect.top) * dpr;
2287
+ const contentW = content.width * dpr;
2288
+ const contentH = content.height * dpr;
2289
+ return [
2290
+ originX + displayX / targetWidth * contentW,
2291
+ originY + point[1] / targetHeight * contentH
2292
+ ];
2293
+ }
2294
+
2295
+ // src/drag/drag-track-controller.ts
2296
+ function fadeCanvas(ctx) {
2297
+ ctx.save();
2298
+ ctx.globalCompositeOperation = "destination-out";
2299
+ ctx.fillStyle = "rgba(0,0,0,0.06)";
2300
+ ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
2301
+ ctx.restore();
2302
+ }
2303
+ function drawNeon(ctx, segs, container, targetSize, dpr, fitMode, mirrored) {
2304
+ if (segs.length === 0) {
2305
+ return;
2306
+ }
2307
+ const coreW = 3.6 * dpr;
2308
+ const lineW = 10 * dpr;
2309
+ const glowRadius = 28 * dpr;
2310
+ const color = { r: 20, g: 255, b: 80 };
2311
+ const rgba = (a) => `rgba(${color.r},${color.g},${color.b},${a})`;
2312
+ const strokeSegs = (opts) => {
2313
+ ctx.save();
2314
+ ctx.globalCompositeOperation = opts.composite;
2315
+ ctx.lineCap = "round";
2316
+ ctx.lineJoin = "round";
2317
+ ctx.lineWidth = opts.width;
2318
+ ctx.strokeStyle = rgba(opts.alpha);
2319
+ ctx.shadowBlur = opts.shadowBlur;
2320
+ ctx.shadowColor = rgba(opts.shadowAlpha);
2321
+ ctx.beginPath();
2322
+ for (const seg of segs) {
2323
+ const start = mapTargetPointToCanvas(seg.a, container, targetSize, dpr, { fitMode, mirrored });
2324
+ const end = mapTargetPointToCanvas(seg.b, container, targetSize, dpr, { fitMode, mirrored });
2325
+ if (!start || !end) {
2326
+ continue;
2327
+ }
2328
+ ctx.moveTo(start[0], start[1]);
2329
+ ctx.lineTo(end[0], end[1]);
2330
+ }
2331
+ ctx.stroke();
2332
+ ctx.restore();
2333
+ };
2334
+ strokeSegs({
2335
+ width: lineW,
2336
+ shadowBlur: glowRadius,
2337
+ shadowAlpha: 0.9,
2338
+ alpha: 0.45,
2339
+ composite: "lighter"
2340
+ });
2341
+ strokeSegs({
2342
+ width: lineW,
2343
+ shadowBlur: 12 * dpr,
2344
+ shadowAlpha: 0.9,
2345
+ alpha: 0.9,
2346
+ composite: "lighter"
2347
+ });
2348
+ strokeSegs({
2349
+ width: coreW,
2350
+ shadowBlur: 6 * dpr,
2351
+ shadowAlpha: 0.8,
2352
+ alpha: 1,
2353
+ composite: "source-over"
2354
+ });
2355
+ ctx.save();
2356
+ ctx.globalCompositeOperation = "lighter";
2357
+ ctx.fillStyle = rgba(1);
2358
+ ctx.shadowBlur = 10 * dpr;
2359
+ ctx.shadowColor = rgba(0.8);
2360
+ const head = segs[segs.length - 1];
2361
+ if (head) {
2362
+ const headPoint = mapTargetPointToCanvas(head.b, container, targetSize, dpr, { fitMode, mirrored });
2363
+ if (headPoint) {
2364
+ ctx.beginPath();
2365
+ ctx.arc(headPoint[0], headPoint[1], 3.2 * dpr, 0, Math.PI * 2);
2366
+ ctx.fill();
2367
+ }
2368
+ }
2369
+ ctx.restore();
2370
+ }
2371
+ var DragTrackController = class {
2372
+ constructor(container, options) {
2373
+ this.activePointers = /* @__PURE__ */ new Map();
2374
+ this.lastTargetPoints = /* @__PURE__ */ new Map();
2375
+ this.segQueue = [];
2376
+ this.sampleTimer = null;
2377
+ this.sampleInFlight = false;
2378
+ this.rafId = null;
2379
+ this.resizeObserver = null;
2380
+ this.container = container;
2381
+ this.onTracks = options.onTracks;
2382
+ this.targetSize = options.targetSize;
2383
+ this.fitMode = options.fitMode ?? "cover";
2384
+ this.mirrored = options.mirrored ?? false;
2385
+ this.enabled = options.enabled ?? true;
2386
+ this.emitEmptyTracksWhenIdle = options.emitEmptyTracksWhenIdle ?? false;
2387
+ this.canvas = document.createElement("canvas");
2388
+ this.canvas.className = "Xmax-drag-track-canvas";
2389
+ this.canvas.style.cssText = "position:absolute;inset:0;z-index:21;width:100%;height:100%;pointer-events:none;";
2390
+ container.appendChild(this.canvas);
2391
+ this.onWindowResize = () => this.resizeCanvas();
2392
+ this.onPointerDown = (e) => this.handlePointerDown(e);
2393
+ this.onPointerMove = (e) => this.handlePointerMove(e);
2394
+ this.onPointerUp = (e) => this.handlePointerUp(e);
2395
+ this.onPointerCancel = (e) => this.handlePointerCancel(e);
2396
+ this.resizeCanvas();
2397
+ this.bindResizeObserver();
2398
+ this.bindPointerEvents();
2399
+ this.applyInteractionStyle();
2400
+ if (this.enabled && this.emitEmptyTracksWhenIdle) {
2401
+ this.startSampling();
2402
+ }
2403
+ }
2404
+ setEnabled(enabled) {
2405
+ this.enabled = enabled;
2406
+ this.applyInteractionStyle();
2407
+ if (!enabled) {
2408
+ this.finishStroke({ stopIdleSampling: true });
2409
+ return;
2410
+ }
2411
+ if (this.emitEmptyTracksWhenIdle && this.activePointers.size === 0) {
2412
+ this.startSampling();
2413
+ }
2414
+ }
2415
+ setEmitEmptyTracksWhenIdle(emitEmptyTracksWhenIdle) {
2416
+ if (this.emitEmptyTracksWhenIdle === emitEmptyTracksWhenIdle) {
2417
+ return;
2418
+ }
2419
+ this.emitEmptyTracksWhenIdle = emitEmptyTracksWhenIdle;
2420
+ if (emitEmptyTracksWhenIdle && this.enabled && this.activePointers.size === 0) {
2421
+ this.startSampling();
2422
+ return;
2423
+ }
2424
+ if (!emitEmptyTracksWhenIdle && this.activePointers.size === 0) {
2425
+ this.stopSampling();
2426
+ }
2427
+ }
2428
+ setTargetSize(targetSize) {
2429
+ this.targetSize = targetSize;
2430
+ }
2431
+ setFitMode(fitMode) {
2432
+ this.fitMode = fitMode;
2433
+ }
2434
+ setMirrored(mirrored) {
2435
+ this.mirrored = mirrored;
2436
+ }
2437
+ destroy() {
2438
+ this.finishStroke({ stopIdleSampling: true });
2439
+ this.unbindPointerEvents();
2440
+ this.unbindResizeObserver();
2441
+ this.canvas.remove();
2442
+ }
2443
+ applyInteractionStyle() {
2444
+ this.container.style.pointerEvents = this.enabled ? "auto" : "none";
2445
+ this.container.style.touchAction = this.enabled ? "none" : "auto";
2446
+ }
2447
+ bindResizeObserver() {
2448
+ const ResizeObserverCtor = globalThis.ResizeObserver;
2449
+ if (ResizeObserverCtor) {
2450
+ this.resizeObserver = new ResizeObserverCtor(() => this.resizeCanvas());
2451
+ this.resizeObserver.observe(this.container);
2452
+ return;
2453
+ }
2454
+ window.addEventListener("resize", this.onWindowResize);
2455
+ }
2456
+ unbindResizeObserver() {
2457
+ this.resizeObserver?.disconnect();
2458
+ this.resizeObserver = null;
2459
+ window.removeEventListener("resize", this.onWindowResize);
2460
+ }
2461
+ bindPointerEvents() {
2462
+ this.container.addEventListener("pointerdown", this.onPointerDown);
2463
+ this.container.addEventListener("pointermove", this.onPointerMove);
2464
+ this.container.addEventListener("pointerup", this.onPointerUp);
2465
+ this.container.addEventListener("pointercancel", this.onPointerCancel);
2466
+ }
2467
+ unbindPointerEvents() {
2468
+ this.container.removeEventListener("pointerdown", this.onPointerDown);
2469
+ this.container.removeEventListener("pointermove", this.onPointerMove);
2470
+ this.container.removeEventListener("pointerup", this.onPointerUp);
2471
+ this.container.removeEventListener("pointercancel", this.onPointerCancel);
2472
+ this.activePointers.clear();
2473
+ this.lastTargetPoints.clear();
2474
+ this.sampleInFlight = false;
2475
+ }
2476
+ clearCanvas() {
2477
+ const ctx = this.canvas.getContext("2d");
2478
+ if (!ctx) {
2479
+ return;
2480
+ }
2481
+ ctx.save();
2482
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
2483
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
2484
+ ctx.restore();
2485
+ }
2486
+ resizeCanvas() {
2487
+ const rect = this.container.getBoundingClientRect();
2488
+ const dpr = window.devicePixelRatio || 1;
2489
+ const cssW = Math.max(1, Math.round(rect.width));
2490
+ const cssH = Math.max(1, Math.round(rect.height));
2491
+ const nextW = Math.max(1, Math.round(cssW * dpr));
2492
+ const nextH = Math.max(1, Math.round(cssH * dpr));
2493
+ if (this.canvas.width !== nextW || this.canvas.height !== nextH) {
2494
+ this.canvas.width = nextW;
2495
+ this.canvas.height = nextH;
2496
+ this.canvas.style.width = `${cssW}px`;
2497
+ this.canvas.style.height = `${cssH}px`;
2498
+ this.clearCanvas();
2499
+ }
2500
+ }
2501
+ toTargetPoint(e) {
2502
+ return mapViewportPointToCoordinateSpace(e, this.container, this.targetSize, {
2503
+ fitMode: this.fitMode,
2504
+ mirrored: this.mirrored
2505
+ });
2506
+ }
2507
+ stopDrawLoop() {
2508
+ if (this.rafId !== null) {
2509
+ window.cancelAnimationFrame(this.rafId);
2510
+ this.rafId = null;
2511
+ }
2512
+ }
2513
+ startDrawLoop() {
2514
+ if (this.rafId !== null) {
2515
+ return;
2516
+ }
2517
+ const tick = () => {
2518
+ const ctx = this.canvas.getContext("2d");
2519
+ if (!ctx) {
2520
+ this.rafId = null;
2521
+ return;
2522
+ }
2523
+ const dpr = window.devicePixelRatio || 1;
2524
+ fadeCanvas(ctx);
2525
+ drawNeon(ctx, this.segQueue.splice(0), this.container, this.targetSize, dpr, this.fitMode, this.mirrored);
2526
+ if (this.activePointers.size > 0) {
2527
+ this.rafId = window.requestAnimationFrame(tick);
2528
+ } else {
2529
+ this.rafId = null;
2530
+ }
2531
+ };
2532
+ this.rafId = window.requestAnimationFrame(tick);
2533
+ }
2534
+ stopSampling() {
2535
+ if (this.sampleTimer !== null) {
2536
+ window.clearInterval(this.sampleTimer);
2537
+ this.sampleTimer = null;
2538
+ }
2539
+ }
2540
+ collectCurrentTracks() {
2541
+ const tracks = [];
2542
+ for (const pointer of Array.from(this.activePointers.values())) {
2543
+ const point = this.toTargetPoint(pointer);
2544
+ if (point) {
2545
+ tracks.push(point);
2546
+ }
2547
+ }
2548
+ return tracks;
2549
+ }
2550
+ flushTracks() {
2551
+ if (this.sampleInFlight) {
2552
+ return;
2553
+ }
2554
+ const tracks = this.collectCurrentTracks();
2555
+ if (!tracks.length && !this.emitEmptyTracksWhenIdle) {
2556
+ return;
2557
+ }
2558
+ this.sampleInFlight = true;
2559
+ Promise.resolve(this.onTracks(tracks)).catch(() => {
2560
+ }).finally(() => {
2561
+ this.sampleInFlight = false;
2562
+ });
2563
+ }
2564
+ startSampling() {
2565
+ if (this.sampleTimer !== null) {
2566
+ return;
2567
+ }
2568
+ this.flushTracks();
2569
+ this.sampleTimer = window.setInterval(() => {
2570
+ this.flushTracks();
2571
+ }, 30);
2572
+ }
2573
+ finishStroke(options) {
2574
+ const stopIdleSampling = options?.stopIdleSampling ?? !this.emitEmptyTracksWhenIdle;
2575
+ if (stopIdleSampling) {
2576
+ this.stopSampling();
2577
+ }
2578
+ this.stopDrawLoop();
2579
+ this.segQueue.length = 0;
2580
+ this.clearCanvas();
2581
+ }
2582
+ handlePointerDown(e) {
2583
+ if (!this.enabled) {
2584
+ return;
2585
+ }
2586
+ try {
2587
+ this.container.setPointerCapture(e.pointerId);
2588
+ } catch {
2589
+ }
2590
+ this.resizeCanvas();
2591
+ if (this.activePointers.size === 0) {
2592
+ this.segQueue.length = 0;
2593
+ this.clearCanvas();
2594
+ }
2595
+ this.activePointers.set(e.pointerId, {
2596
+ pointerId: e.pointerId,
2597
+ clientX: e.clientX,
2598
+ clientY: e.clientY
2599
+ });
2600
+ const targetPoint = this.toTargetPoint(e);
2601
+ if (targetPoint) {
2602
+ this.lastTargetPoints.set(e.pointerId, targetPoint);
2603
+ }
2604
+ this.startDrawLoop();
2605
+ this.startSampling();
2606
+ }
2607
+ handlePointerMove(e) {
2608
+ if (!this.enabled || !this.activePointers.has(e.pointerId)) {
2609
+ return;
2610
+ }
2611
+ const nextTargetPoint = this.toTargetPoint(e);
2612
+ const last = this.lastTargetPoints.get(e.pointerId);
2613
+ if (last && nextTargetPoint) {
2614
+ this.segQueue.push({ a: last, b: nextTargetPoint });
2615
+ }
2616
+ if (nextTargetPoint) {
2617
+ this.lastTargetPoints.set(e.pointerId, nextTargetPoint);
2618
+ }
2619
+ this.activePointers.set(e.pointerId, {
2620
+ pointerId: e.pointerId,
2621
+ clientX: e.clientX,
2622
+ clientY: e.clientY
2623
+ });
2624
+ }
2625
+ handlePointerUp(e) {
2626
+ if (!this.activePointers.has(e.pointerId)) {
2627
+ return;
2628
+ }
2629
+ this.activePointers.delete(e.pointerId);
2630
+ this.lastTargetPoints.delete(e.pointerId);
2631
+ try {
2632
+ this.container.releasePointerCapture(e.pointerId);
2633
+ } catch {
2634
+ }
2635
+ if (this.activePointers.size === 0) {
2636
+ this.finishStroke();
2637
+ }
2638
+ }
2639
+ handlePointerCancel(e) {
2640
+ if (!this.activePointers.has(e.pointerId)) {
2641
+ return;
2642
+ }
2643
+ this.activePointers.delete(e.pointerId);
2644
+ this.lastTargetPoints.delete(e.pointerId);
2645
+ if (this.activePointers.size === 0) {
2646
+ this.finishStroke();
2647
+ }
2648
+ }
2649
+ };
2650
+ function createDragTrackController(container, options) {
2651
+ return new DragTrackController(container, options);
2652
+ }
2653
+
2654
+ // src/realtime/models.ts
2655
+ var DEFAULT_REALTIME_SIZE = {
2656
+ width: 1280,
2657
+ height: 960,
2658
+ fps: 24
2659
+ };
2660
+ var models = {
2661
+ realtime(name, options) {
2662
+ return {
2663
+ name,
2664
+ width: options?.width ?? DEFAULT_REALTIME_SIZE.width,
2665
+ height: options?.height ?? DEFAULT_REALTIME_SIZE.height,
2666
+ fps: options?.fps ?? DEFAULT_REALTIME_SIZE.fps
2667
+ };
2668
+ }
2669
+ };
2670
+
2671
+ export { ACTIVE_STATUS, PREVIEW_CONTAIN_ATTR, PREVIEW_COVER_ATTR, PREVIEW_OUTPUT_LAYOUT_ATTR, PREVIEW_UPLOAD_FIT_ATTR, PREVIEW_UPLOAD_LAYOUT_ATTR, PUBLISH_COLD_FRAME_TIMEOUT_MS, PUBLISH_COLD_MIN_FRAMES, PUBLISH_COLD_WARMUP_MS, PUBLISH_START_FRAME_TIMEOUT_MS, PUBLISH_START_MIN_FRAMES, PUBLISH_WARM_SKIP_MS, RtcManager, XMAX_OPEN_API_BASE_URLS, XMAX_OPEN_API_PRODUCTION_BASE_URL, XmaxOpenClient, applyUploadPreviewMediaLayout, browserToast, classifyCameraAccessError, createDragTrackController, createNativeCameraStream, createStartRtcRoomEvent, createStopRtcRoomEvent, createTaskUid, createTracksRtcRoomEvent, getCameraEnvironmentIssue, getContentRect, getFitContentRect, isCameraAccessError, mapTargetPointToCanvas, mapViewportPointToCoordinateSpace, models, notifyError, observeUploadPreviewMediaLayout, openNativeCameraStream, requestCameraMediaStream, stabilizeCameraVideoTrack };
2672
+ //# sourceMappingURL=chunk-67XSD3QJ.js.map
2673
+ //# sourceMappingURL=chunk-67XSD3QJ.js.map