@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,3897 @@
1
+ 'use strict';
2
+
3
+ var COS = require('cos-js-sdk-v5');
4
+ var VERTC = require('@volcengine/rtc');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var COS__default = /*#__PURE__*/_interopDefault(COS);
9
+ var VERTC__default = /*#__PURE__*/_interopDefault(VERTC);
10
+
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __esm = (fn, res) => function __init() {
14
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
+ };
16
+ var __export = (target, all) => {
17
+ for (var name in all)
18
+ __defProp(target, name, { get: all[name], enumerable: true });
19
+ };
20
+
21
+ // src/rtc/local-publish-diagnostics.ts
22
+ var local_publish_diagnostics_exports = {};
23
+ __export(local_publish_diagnostics_exports, {
24
+ waitForMediaStreamTrackFrames: () => waitForMediaStreamTrackFrames
25
+ });
26
+ async function waitForMediaStreamTrackFrames(track, options = {}) {
27
+ const minFrames = options.minFrames ?? 3;
28
+ const timeoutMs = options.timeoutMs ?? 3e3;
29
+ const startedAtMs = performance.now();
30
+ if (track.readyState === "ended") {
31
+ return { ready: false, frameCount: 0, elapsedMs: 0 };
32
+ }
33
+ return new Promise((resolve) => {
34
+ let frameCount = 0;
35
+ let settled = false;
36
+ let rvfcHandle = null;
37
+ let rafId = null;
38
+ const finish = (ready) => {
39
+ if (settled) {
40
+ return;
41
+ }
42
+ settled = true;
43
+ clearTimeout(timeoutTimer);
44
+ const video = videoEl;
45
+ if (rvfcHandle !== null) {
46
+ video.cancelVideoFrameCallback?.(rvfcHandle);
47
+ }
48
+ if (rafId !== null) {
49
+ window.cancelAnimationFrame(rafId);
50
+ }
51
+ try {
52
+ videoEl.pause();
53
+ videoEl.srcObject = null;
54
+ videoEl.remove();
55
+ } catch {
56
+ }
57
+ resolve({
58
+ ready,
59
+ frameCount,
60
+ elapsedMs: Number((performance.now() - startedAtMs).toFixed(1))
61
+ });
62
+ };
63
+ const onFrame = () => {
64
+ frameCount += 1;
65
+ if (frameCount >= minFrames) {
66
+ finish(true);
67
+ }
68
+ };
69
+ const videoEl = document.createElement("video");
70
+ videoEl.muted = true;
71
+ videoEl.playsInline = true;
72
+ videoEl.autoplay = true;
73
+ videoEl.srcObject = new MediaStream([track]);
74
+ videoEl.style.cssText = "position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;opacity:0;pointer-events:none";
75
+ const schedule = () => {
76
+ if (settled) {
77
+ return;
78
+ }
79
+ const video = videoEl;
80
+ if (video.requestVideoFrameCallback) {
81
+ rvfcHandle = video.requestVideoFrameCallback(() => {
82
+ onFrame();
83
+ schedule();
84
+ });
85
+ return;
86
+ }
87
+ rafId = window.requestAnimationFrame(() => {
88
+ onFrame();
89
+ schedule();
90
+ });
91
+ };
92
+ const timeoutTimer = setTimeout(() => finish(frameCount >= minFrames), timeoutMs);
93
+ void videoEl.play().then(() => {
94
+ if (!settled) {
95
+ schedule();
96
+ }
97
+ }).catch(() => finish(false));
98
+ });
99
+ }
100
+ var init_local_publish_diagnostics = __esm({
101
+ "src/rtc/local-publish-diagnostics.ts"() {
102
+ }
103
+ });
104
+
105
+ // src/i18n/en-US.ts
106
+ var enUS = {
107
+ "errors.apiKeyEmpty": "apiKey cannot be empty",
108
+ "errors.baseUrlEmpty": "baseUrl is required \u2014 pass XMAX_OPEN_API_PRODUCTION_BASE_URL for production",
109
+ "errors.modelEmpty": "model cannot be empty",
110
+ "errors.invalidJson": "Response is not valid JSON",
111
+ "errors.invalidPayload": "Response payload is invalid",
112
+ "errors.requestFailed": "Request failed",
113
+ "errors.requestRetry": "Request failed. Please try again later.",
114
+ "errors.requestTimeout": "Request timeout",
115
+ "errors.invalidFile": "file must be a File",
116
+ "errors.invalidFileType": "invalid file type: {type}",
117
+ "errors.sessionJoinInfoIncomplete": "session does not contain complete RTC join info",
118
+ "errors.sessionUidMissing": "session does not contain sessionUid",
119
+ "errors.refImageMissing": "missing reference image",
120
+ "errors.captureStreamUnsupported": "captureStream is not supported in this browser",
121
+ "errors.videoTrackMissing": "failed to create video track from file",
122
+ "errors.imageTrackMissing": "failed to create video track from image file",
123
+ "errors.imageLoadFailed": "failed to load image \u2014 try again after selecting the file",
124
+ "errors.invalidMediaFileType": "unsupported media file type: {type}",
125
+ "errors.videoPlaybackFailed": "failed to start video playback \u2014 try again after selecting the file",
126
+ "errors.rtcNotJoined": "Please join the RTC room first",
127
+ "errors.rtcNotJoinedForMessaging": "Please join the RTC room before sending events",
128
+ "rtc.alreadyInRoom": "RTC is already in the target room. Skip duplicate join.",
129
+ "rtc.joiningRoom": "Joining RTC room",
130
+ "rtc.joinSuccess": "RTC joinRoom succeeded",
131
+ "rtc.roomUsers": "Current room users: {users}",
132
+ "rtc.localVideoAlreadyPublished": "Local video is already published",
133
+ "rtc.localVideoPublished": "Local video capture started and stream published",
134
+ "rtc.externalVideoPublished": "External video stream published",
135
+ "rtc.localVideoStopped": "Local video publishing stopped",
136
+ "rtc.leaveRoomError": "Error occurred during RTC leaveRoom",
137
+ "rtc.engineDestroyed": "RTC engine destroyed",
138
+ "rtc.roomEventSent": "RTC event sent",
139
+ "rtc.remotePlayerRefreshed": "Remote video player refreshed: {userId}",
140
+ "rtc.remotePlayerRefreshFailed": "Failed to refresh remote video player",
141
+ "rtc.userJoined": "User joined RTC room: {userId}",
142
+ "rtc.userLeft": "User left RTC room: {userId}",
143
+ "rtc.connectionStateChanged": "RTC connection state changed",
144
+ "rtc.userPublished": "User published stream: {userId}",
145
+ "rtc.userUnpublished": "User unpublished stream: {userId}",
146
+ "rtc.seiReceived": "Remote SEI received: {userId}",
147
+ "rtc.error": "RTC error: {code}",
148
+ "rtc.captureSizeSet": "Local video capture size set: {width} x {height}",
149
+ "rtc.encoderPreferenceSet": "Video encoder preference set to H.264",
150
+ "rtc.encoderPreferenceFailed": "Failed to set video encoder preference",
151
+ "rtc.remoteWaitSei": "Remote video is waiting for SEI gate: {userId}",
152
+ "rtc.remoteContainerNotReady": "Remote video container is not ready. Skip binding.",
153
+ "rtc.remotePlayerBound": "Remote video player bound: {userId}",
154
+ "rtc.remoteOutputStalled": "Remote output stalled for 15s; attempting to recover generation",
155
+ "rtc.sendSeiFailed": "Failed to send SEI. The browser or encoder config may not support it.",
156
+ "rtc.seiUnsupported": "H.264 is not supported. SEI gate will be disabled and output will not be blocked.",
157
+ "rtc.seiGateFallback": "SEI gate timed out \u2014 showing remote output",
158
+ "rtc.getCodecsFailed": "Failed to get supported codecs. SEI gate will be disabled and output will not be blocked."
159
+ };
160
+
161
+ // src/i18n/zh-CN.ts
162
+ var zhCN = {
163
+ "errors.apiKeyEmpty": "apiKey \u4E0D\u80FD\u4E3A\u7A7A",
164
+ "errors.baseUrlEmpty": "baseUrl \u4E3A\u5FC5\u586B\u9879 \u2014 \u751F\u4EA7\u73AF\u5883\u8BF7\u4F20\u5165 XMAX_OPEN_API_PRODUCTION_BASE_URL",
165
+ "errors.modelEmpty": "model \u4E0D\u80FD\u4E3A\u7A7A",
166
+ "errors.invalidJson": "\u54CD\u5E94\u4E0D\u662F\u6709\u6548\u7684 JSON",
167
+ "errors.invalidPayload": "\u54CD\u5E94\u6570\u636E\u683C\u5F0F\u4E0D\u6B63\u786E",
168
+ "errors.requestFailed": "\u8BF7\u6C42\u5931\u8D25",
169
+ "errors.requestRetry": "\u8BF7\u6C42\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
170
+ "errors.requestTimeout": "\u8BF7\u6C42\u8D85\u65F6",
171
+ "errors.invalidFile": "file \u5FC5\u987B\u662F File \u7C7B\u578B",
172
+ "errors.invalidFileType": "\u6587\u4EF6\u7C7B\u578B\u4E0D\u6B63\u786E\uFF1A{type}",
173
+ "errors.sessionJoinInfoIncomplete": "session \u7F3A\u5C11\u5B8C\u6574\u7684 RTC \u8FDB\u623F\u4FE1\u606F",
174
+ "errors.sessionUidMissing": "session \u7F3A\u5C11 sessionUid",
175
+ "errors.refImageMissing": "\u7F3A\u5C11\u53C2\u8003\u56FE",
176
+ "errors.captureStreamUnsupported": "\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301 captureStream",
177
+ "errors.videoTrackMissing": "\u672A\u80FD\u4ECE\u89C6\u9891\u6587\u4EF6\u521B\u5EFA video track",
178
+ "errors.imageTrackMissing": "\u672A\u80FD\u4ECE\u56FE\u7247\u6587\u4EF6\u521B\u5EFA video track",
179
+ "errors.imageLoadFailed": "\u56FE\u7247\u65E0\u6CD5\u52A0\u8F7D\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9\u6587\u4EF6\u540E\u518D\u8BD5",
180
+ "errors.invalidMediaFileType": "\u4E0D\u652F\u6301\u7684\u5A92\u4F53\u6587\u4EF6\u7C7B\u578B\uFF1A{type}",
181
+ "errors.videoPlaybackFailed": "\u89C6\u9891\u65E0\u6CD5\u5F00\u59CB\u64AD\u653E\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9\u6587\u4EF6\u540E\u518D\u8BD5",
182
+ "errors.rtcNotJoined": "\u8BF7\u5148\u6210\u529F\u52A0\u5165 RTC \u623F\u95F4",
183
+ "errors.rtcNotJoinedForMessaging": "\u8BF7\u5148\u6210\u529F\u52A0\u5165 RTC \u623F\u95F4\u540E\u518D\u53D1\u9001\u4E8B\u4EF6",
184
+ "rtc.alreadyInRoom": "RTC \u5DF2\u5728\u76EE\u6807\u623F\u95F4\u4E2D\uFF0C\u8DF3\u8FC7\u91CD\u590D\u8FDB\u623F",
185
+ "rtc.joiningRoom": "\u51C6\u5907\u52A0\u5165 RTC \u623F\u95F4",
186
+ "rtc.joinSuccess": "RTC joinRoom \u6210\u529F",
187
+ "rtc.roomUsers": "\u5F53\u524D\u623F\u95F4\u7528\u6237: {users}",
188
+ "rtc.localVideoAlreadyPublished": "\u672C\u5730\u89C6\u9891\u5DF2\u5904\u4E8E\u53D1\u5E03\u72B6\u6001",
189
+ "rtc.localVideoPublished": "\u5DF2\u5F00\u542F\u672C\u5730\u89C6\u9891\u91C7\u96C6\u5E76\u53D1\u5E03\u89C6\u9891\u6D41",
190
+ "rtc.externalVideoPublished": "\u5DF2\u53D1\u5E03\u81EA\u5B9A\u4E49\u89C6\u9891\u6D41",
191
+ "rtc.localVideoStopped": "\u5DF2\u505C\u6B62\u672C\u5730\u89C6\u9891\u53D1\u5E03",
192
+ "rtc.leaveRoomError": "RTC leaveRoom \u8FC7\u7A0B\u4E2D\u51FA\u73B0\u5F02\u5E38",
193
+ "rtc.engineDestroyed": "RTC \u5F15\u64CE\u5DF2\u9500\u6BC1",
194
+ "rtc.roomEventSent": "\u5DF2\u53D1\u9001 RTC \u4E8B\u4EF6",
195
+ "rtc.remotePlayerRefreshed": "\u5DF2\u5237\u65B0\u8FDC\u7AEF\u89C6\u9891\u64AD\u653E\u5668: {userId}",
196
+ "rtc.remotePlayerRefreshFailed": "\u5237\u65B0\u8FDC\u7AEF\u89C6\u9891\u64AD\u653E\u5668\u5931\u8D25",
197
+ "rtc.userJoined": "\u7528\u6237\u52A0\u5165 RTC \u623F\u95F4: {userId}",
198
+ "rtc.userLeft": "\u7528\u6237\u79BB\u5F00 RTC \u623F\u95F4: {userId}",
199
+ "rtc.connectionStateChanged": "RTC \u8FDE\u63A5\u72B6\u6001\u53D8\u5316",
200
+ "rtc.userPublished": "\u7528\u6237\u53D1\u5E03\u6D41: {userId}",
201
+ "rtc.userUnpublished": "\u7528\u6237\u53D6\u6D88\u53D1\u5E03\u6D41: {userId}",
202
+ "rtc.seiReceived": "\u6536\u5230\u8FDC\u7AEF SEI: {userId}",
203
+ "rtc.error": "RTC \u9519\u8BEF: {code}",
204
+ "rtc.captureSizeSet": "\u5DF2\u8BBE\u7F6E\u672C\u5730\u89C6\u9891\u91C7\u96C6\u5C3A\u5BF8: {width} x {height}",
205
+ "rtc.encoderPreferenceSet": "\u5DF2\u8BBE\u7F6E\u89C6\u9891\u7F16\u7801\u504F\u597D\u4E3A H.264",
206
+ "rtc.encoderPreferenceFailed": "\u8BBE\u7F6E\u89C6\u9891\u7F16\u7801\u504F\u597D\u5931\u8D25",
207
+ "rtc.remoteWaitSei": "\u8FDC\u7AEF\u89C6\u9891\u7B49\u5F85 SEI \u653E\u884C: {userId}",
208
+ "rtc.remoteContainerNotReady": "\u8FDC\u7AEF\u89C6\u9891\u5BB9\u5668\u672A\u5C31\u7EEA\uFF0C\u8DF3\u8FC7\u7ED1\u5B9A",
209
+ "rtc.remotePlayerBound": "\u5DF2\u7ED1\u5B9A\u8FDC\u7AEF\u89C6\u9891\u64AD\u653E\u5668: {userId}",
210
+ "rtc.remoteOutputStalled": "\u8FDC\u7AEF\u8F93\u51FA\u8D85\u8FC7 15 \u79D2\u65E0\u65B0\u5E27\uFF0C\u5C1D\u8BD5\u6062\u590D\u751F\u6210\u4EFB\u52A1",
211
+ "rtc.sendSeiFailed": "\u53D1\u9001 SEI \u5931\u8D25\uFF0C\u5F53\u524D\u6D4F\u89C8\u5668\u6216\u7F16\u7801\u914D\u7F6E\u53EF\u80FD\u4E0D\u652F\u6301\u8BE5\u80FD\u529B",
212
+ "rtc.seiUnsupported": "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 H.264\uFF0CSEI gate \u5C06\u4E0D\u4F1A\u751F\u6548\uFF0Coutput \u5C06\u4E0D\u518D\u88AB\u62E6\u622A",
213
+ "rtc.seiGateFallback": "SEI \u95E8\u63A7\u8D85\u65F6\uFF0C\u5DF2\u663E\u793A\u8FDC\u7AEF\u8F93\u51FA",
214
+ "rtc.getCodecsFailed": "\u83B7\u53D6\u6D4F\u89C8\u5668\u7F16\u89E3\u7801\u652F\u6301\u5931\u8D25\uFF0CSEI gate \u5C06\u4E0D\u4F1A\u751F\u6548\uFF0Coutput \u5C06\u4E0D\u518D\u88AB\u62E6\u622A"
215
+ };
216
+
217
+ // src/i18n/api-error-codes.ts
218
+ var SYSTEM_OVERLOADED = {
219
+ "zh-CN": "\u5F53\u524D\u4F7F\u7528\u4EBA\u6570\u8FC7\u591A\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5",
220
+ "en-US": "The system is currently overloaded. Please retry after some time."
221
+ };
222
+ var API_ERROR_MESSAGES = {
223
+ "46001": { "zh-CN": "X-Api-Key \u4E0D\u80FD\u4E3A\u7A7A", "en-US": "X-Api-Key cannot be empty" },
224
+ "46002": { "zh-CN": "\u65E0\u6548\u7684\u7528\u6237 API Key", "en-US": "Invalid user API Key" },
225
+ "46003": { "zh-CN": "model \u4E0D\u80FD\u4E3A\u7A7A", "en-US": "model cannot be empty" },
226
+ "46004": { "zh-CN": "sessionUid \u4E0D\u80FD\u4E3A\u7A7A", "en-US": "sessionUid cannot be empty" },
227
+ "46005": { "zh-CN": "\u6A21\u578B\u7C7B\u578B\u4E0D\u5B58\u5728", "en-US": "Model type does not exist" },
228
+ "46006": SYSTEM_OVERLOADED,
229
+ "46007": SYSTEM_OVERLOADED,
230
+ "46008": SYSTEM_OVERLOADED,
231
+ "46009": SYSTEM_OVERLOADED,
232
+ "46010": SYSTEM_OVERLOADED,
233
+ "46012": { "zh-CN": "\u4F1A\u8BDD\u4E0D\u5B58\u5728", "en-US": "Session does not exist" },
234
+ "46013": { "zh-CN": "\u4F1A\u8BDD\u4E0D\u5C5E\u4E8E\u5F53\u524D\u7528\u6237", "en-US": "Session does not belong to the current user" },
235
+ "46014": { "zh-CN": "\u4F1A\u8BDD\u5DF2\u5173\u95ED", "en-US": "Session has been closed" },
236
+ "46015": { "zh-CN": "\u4F1A\u8BDD\u5DF2\u8D85\u65F6", "en-US": "Session has timed out" },
237
+ "46018": { "zh-CN": "\u4E34\u65F6 API Key \u5DF2\u8FC7\u671F", "en-US": "Temporary API Key has expired" },
238
+ "46019": {
239
+ "zh-CN": "\u4E34\u65F6 API Key \u5DF2\u8FBE\u5230\u79EF\u5206\u4E0A\u9650",
240
+ "en-US": "Temporary API Key has reached its points limit"
241
+ },
242
+ "46021": {
243
+ "zh-CN": "\u4E34\u65F6 API Key \u4E0D\u5141\u8BB8\u518D\u521B\u5EFA\u4E34\u65F6 API Key",
244
+ "en-US": "A temporary API Key cannot create another temporary API Key"
245
+ }
246
+ };
247
+ function resolveApiErrorMessage(code, locale) {
248
+ if (code === null || code === void 0) return void 0;
249
+ const entry = API_ERROR_MESSAGES[String(code)];
250
+ if (!entry) return void 0;
251
+ return entry[locale] ?? entry["en-US"];
252
+ }
253
+
254
+ // src/i18n/index.ts
255
+ function formatTemplate(template, params) {
256
+ if (!params) return template;
257
+ return template.replace(/\{(\w+)\}/g, (_, rawKey) => {
258
+ const value = params[rawKey];
259
+ return value === null || value === void 0 ? "" : String(value);
260
+ });
261
+ }
262
+ function getDefaultMessages(locale) {
263
+ return locale === "zh-CN" ? zhCN : enUS;
264
+ }
265
+ function createSDKI18n(input) {
266
+ const locale = input?.locale ?? "en-US";
267
+ if (typeof input?.t === "function") {
268
+ return { locale, t: input.t };
269
+ }
270
+ const base = getDefaultMessages(locale);
271
+ const merged = { ...base, ...input?.messages ?? {} };
272
+ return {
273
+ locale,
274
+ t: (key, params) => {
275
+ const template = merged[key] ?? key;
276
+ return formatTemplate(template, params);
277
+ }
278
+ };
279
+ }
280
+
281
+ // src/shared/browser-toast.ts
282
+ var TOAST_ROOT_ID = "Xmax-sdk-toast-root";
283
+ var TOAST_DURATION_MS = 3e3;
284
+ var toastSeed = 0;
285
+ var timeoutMap = /* @__PURE__ */ new Map();
286
+ var elementMap = /* @__PURE__ */ new Map();
287
+ function ensureToastRoot() {
288
+ if (typeof document === "undefined") {
289
+ return null;
290
+ }
291
+ let root = document.getElementById(TOAST_ROOT_ID);
292
+ if (root) {
293
+ return root;
294
+ }
295
+ root = document.createElement("div");
296
+ root.id = TOAST_ROOT_ID;
297
+ root.setAttribute("aria-live", "polite");
298
+ Object.assign(root.style, {
299
+ position: "fixed",
300
+ left: "50%",
301
+ top: "24px",
302
+ transform: "translateX(-50%)",
303
+ zIndex: "3000",
304
+ display: "flex",
305
+ flexDirection: "column",
306
+ alignItems: "center",
307
+ gap: "12px",
308
+ pointerEvents: "none"
309
+ });
310
+ document.body.appendChild(root);
311
+ return root;
312
+ }
313
+ function createIcon(type) {
314
+ const icon = document.createElement("span");
315
+ icon.setAttribute("aria-hidden", "true");
316
+ 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>';
317
+ Object.assign(icon.style, {
318
+ display: "inline-flex",
319
+ width: "12px",
320
+ height: "12px",
321
+ flexShrink: "0"
322
+ });
323
+ return icon;
324
+ }
325
+ function clearToastTimer(id) {
326
+ const timer = timeoutMap.get(id);
327
+ if (timer) {
328
+ window.clearTimeout(timer);
329
+ timeoutMap.delete(id);
330
+ }
331
+ }
332
+ function destroyToast(id) {
333
+ if (!id) {
334
+ timeoutMap.forEach((timer) => window.clearTimeout(timer));
335
+ timeoutMap.clear();
336
+ elementMap.forEach((element2) => element2.remove());
337
+ elementMap.clear();
338
+ const root2 = typeof document !== "undefined" ? document.getElementById(TOAST_ROOT_ID) : null;
339
+ root2?.remove();
340
+ return;
341
+ }
342
+ clearToastTimer(id);
343
+ const element = elementMap.get(id);
344
+ element?.remove();
345
+ elementMap.delete(id);
346
+ const root = typeof document !== "undefined" ? document.getElementById(TOAST_ROOT_ID) : null;
347
+ if (root && !root.childElementCount) {
348
+ root.remove();
349
+ }
350
+ }
351
+ function showToast(type, message, options) {
352
+ const root = ensureToastRoot();
353
+ if (!root) {
354
+ return "";
355
+ }
356
+ const id = options?.key || `toast-${Date.now()}-${++toastSeed}`;
357
+ clearToastTimer(id);
358
+ elementMap.get(id)?.remove();
359
+ const isMobile = typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches;
360
+ const textSize = isMobile ? "12px" : "14px";
361
+ const toast = document.createElement("div");
362
+ toast.setAttribute("role", "status");
363
+ Object.assign(toast.style, {
364
+ pointerEvents: "auto",
365
+ display: "flex",
366
+ alignItems: "center",
367
+ gap: "12px",
368
+ minWidth: "240px",
369
+ padding: "16px",
370
+ borderRadius: "12px",
371
+ border: "1px solid rgba(255, 255, 255, 0.20)",
372
+ background: "rgba(0, 0, 0, 0.80)",
373
+ boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
374
+ fontFamily: "Rubik, ui-sans-serif, system-ui, sans-serif",
375
+ fontSize: textSize,
376
+ fontWeight: "400",
377
+ lineHeight: "14px"
378
+ });
379
+ const content = document.createElement("span");
380
+ content.textContent = message;
381
+ Object.assign(content.style, {
382
+ paddingRight: "0px",
383
+ textAlign: "center",
384
+ color: "#ffffff",
385
+ fontFamily: "Rubik, ui-sans-serif, system-ui, sans-serif",
386
+ fontSize: textSize,
387
+ fontWeight: "400",
388
+ lineHeight: "14px"
389
+ });
390
+ toast.appendChild(createIcon(type));
391
+ toast.appendChild(content);
392
+ root.appendChild(toast);
393
+ elementMap.set(id, toast);
394
+ const timer = window.setTimeout(() => {
395
+ destroyToast(id);
396
+ }, options?.duration ?? TOAST_DURATION_MS);
397
+ timeoutMap.set(id, timer);
398
+ return id;
399
+ }
400
+ var browserToast = {
401
+ success(message, options) {
402
+ return showToast("success", message, options);
403
+ },
404
+ error(message, options) {
405
+ return showToast("error", message, options);
406
+ },
407
+ destroy(id) {
408
+ destroyToast(id);
409
+ }
410
+ };
411
+
412
+ // src/shared/error-utils.ts
413
+ function notifyError(error, notifier, fallbackMessage) {
414
+ const resolved = error instanceof Error ? error : Object.assign(new Error(fallbackMessage), { cause: error });
415
+ if (!resolved.__XmaxNotified) {
416
+ resolved.__XmaxNotified = true;
417
+ const message = resolved.message || fallbackMessage;
418
+ if (notifier) {
419
+ notifier(message, error);
420
+ } else if (typeof window !== "undefined" && typeof document !== "undefined") {
421
+ browserToast.error(message);
422
+ }
423
+ }
424
+ return resolved;
425
+ }
426
+
427
+ // src/sdk/client.ts
428
+ var XMAX_OPEN_API_BASE_URLS = {
429
+ // 国内 — @xmaxai/sdk
430
+ cn: "https://cloud.xmax.22duck.cn/open/api/v1",
431
+ // 海外 — @xmaxai/sdk-global
432
+ global: "https://api.xmax.cloud/open/api/v1"
433
+ };
434
+ var XMAX_SDK_BUILD_REGION = "cn";
435
+ var XMAX_OPEN_API_PRODUCTION_BASE_URL = XMAX_OPEN_API_BASE_URLS[XMAX_SDK_BUILD_REGION];
436
+ var ACTIVE_STATUS = "ACTIVE";
437
+ var STS_TTL_MS = 30 * 60 * 1e3;
438
+ var STS_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
439
+ function sanitizeFileName(fileName) {
440
+ return fileName.trim().replace(/[^\w.\-一-龥]/g, "_").replace(/_+/g, "_");
441
+ }
442
+ function buildCosFileUrl(endpoint, bucket, key) {
443
+ const trimmed = endpoint.trim();
444
+ const url = new URL(
445
+ /^https?:\/\//i.test(trimmed) ? trimmed.endsWith("/") ? trimmed : `${trimmed}/` : `https://${trimmed.replace(/^\/+/, "")}${trimmed.endsWith("/") ? "" : "/"}`
446
+ );
447
+ if (url.hostname.startsWith("cos.") && !url.hostname.startsWith(`${bucket}.`)) {
448
+ url.hostname = `${bucket}.${url.hostname}`;
449
+ }
450
+ return new URL(key.replace(/^\/+/, ""), url.toString()).toString();
451
+ }
452
+ function resolveCosUploadUrl(raw, sts, key) {
453
+ const location = raw?.Location?.trim();
454
+ if (location) {
455
+ return /^https?:\/\//i.test(location) ? location : `https://${location.replace(/^\/+/, "")}`;
456
+ }
457
+ const endpoint = sts.endpoint?.trim();
458
+ if (endpoint) {
459
+ return buildCosFileUrl(endpoint, sts.bucket, key);
460
+ }
461
+ return `https://${sts.bucket}.cos.${sts.region}.myqcloud.com/${key.replace(/^\/+/, "")}`;
462
+ }
463
+ var XmaxOpenClient = class {
464
+ constructor(options) {
465
+ this.stsCache = null;
466
+ this.heartbeatIntervalId = null;
467
+ this.suppressErrorNotification = false;
468
+ this.teardownAbortController = null;
469
+ this.pendingRequestControllers = /* @__PURE__ */ new Set();
470
+ const i18n = options.i18n ?? createSDKI18n({ locale: options.locale });
471
+ if (!options.apiKey?.trim()) {
472
+ throw new Error(i18n.t("errors.apiKeyEmpty"));
473
+ }
474
+ if (!options.baseUrl?.trim()) {
475
+ throw new Error(i18n.t("errors.baseUrlEmpty"));
476
+ }
477
+ this.apiKey = options.apiKey;
478
+ this.authToken = options.authToken;
479
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
480
+ this.timeoutMs = options.timeoutMs ?? 1e4;
481
+ this.i18n = i18n;
482
+ this.onError = options.onError;
483
+ }
484
+ throwError(message, cause) {
485
+ const error = new Error(message);
486
+ if (cause !== void 0) {
487
+ error.cause = cause;
488
+ }
489
+ if (this.suppressErrorNotification) {
490
+ throw error;
491
+ }
492
+ throw notifyError(error, this.onError, message);
493
+ }
494
+ /** Stop heartbeat, abort in-flight API requests, and suppress error toasts during teardown. */
495
+ beginTeardown() {
496
+ this.suppressErrorNotification = true;
497
+ this.stopHeartbeat();
498
+ if (this.teardownAbortController) {
499
+ this.teardownAbortController.abort();
500
+ }
501
+ this.teardownAbortController = new AbortController();
502
+ this.pendingRequestControllers.forEach((controller) => {
503
+ controller.abort();
504
+ });
505
+ this.pendingRequestControllers.clear();
506
+ }
507
+ endTeardown() {
508
+ this.suppressErrorNotification = false;
509
+ this.teardownAbortController = null;
510
+ }
511
+ isAbortError(error) {
512
+ return error instanceof DOMException && error.name === "AbortError";
513
+ }
514
+ async request(method, path, body) {
515
+ const controller = new AbortController();
516
+ this.pendingRequestControllers.add(controller);
517
+ const onAbort = () => controller.abort();
518
+ this.teardownAbortController?.signal.addEventListener("abort", onAbort);
519
+ const timeoutId = window.setTimeout(() => controller.abort(), this.timeoutMs);
520
+ const headers = {
521
+ "Content-Type": "application/json",
522
+ Accept: "application/json",
523
+ "X-Api-Key": this.apiKey
524
+ };
525
+ if (this.authToken) {
526
+ headers["Authorization"] = `Bearer ${this.authToken}`;
527
+ }
528
+ try {
529
+ const response = await fetch(`${this.baseUrl}${path}`, {
530
+ method,
531
+ headers,
532
+ body: body === void 0 ? void 0 : JSON.stringify(body),
533
+ signal: controller.signal
534
+ });
535
+ const text = await response.text();
536
+ let payload;
537
+ try {
538
+ payload = text ? JSON.parse(text) : null;
539
+ } catch (parseError) {
540
+ throw this.throwError(this.i18n.t("errors.invalidJson"), parseError);
541
+ }
542
+ if (typeof payload !== "object" || payload === null) {
543
+ throw this.throwError(this.i18n.t("errors.invalidPayload"));
544
+ }
545
+ const p = payload;
546
+ if (response.ok && p.success === true) {
547
+ return p.data;
548
+ }
549
+ const mappedMessage = resolveApiErrorMessage(p.code, this.i18n.locale);
550
+ const errorMessage = mappedMessage ?? this.i18n.t("errors.requestRetry");
551
+ throw new Error(errorMessage);
552
+ } catch (error) {
553
+ if (this.isAbortError(error)) {
554
+ if (this.suppressErrorNotification) {
555
+ throw new Error(this.i18n.t("errors.requestTimeout"));
556
+ }
557
+ throw this.throwError(this.i18n.t("errors.requestTimeout"), error);
558
+ } else if (error instanceof Error) {
559
+ if (this.suppressErrorNotification) {
560
+ throw error;
561
+ }
562
+ throw notifyError(error, this.onError, error.message || this.i18n.t("errors.requestFailed"));
563
+ } else {
564
+ throw this.throwError(this.i18n.t("errors.requestFailed"), error);
565
+ }
566
+ } finally {
567
+ this.teardownAbortController?.signal.removeEventListener("abort", onAbort);
568
+ this.pendingRequestControllers.delete(controller);
569
+ window.clearTimeout(timeoutId);
570
+ }
571
+ }
572
+ async createSession(model) {
573
+ if (!model.trim()) {
574
+ this.throwError(this.i18n.t("errors.modelEmpty"));
575
+ }
576
+ return this.request("POST", "/session", { model });
577
+ }
578
+ async getSession(sessionUid) {
579
+ return this.request("GET", `/session/${sessionUid}`);
580
+ }
581
+ async heartbeatSession(sessionUid) {
582
+ return this.request("PUT", `/session/${sessionUid}/heartbeat`);
583
+ }
584
+ async closeSession(sessionUid) {
585
+ const previousSuppress = this.suppressErrorNotification;
586
+ this.suppressErrorNotification = true;
587
+ try {
588
+ return await this.request("DELETE", `/session/${sessionUid}`);
589
+ } catch {
590
+ return null;
591
+ } finally {
592
+ this.suppressErrorNotification = previousSuppress;
593
+ }
594
+ }
595
+ async getCosSts(forceRefresh = false) {
596
+ const now = Date.now();
597
+ if (!forceRefresh && this.stsCache && now - this.stsCache.fetchedAt < STS_TTL_MS - STS_REFRESH_BUFFER_MS) {
598
+ return this.stsCache.value;
599
+ }
600
+ const sts = await this.request("GET", "/cos/sts");
601
+ this.stsCache = {
602
+ value: sts,
603
+ fetchedAt: now
604
+ };
605
+ return sts;
606
+ }
607
+ async uploadObject(file, expectedTypePrefix) {
608
+ if (!(file instanceof File)) {
609
+ this.throwError(this.i18n.t("errors.invalidFile"));
610
+ }
611
+ if (!file.type.startsWith(expectedTypePrefix)) {
612
+ this.throwError(this.i18n.t("errors.invalidFileType", { type: file.type }));
613
+ }
614
+ const sts = await this.getCosSts();
615
+ const safeFileName = sanitizeFileName(file.name) || "image";
616
+ const key = `${sts.prefix}${Date.now()}_${safeFileName}`;
617
+ const cos = new COS__default.default({
618
+ SecretId: sts.credentials.accessKeyId,
619
+ SecretKey: sts.credentials.secretAccessKey,
620
+ SecurityToken: sts.credentials.sessionToken
621
+ });
622
+ const raw = await new Promise((resolve, reject) => {
623
+ cos.putObject(
624
+ {
625
+ Bucket: sts.bucket,
626
+ Region: sts.region,
627
+ Key: key,
628
+ Body: file,
629
+ ContentType: file.type
630
+ },
631
+ (error, data) => {
632
+ if (error) {
633
+ reject(error);
634
+ return;
635
+ }
636
+ resolve(data);
637
+ }
638
+ );
639
+ });
640
+ return {
641
+ key,
642
+ url: resolveCosUploadUrl(raw, sts, key),
643
+ raw,
644
+ sts
645
+ };
646
+ }
647
+ async uploadImage(file) {
648
+ return this.uploadObject(file, "image/");
649
+ }
650
+ async uploadVideo(file) {
651
+ return this.uploadObject(file, "video/");
652
+ }
653
+ startHeartbeat(sessionUid, intervalMs = 1e4, onHeartbeat, onError) {
654
+ this.stopHeartbeat();
655
+ const heartbeat = async () => {
656
+ try {
657
+ const session = await this.heartbeatSession(sessionUid);
658
+ onHeartbeat?.(session);
659
+ if (session.status !== ACTIVE_STATUS) {
660
+ this.stopHeartbeat();
661
+ }
662
+ } catch (error) {
663
+ onError?.(error);
664
+ this.stopHeartbeat();
665
+ }
666
+ };
667
+ heartbeat();
668
+ this.heartbeatIntervalId = window.setInterval(heartbeat, intervalMs);
669
+ }
670
+ stopHeartbeat() {
671
+ if (this.heartbeatIntervalId !== null) {
672
+ clearInterval(this.heartbeatIntervalId);
673
+ this.heartbeatIntervalId = null;
674
+ }
675
+ }
676
+ getRtcJoinInfo(session) {
677
+ const roomId = session.modelExtra?.room_id?.trim();
678
+ const appId = session.modelExtra?.rtc_app_id?.trim();
679
+ const token = session.modelExtra?.room_token?.trim();
680
+ const userId = session.modelExtra?.user_id?.trim() || session.userUid?.trim() || "";
681
+ if (!roomId || !appId || !token || !userId) {
682
+ this.throwError(this.i18n.t("errors.sessionJoinInfoIncomplete"));
683
+ }
684
+ return {
685
+ appId,
686
+ roomId,
687
+ userId,
688
+ token,
689
+ botName: session.modelExtra?.bot_name
690
+ };
691
+ }
692
+ };
693
+
694
+ // src/sdk/rtc-events.ts
695
+ function createTaskUid() {
696
+ return `task-${Date.now()}`;
697
+ }
698
+ function createStartRtcRoomEvent(input) {
699
+ const resolvedPrompt = input.prompt?.trim() ?? "";
700
+ const refImagePath = input.refImagePath?.trim() || input.refImage?.trim() || void 0;
701
+ const useRefImage = !!refImagePath;
702
+ return {
703
+ event: "start",
704
+ user_id: input.userId?.trim() || void 0,
705
+ uid: input.uid?.trim() || void 0,
706
+ session_uid: input.sessionUid?.trim() || void 0,
707
+ params: {
708
+ model: input.model.trim(),
709
+ size: input.size,
710
+ prompt: resolvedPrompt,
711
+ ref_image_path: useRefImage ? refImagePath : void 0,
712
+ ref_image: useRefImage ? refImagePath : void 0,
713
+ static_generate: input.staticGenerate === void 0 ? void 0 : input.staticGenerate,
714
+ ...input.extraParams ?? {}
715
+ }
716
+ };
717
+ }
718
+ function createStopRtcRoomEvent(input) {
719
+ return {
720
+ event: "stop",
721
+ user_id: input.userId?.trim() || void 0,
722
+ session_uid: input.sessionUid?.trim() || void 0
723
+ };
724
+ }
725
+ function createTracksRtcRoomEvent(input) {
726
+ return {
727
+ event: "tracks",
728
+ user_id: input.userId?.trim() || void 0,
729
+ uid: input.uid?.trim() || void 0,
730
+ session_uid: input.sessionUid?.trim() || void 0,
731
+ tracks: input.tracks
732
+ };
733
+ }
734
+
735
+ // src/rtc/video-frame-rate.ts
736
+ var COMMON_VIDEO_FPS = [24, 25, 30, 48, 50, 60];
737
+ function snapToCommonVideoFps(fps) {
738
+ if (!Number.isFinite(fps) || fps <= 0) {
739
+ return 30;
740
+ }
741
+ const clamped = Math.max(1, Math.min(120, fps));
742
+ let best = 30;
743
+ let bestDiff = Infinity;
744
+ for (const candidate of COMMON_VIDEO_FPS) {
745
+ const diff = Math.abs(clamped - candidate);
746
+ if (diff < bestDiff || diff === bestDiff && candidate < best) {
747
+ bestDiff = diff;
748
+ best = candidate;
749
+ }
750
+ }
751
+ return best;
752
+ }
753
+ function snapToCommonVideoFpsConservative(fps) {
754
+ if (!Number.isFinite(fps) || fps <= 0) {
755
+ return 30;
756
+ }
757
+ const snapped = snapToCommonVideoFps(fps);
758
+ if (snapped <= fps + 0.5) {
759
+ return snapped;
760
+ }
761
+ const lowerCandidates = COMMON_VIDEO_FPS.filter((candidate) => candidate < snapped);
762
+ if (lowerCandidates.length > 0) {
763
+ return lowerCandidates[lowerCandidates.length - 1];
764
+ }
765
+ return snapped;
766
+ }
767
+ function resolveTrackFrameRate(track, fallback) {
768
+ const settings = track.getSettings?.();
769
+ if (typeof settings?.frameRate === "number" && settings.frameRate > 0) {
770
+ return snapToCommonVideoFpsConservative(settings.frameRate);
771
+ }
772
+ return snapToCommonVideoFpsConservative(fallback);
773
+ }
774
+ function resolvePlaybackDrivenEncoderFps(track, fallback = 24) {
775
+ return resolveTrackFrameRate(track, fallback);
776
+ }
777
+
778
+ // src/rtc/preview-media-layout.ts
779
+ var PREVIEW_UPLOAD_LAYOUT_ATTR = "data-preview-upload-layout";
780
+ var PREVIEW_UPLOAD_FIT_ATTR = "data-preview-upload-fit";
781
+ var PREVIEW_OUTPUT_LAYOUT_ATTR = "data-preview-output-layout";
782
+ var PREVIEW_CONTAIN_ATTR = "data-preview-contain";
783
+ var PREVIEW_COVER_ATTR = "data-preview-cover";
784
+ function hasFitPreviewLayout(container) {
785
+ 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;
786
+ }
787
+ function hasCoverPreviewLayout(container) {
788
+ return container.hasAttribute(PREVIEW_COVER_ATTR) || container.closest(`[${PREVIEW_COVER_ATTR}]`) !== null;
789
+ }
790
+ function resolveUploadFitMode(container) {
791
+ const node = container.hasAttribute(PREVIEW_UPLOAD_FIT_ATTR) ? container : container.closest(`[${PREVIEW_UPLOAD_FIT_ATTR}]`);
792
+ const fit = node?.getAttribute(PREVIEW_UPLOAD_FIT_ATTR);
793
+ return fit === "landscape" ? "landscape" : "portrait";
794
+ }
795
+ function applyUploadPreviewMediaLayout(container) {
796
+ if (hasCoverPreviewLayout(container)) {
797
+ return;
798
+ }
799
+ if (!hasFitPreviewLayout(container)) {
800
+ return;
801
+ }
802
+ const fitMode = resolveUploadFitMode(container);
803
+ container.style.overflow = "hidden";
804
+ container.style.display = "flex";
805
+ container.style.alignItems = "center";
806
+ container.style.justifyContent = "center";
807
+ container.querySelectorAll("div").forEach((node) => {
808
+ if (node === container) {
809
+ return;
810
+ }
811
+ const el = node;
812
+ el.style.width = "100%";
813
+ el.style.height = "100%";
814
+ if (fitMode === "landscape") {
815
+ el.style.display = "flex";
816
+ el.style.alignItems = "center";
817
+ el.style.justifyContent = "center";
818
+ el.style.overflow = "hidden";
819
+ }
820
+ });
821
+ container.querySelectorAll("video, canvas").forEach((node) => {
822
+ const el = node;
823
+ el.style.display = "block";
824
+ el.style.objectFit = "contain";
825
+ el.style.objectPosition = "center";
826
+ el.style.position = "relative";
827
+ el.style.left = "auto";
828
+ el.style.right = "auto";
829
+ el.style.top = "auto";
830
+ el.style.bottom = "auto";
831
+ el.style.transform = "none";
832
+ if (fitMode === "landscape") {
833
+ el.style.width = "100%";
834
+ el.style.height = "100%";
835
+ el.style.maxWidth = "none";
836
+ el.style.maxHeight = "none";
837
+ el.style.margin = "0";
838
+ return;
839
+ }
840
+ el.style.height = "100%";
841
+ el.style.width = "auto";
842
+ el.style.maxWidth = "none";
843
+ el.style.maxHeight = "none";
844
+ el.style.marginLeft = "auto";
845
+ el.style.marginRight = "auto";
846
+ el.style.marginTop = "auto";
847
+ el.style.marginBottom = "auto";
848
+ });
849
+ }
850
+ function observeUploadPreviewMediaLayout(container) {
851
+ if (!hasFitPreviewLayout(container) && !hasCoverPreviewLayout(container)) {
852
+ return () => {
853
+ };
854
+ }
855
+ let rafId = null;
856
+ const apply = () => applyUploadPreviewMediaLayout(container);
857
+ const scheduleApply = () => {
858
+ if (rafId !== null) {
859
+ return;
860
+ }
861
+ rafId = requestAnimationFrame(() => {
862
+ rafId = null;
863
+ apply();
864
+ });
865
+ };
866
+ apply();
867
+ const observer = new MutationObserver(() => scheduleApply());
868
+ observer.observe(container, {
869
+ childList: true,
870
+ subtree: true,
871
+ attributes: true
872
+ });
873
+ return () => {
874
+ if (rafId !== null) {
875
+ cancelAnimationFrame(rafId);
876
+ }
877
+ observer.disconnect();
878
+ };
879
+ }
880
+
881
+ // src/rtc/rtc-manager.ts
882
+ function resolveVideoRenderMode(container) {
883
+ 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}]`)) {
884
+ return VERTC.VideoRenderMode.RENDER_MODE_FIT;
885
+ }
886
+ if (container.hasAttribute("data-preview-cover") || container.hasAttribute("data-upload-preview-cover") || container.closest("[data-preview-cover]") || container.closest("[data-upload-preview-cover]")) {
887
+ return VERTC.VideoRenderMode.RENDER_MODE_HIDDEN;
888
+ }
889
+ const rect = container.getBoundingClientRect();
890
+ if (rect.width <= 0 || rect.height <= 0) {
891
+ return VERTC.VideoRenderMode.RENDER_MODE_FIT;
892
+ }
893
+ const isCompactPreview = rect.width <= 160;
894
+ const isPortraitFrame = rect.height > rect.width;
895
+ return isCompactPreview || isPortraitFrame ? VERTC.VideoRenderMode.RENDER_MODE_HIDDEN : VERTC.VideoRenderMode.RENDER_MODE_FIT;
896
+ }
897
+ var REMOTE_OUTPUT_STALL_MS = 2e4;
898
+ var REMOTE_OUTPUT_RECOVERY_COOLDOWN_MS = 6e4;
899
+ var SESSION_SEI_INTERVAL_MS = 500;
900
+ var DEFAULT_SEI_REPEAT_COUNT = 1;
901
+ var PUBLISH_START_MIN_FRAMES = 5;
902
+ var PUBLISH_START_FRAME_TIMEOUT_MS = 800;
903
+ var PUBLISH_COLD_MIN_FRAMES = 12;
904
+ var PUBLISH_COLD_FRAME_TIMEOUT_MS = 1500;
905
+ var PUBLISH_COLD_WARMUP_MS = 600;
906
+ var PUBLISH_WARM_SKIP_MS = 800;
907
+ var COLD_START_MIN_FRAMES = 3;
908
+ var WARM_START_MIN_FRAMES = 1;
909
+ var WARM_START_FRAME_TIMEOUT_MS = 200;
910
+ var COLD_START_SETTLE_MS = 50;
911
+ var COLD_START_OUTBOUND_TIMEOUT_MS = 1500;
912
+ var COLD_START_OUTBOUND_STABLE_MS = 200;
913
+ var COLD_START_MIN_OUTBOUND_FPS = 5;
914
+ var PUBLISH_READY_BUFFER_MS = 300;
915
+ var PUBLISH_READY_TIMEOUT_MS = 5e3;
916
+ var _RtcManager = class _RtcManager {
917
+ constructor(options = {}) {
918
+ this.engine = null;
919
+ this.leaveTask = null;
920
+ this.users = /* @__PURE__ */ new Set();
921
+ this.currentJoinInfo = null;
922
+ this.joined = false;
923
+ this.preserveRemoteDom = false;
924
+ this.localVideoPublished = false;
925
+ this.localVideoSourceType = VERTC.VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL;
926
+ this.shouldMirrorLocalPreview = false;
927
+ this.externalVideoTrack = null;
928
+ this.remoteVideoUserId = null;
929
+ this.pendingRemoteVideoUserId = null;
930
+ this.pendingRemoteVideoMediaType = VERTC.MediaType.VIDEO;
931
+ /** Subscribed to RTC media, but player may still be gated by SEI / container. */
932
+ this.subscribedRemoteUserId = null;
933
+ this.subscribedRemoteMediaType = VERTC.MediaType.VIDEO;
934
+ this.localVideoContainer = null;
935
+ this.remoteVideoContainer = null;
936
+ this.expectedRemoteSei = null;
937
+ this.lastRemoteSei = null;
938
+ this.lastRemoteSeiUserId = null;
939
+ this.localSeiMessage = null;
940
+ this.localSeiRepeatCount = DEFAULT_SEI_REPEAT_COUNT;
941
+ this.seiSendInterval = null;
942
+ this.lastSeiSentAtMs = 0;
943
+ this.hasLoggedSeiSendFailure = false;
944
+ this.seiSupport = "unknown";
945
+ this.hasLoggedSeiUnsupported = false;
946
+ this.lastDebugSeiMatched = null;
947
+ this.lastDebugRemoteSei = null;
948
+ this.remoteFrameWatchStop = null;
949
+ this.uploadPreviewLayoutObserverStop = null;
950
+ this.remoteOutputRecoveryCooldownUntil = 0;
951
+ this.joinedAtMs = 0;
952
+ this.publishStartedAtMs = 0;
953
+ this.lastRoomStartEventAtMs = 0;
954
+ this.localStreamStatsCount = 0;
955
+ this.firstNonZeroSentFrameRateAtMs = 0;
956
+ this.lastObservedSentFrameRate = 0;
957
+ this.publishReadyWaiters = [];
958
+ this.stablePublishWaiters = [];
959
+ this.publishReadyBufferTimer = null;
960
+ this.seiGateFallbackTimer = null;
961
+ this.seiMatchedDelayTimer = null;
962
+ this.lastSentResolution = "";
963
+ this.lastReceivedResolution = "";
964
+ this.onLog = options.onLog;
965
+ this.onStateChange = options.onStateChange;
966
+ this.onSeiGateChange = options.onSeiGateChange;
967
+ this.onRemoteSeiReceived = options.onRemoteSeiReceived;
968
+ this.onRemoteOutputStalled = options.onRemoteOutputStalled;
969
+ this.debug = options.debug ?? false;
970
+ this.i18n = options.i18n ?? createSDKI18n();
971
+ }
972
+ emitSeiGateState() {
973
+ const matched = this.isSeiMatched();
974
+ this.onSeiGateChange?.({
975
+ expectedSei: this.expectedRemoteSei,
976
+ sei: this.lastRemoteSei,
977
+ userId: this.lastRemoteSeiUserId,
978
+ matched
979
+ });
980
+ }
981
+ setPreserveRemoteDom(preserve) {
982
+ this.preserveRemoteDom = preserve;
983
+ }
984
+ setOnRemotePlayerMounted(callback) {
985
+ this.onRemotePlayerMounted = callback;
986
+ }
987
+ setLocalVideoContainer(container) {
988
+ const previous = this.localVideoContainer;
989
+ const changed = previous !== container;
990
+ this.localVideoContainer = container;
991
+ if (previous && changed) {
992
+ previous.replaceChildren();
993
+ }
994
+ if (container && changed) {
995
+ container.replaceChildren();
996
+ }
997
+ if (container && this.localVideoPublished && this.engine) {
998
+ this.bindLocalVideoPlayer();
999
+ }
1000
+ }
1001
+ setRemoteVideoContainer(container) {
1002
+ const previous = this.remoteVideoContainer;
1003
+ if (previous === container) {
1004
+ if (this.debug && container && container.childElementCount === 0 && (this.remoteVideoUserId || this.pendingRemoteVideoUserId)) {
1005
+ this.debugLog("setRemoteVideoContainer skipped but container empty", {
1006
+ remoteVideoUserId: this.remoteVideoUserId,
1007
+ pendingRemoteVideoUserId: this.pendingRemoteVideoUserId
1008
+ });
1009
+ }
1010
+ return;
1011
+ }
1012
+ this.remoteVideoContainer = container;
1013
+ this.debugLog("setRemoteVideoContainer", {
1014
+ hadPrevious: !!previous,
1015
+ next: !!container,
1016
+ remoteVideoUserId: this.remoteVideoUserId,
1017
+ pendingRemoteVideoUserId: this.pendingRemoteVideoUserId
1018
+ });
1019
+ if (previous) {
1020
+ previous.replaceChildren();
1021
+ }
1022
+ if (container && this.engine && this.joined && (this.remoteVideoUserId || this.pendingRemoteVideoUserId)) {
1023
+ void this.refreshRemoteVideoBinding();
1024
+ }
1025
+ }
1026
+ /** Display-only gate: expect bot output SEI to match task id — no local SEI send, no start blocking. */
1027
+ configureSessionSei(sessionId, _options = {}) {
1028
+ const normalizedSessionId = this.normalizeSei(sessionId);
1029
+ const previousExpectedRemoteSei = this.expectedRemoteSei;
1030
+ this.stopSeiSending();
1031
+ this.clearSeiGateFallbackTimer();
1032
+ this.clearSeiMatchedDelayTimer();
1033
+ this.localSeiMessage = null;
1034
+ this.expectedRemoteSei = this.seiSupport === "unsupported" ? null : normalizedSessionId;
1035
+ this.lastRemoteSei = null;
1036
+ this.lastRemoteSeiUserId = null;
1037
+ this.hasLoggedSeiSendFailure = false;
1038
+ if (previousExpectedRemoteSei !== this.expectedRemoteSei && this.remoteVideoUserId) {
1039
+ this.pendingRemoteVideoUserId = this.remoteVideoUserId;
1040
+ this.remoteVideoUserId = null;
1041
+ }
1042
+ this.emitSeiGateState();
1043
+ if (previousExpectedRemoteSei !== this.expectedRemoteSei && !this.preserveRemoteDom) {
1044
+ this.clearContainer(this.remoteVideoContainer);
1045
+ }
1046
+ this.scheduleSeiGateFallback(normalizedSessionId);
1047
+ }
1048
+ clearSeiGateFallbackTimer() {
1049
+ if (this.seiGateFallbackTimer !== null) {
1050
+ clearTimeout(this.seiGateFallbackTimer);
1051
+ this.seiGateFallbackTimer = null;
1052
+ }
1053
+ }
1054
+ clearSeiMatchedDelayTimer() {
1055
+ if (this.seiMatchedDelayTimer !== null) {
1056
+ clearTimeout(this.seiMatchedDelayTimer);
1057
+ this.seiMatchedDelayTimer = null;
1058
+ }
1059
+ }
1060
+ /** Show output if bot SEI never arrives — UI gate only, RTC bind is not blocked. */
1061
+ scheduleSeiGateFallback(expectedSei) {
1062
+ this.clearSeiGateFallbackTimer();
1063
+ if (!expectedSei) {
1064
+ return;
1065
+ }
1066
+ this.seiGateFallbackTimer = setTimeout(() => {
1067
+ this.seiGateFallbackTimer = null;
1068
+ if (this.expectedRemoteSei !== expectedSei || this.isSeiMatched()) {
1069
+ return;
1070
+ }
1071
+ this.log("warn", this.i18n.t("rtc.seiGateFallback"), { expectedSei });
1072
+ this.expectedRemoteSei = null;
1073
+ this.emitSeiGateState();
1074
+ void this.refreshRemoteVideoBinding({ force: true });
1075
+ }, _RtcManager.SEI_GATE_FALLBACK_MS);
1076
+ }
1077
+ get snapshot() {
1078
+ return {
1079
+ roomId: this.currentJoinInfo?.roomId ?? null,
1080
+ userId: this.currentJoinInfo?.userId ?? null,
1081
+ appId: this.currentJoinInfo?.appId ?? null,
1082
+ botUserId: this.currentJoinInfo?.botName ?? null,
1083
+ joined: this.joined,
1084
+ localVideoPublished: this.localVideoPublished,
1085
+ remoteVideoUserId: this.remoteVideoUserId,
1086
+ users: Array.from(this.users)
1087
+ };
1088
+ }
1089
+ getLocalVideoSize() {
1090
+ if (!this.engine || !this.joined) {
1091
+ return null;
1092
+ }
1093
+ const track = this.engine.getLocalStreamTrack(VERTC.StreamIndex.STREAM_INDEX_MAIN, "video");
1094
+ if (!track) {
1095
+ return null;
1096
+ }
1097
+ const settings = track.getSettings?.();
1098
+ const width = typeof settings?.width === "number" ? settings.width : 0;
1099
+ const height = typeof settings?.height === "number" ? settings.height : 0;
1100
+ if (!width || !height) {
1101
+ return null;
1102
+ }
1103
+ return { width, height };
1104
+ }
1105
+ async join(joinInfo) {
1106
+ if (this.currentJoinInfo?.roomId === joinInfo.roomId && this.joined) {
1107
+ this.log("info", this.i18n.t("rtc.alreadyInRoom"), joinInfo);
1108
+ return;
1109
+ }
1110
+ await this.leave();
1111
+ this.currentJoinInfo = joinInfo;
1112
+ this.users.clear();
1113
+ this.users.add(joinInfo.userId);
1114
+ this.localVideoPublished = false;
1115
+ this.remoteVideoUserId = null;
1116
+ this.pendingRemoteVideoUserId = null;
1117
+ this.pendingRemoteVideoMediaType = VERTC.MediaType.VIDEO;
1118
+ this.lastRemoteSei = null;
1119
+ this.lastRemoteSeiUserId = null;
1120
+ this.clearVideoContainers();
1121
+ const engine = VERTC__default.default.createEngine(joinInfo.appId);
1122
+ this.engine = engine;
1123
+ this.bindEngineEvents(engine);
1124
+ this.log("info", this.i18n.t("rtc.joiningRoom"), joinInfo);
1125
+ await engine.joinRoom(
1126
+ joinInfo.token,
1127
+ joinInfo.roomId,
1128
+ {
1129
+ userId: joinInfo.userId
1130
+ },
1131
+ {
1132
+ isAutoPublish: false,
1133
+ isAutoSubscribeAudio: false,
1134
+ isAutoSubscribeVideo: true,
1135
+ roomProfileType: VERTC.RoomProfileType.communication
1136
+ }
1137
+ );
1138
+ this.joined = true;
1139
+ this.joinedAtMs = performance.now();
1140
+ this.ensureSeiSupport();
1141
+ this.emitState();
1142
+ this.log("success", this.i18n.t("rtc.joinSuccess"), this.snapshot);
1143
+ this.log("info", this.i18n.t("rtc.roomUsers", { users: this.snapshot.users.join(", ") }), this.snapshot.users);
1144
+ }
1145
+ async startVideoPublishing(size) {
1146
+ if (!this.engine || !this.currentJoinInfo || !this.joined) {
1147
+ throw new Error(this.i18n.t("errors.rtcNotJoined"));
1148
+ }
1149
+ const publishStartedAtMs = performance.now();
1150
+ await this.applyVideoEncoderPreference(size);
1151
+ if (size) {
1152
+ await this.applyLocalVideoCaptureConfig(size);
1153
+ }
1154
+ if (this.localVideoPublished) {
1155
+ this.log("info", this.i18n.t("rtc.localVideoAlreadyPublished"));
1156
+ return;
1157
+ }
1158
+ this.shouldMirrorLocalPreview = true;
1159
+ this.bindLocalVideoPlayer();
1160
+ await this.switchToInternalVideoSource();
1161
+ await this.engine.startVideoCapture();
1162
+ await this.engine.publishStream(VERTC.MediaType.VIDEO);
1163
+ this.localVideoPublished = true;
1164
+ this.publishStartedAtMs = publishStartedAtMs;
1165
+ this.localStreamStatsCount = 0;
1166
+ this.firstNonZeroSentFrameRateAtMs = 0;
1167
+ this.lastObservedSentFrameRate = 0;
1168
+ this.ensureSeiSupport();
1169
+ this.emitState();
1170
+ this.log("success", this.i18n.t("rtc.localVideoPublished"));
1171
+ }
1172
+ async startExternalVideoPublishing(track, encoderSize, publishOptions) {
1173
+ if (!this.engine || !this.currentJoinInfo || !this.joined) {
1174
+ throw new Error(this.i18n.t("errors.rtcNotJoined"));
1175
+ }
1176
+ if (this.localVideoPublished) {
1177
+ this.log("info", this.i18n.t("rtc.localVideoAlreadyPublished"));
1178
+ return;
1179
+ }
1180
+ const publishStartedAtMs = performance.now();
1181
+ const settings = track.getSettings?.();
1182
+ const width = typeof settings?.width === "number" && settings.width > 0 ? settings.width : void 0;
1183
+ const height = typeof settings?.height === "number" && settings.height > 0 ? settings.height : void 0;
1184
+ const encoderOptions = publishOptions?.highQuality !== false ? {
1185
+ ...publishOptions,
1186
+ highQuality: true,
1187
+ fps: publishOptions?.fps ?? resolvePlaybackDrivenEncoderFps(track)
1188
+ } : publishOptions;
1189
+ this.shouldMirrorLocalPreview = false;
1190
+ this.bindLocalVideoPlayer();
1191
+ await this.switchToExternalVideoSource(track);
1192
+ await this.engine.publishStream(VERTC.MediaType.VIDEO);
1193
+ await this.applyVideoEncoderPreference(
1194
+ encoderSize ?? (width && height ? [width, height] : void 0),
1195
+ encoderOptions
1196
+ );
1197
+ this.localVideoPublished = true;
1198
+ this.publishStartedAtMs = publishStartedAtMs;
1199
+ this.localStreamStatsCount = 0;
1200
+ this.firstNonZeroSentFrameRateAtMs = 0;
1201
+ this.lastObservedSentFrameRate = 0;
1202
+ this.ensureSeiSupport();
1203
+ this.emitState();
1204
+ this.log("success", this.i18n.t("rtc.externalVideoPublished"));
1205
+ }
1206
+ async stopVideoPublishing(options) {
1207
+ if (!this.engine) {
1208
+ return;
1209
+ }
1210
+ this.stopSeiSending();
1211
+ this.clearPublishReadyWaiters();
1212
+ if (this.localVideoPublished) {
1213
+ await this.engine.unpublishStream(VERTC.MediaType.VIDEO);
1214
+ }
1215
+ if (this.localVideoSourceType === VERTC.VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL) {
1216
+ await this.engine.stopVideoCapture();
1217
+ }
1218
+ if (this.externalVideoTrack) {
1219
+ if (!options?.preserveExternalTrack) {
1220
+ try {
1221
+ this.externalVideoTrack.stop();
1222
+ } catch {
1223
+ }
1224
+ }
1225
+ this.externalVideoTrack = null;
1226
+ }
1227
+ await this.switchToInternalVideoSource(false);
1228
+ this.clearUploadPreviewLayoutObserver();
1229
+ this.clearContainer(this.localVideoContainer);
1230
+ this.clearRemoteVideo();
1231
+ this.localVideoPublished = false;
1232
+ this.publishStartedAtMs = 0;
1233
+ this.lastSentResolution = "";
1234
+ this.emitState();
1235
+ this.log("warn", this.i18n.t("rtc.localVideoStopped"));
1236
+ }
1237
+ async leave() {
1238
+ if (this.leaveTask) {
1239
+ await this.leaveTask;
1240
+ return;
1241
+ }
1242
+ const engine = this.engine;
1243
+ if (!engine) {
1244
+ return;
1245
+ }
1246
+ this.leaveTask = (async () => {
1247
+ try {
1248
+ if (this.localVideoPublished) {
1249
+ await this.stopVideoPublishing();
1250
+ }
1251
+ await engine.leaveRoom();
1252
+ } catch (error) {
1253
+ this.log("warn", this.i18n.t("rtc.leaveRoomError"), error);
1254
+ } finally {
1255
+ try {
1256
+ engine.destroy();
1257
+ } catch {
1258
+ }
1259
+ if (this.engine === engine) {
1260
+ this.engine = null;
1261
+ }
1262
+ this.currentJoinInfo = null;
1263
+ this.users.clear();
1264
+ this.joined = false;
1265
+ this.localVideoPublished = false;
1266
+ this.localVideoSourceType = VERTC.VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL;
1267
+ this.shouldMirrorLocalPreview = false;
1268
+ this.externalVideoTrack = null;
1269
+ this.remoteVideoUserId = null;
1270
+ this.pendingRemoteVideoUserId = null;
1271
+ this.pendingRemoteVideoMediaType = VERTC.MediaType.VIDEO;
1272
+ this.subscribedRemoteUserId = null;
1273
+ this.subscribedRemoteMediaType = VERTC.MediaType.VIDEO;
1274
+ this.stopSeiSending();
1275
+ this.clearSeiGateFallbackTimer();
1276
+ this.clearSeiMatchedDelayTimer();
1277
+ this.stopRemoteFrameWatch();
1278
+ this.clearPublishReadyWaiters();
1279
+ this.joinedAtMs = 0;
1280
+ this.publishStartedAtMs = 0;
1281
+ this.lastRoomStartEventAtMs = 0;
1282
+ this.localStreamStatsCount = 0;
1283
+ this.firstNonZeroSentFrameRateAtMs = 0;
1284
+ this.lastSentResolution = "";
1285
+ this.lastReceivedResolution = "";
1286
+ this.clearVideoContainers();
1287
+ this.emitState();
1288
+ this.log("info", this.i18n.t("rtc.engineDestroyed"));
1289
+ }
1290
+ })();
1291
+ try {
1292
+ await this.leaveTask;
1293
+ } finally {
1294
+ this.leaveTask = null;
1295
+ }
1296
+ }
1297
+ /**
1298
+ * Resolve whether to wait for input frames before start.
1299
+ * Warm pipelines (restart / long-lived publish) skip — iOS sends start while frames already flow.
1300
+ */
1301
+ resolveInputFrameWaitOptions() {
1302
+ if (!this.localVideoPublished || this.publishStartedAtMs <= 0) {
1303
+ return {
1304
+ minFrames: PUBLISH_COLD_MIN_FRAMES,
1305
+ timeoutMs: PUBLISH_COLD_FRAME_TIMEOUT_MS,
1306
+ minPublishMs: PUBLISH_COLD_WARMUP_MS
1307
+ };
1308
+ }
1309
+ const elapsedMs = performance.now() - this.publishStartedAtMs;
1310
+ if (elapsedMs >= PUBLISH_WARM_SKIP_MS) {
1311
+ return null;
1312
+ }
1313
+ return {
1314
+ minFrames: PUBLISH_COLD_MIN_FRAMES,
1315
+ timeoutMs: Math.max(400, PUBLISH_COLD_FRAME_TIMEOUT_MS - Math.floor(elapsedMs)),
1316
+ minPublishMs: Math.max(0, PUBLISH_COLD_WARMUP_MS - elapsedMs)
1317
+ };
1318
+ }
1319
+ /**
1320
+ * Wait until the published input track produces enough frames before sending start.
1321
+ * Uses actual frame delivery instead of sentFrameRate (~2s Volcengine stats window).
1322
+ */
1323
+ async waitForPublishedInputFrames(options = {}) {
1324
+ const track = this.externalVideoTrack ?? this.engine?.getLocalStreamTrack(VERTC.StreamIndex.STREAM_INDEX_MAIN, "video") ?? null;
1325
+ if (!track || track.readyState === "ended") {
1326
+ return { ready: false, frameCount: 0, elapsedMs: 0 };
1327
+ }
1328
+ const { waitForMediaStreamTrackFrames: waitForMediaStreamTrackFrames2 } = await Promise.resolve().then(() => (init_local_publish_diagnostics(), local_publish_diagnostics_exports));
1329
+ return waitForMediaStreamTrackFrames2(track, {
1330
+ minFrames: options.minFrames ?? PUBLISH_START_MIN_FRAMES,
1331
+ timeoutMs: options.timeoutMs ?? PUBLISH_START_FRAME_TIMEOUT_MS
1332
+ });
1333
+ }
1334
+ /**
1335
+ * Before every start: ensure the publish track is producing frames.
1336
+ * Cold — longer warmup; warm restart — quick sync (~200ms) so backend gets steady input after start.
1337
+ */
1338
+ async ensurePublishedInputBeforeStart() {
1339
+ if (!this.localVideoPublished || this.publishStartedAtMs <= 0) {
1340
+ return;
1341
+ }
1342
+ const publishAgeMs = performance.now() - this.publishStartedAtMs;
1343
+ const isCold = publishAgeMs < PUBLISH_WARM_SKIP_MS;
1344
+ if (!isCold && this.lastObservedSentFrameRate >= 15) {
1345
+ this.debugLog("ensurePublishedInputBeforeStart (warm, outbound steady)", {
1346
+ publishAgeMs: Number(publishAgeMs.toFixed(1)),
1347
+ sentFrameRate: this.lastObservedSentFrameRate
1348
+ });
1349
+ return;
1350
+ }
1351
+ if (isCold) {
1352
+ const [frameResult2] = await Promise.all([
1353
+ this.waitForPublishedInputFrames({
1354
+ minFrames: COLD_START_MIN_FRAMES,
1355
+ timeoutMs: PUBLISH_START_FRAME_TIMEOUT_MS
1356
+ }),
1357
+ this.waitForMinPublishDuration(PUBLISH_COLD_WARMUP_MS)
1358
+ ]);
1359
+ {
1360
+ await new Promise((resolve) => {
1361
+ setTimeout(resolve, COLD_START_SETTLE_MS);
1362
+ });
1363
+ }
1364
+ this.debugLog("ensurePublishedInputBeforeStart (cold)", {
1365
+ publishAgeMs: Number(publishAgeMs.toFixed(1)),
1366
+ frameReady: frameResult2.ready,
1367
+ frameCount: frameResult2.frameCount,
1368
+ sentFrameRate: this.lastObservedSentFrameRate
1369
+ });
1370
+ return;
1371
+ }
1372
+ const frameResult = await this.waitForPublishedInputFrames({
1373
+ minFrames: WARM_START_MIN_FRAMES,
1374
+ timeoutMs: WARM_START_FRAME_TIMEOUT_MS
1375
+ });
1376
+ if (!frameResult.ready && this.lastObservedSentFrameRate <= 0) {
1377
+ await this.waitForLocalPublishReady({ timeoutMs: 800 });
1378
+ }
1379
+ this.debugLog("ensurePublishedInputBeforeStart (warm sync)", {
1380
+ publishAgeMs: Number(publishAgeMs.toFixed(1)),
1381
+ frameReady: frameResult.ready,
1382
+ frameCount: frameResult.frameCount,
1383
+ sentFrameRate: this.lastObservedSentFrameRate
1384
+ });
1385
+ }
1386
+ /** @deprecated Use {@link ensurePublishedInputBeforeStart}. */
1387
+ async waitForRtcOutboundBeforeStart(options) {
1388
+ const timeoutMs = options?.timeoutMs ?? PUBLISH_READY_TIMEOUT_MS;
1389
+ const publishAgeMs = this.publishStartedAtMs > 0 ? performance.now() - this.publishStartedAtMs : 0;
1390
+ const localFramesPromise = publishAgeMs >= PUBLISH_WARM_SKIP_MS ? Promise.resolve({ ready: true, frameCount: 0, elapsedMs: 0 }) : this.waitForPublishedInputFrames({
1391
+ minFrames: PUBLISH_START_MIN_FRAMES,
1392
+ timeoutMs: PUBLISH_START_FRAME_TIMEOUT_MS
1393
+ });
1394
+ const [, rtcReady] = await Promise.all([
1395
+ localFramesPromise,
1396
+ this.waitForLocalPublishReady({ timeoutMs })
1397
+ ]);
1398
+ return rtcReady;
1399
+ }
1400
+ /** @deprecated Use {@link waitForRtcOutboundBeforeStart}. */
1401
+ async waitForPublishedInputFramesBeforeStart() {
1402
+ const waitOptions = this.resolveInputFrameWaitOptions();
1403
+ const waitTasks = [
1404
+ this.waitForLocalPublishReady({ timeoutMs: PUBLISH_READY_TIMEOUT_MS })
1405
+ ];
1406
+ if (waitOptions) {
1407
+ waitTasks.push(
1408
+ this.waitForMinPublishDuration(waitOptions.minPublishMs),
1409
+ this.waitForPublishedInputFrames({
1410
+ minFrames: waitOptions.minFrames,
1411
+ timeoutMs: waitOptions.timeoutMs
1412
+ })
1413
+ );
1414
+ }
1415
+ const [rtcResult, ...rest] = await Promise.all(waitTasks);
1416
+ const rtcSend = rtcResult;
1417
+ const frameResult = waitOptions ? rest[1] : { ready: true, frameCount: 0, elapsedMs: 0 };
1418
+ return {
1419
+ ...frameResult,
1420
+ rtcSendReady: rtcSend.ready,
1421
+ rtcSendReason: rtcSend.reason,
1422
+ rtcSendElapsedMs: rtcSend.elapsedSincePublishMs
1423
+ };
1424
+ }
1425
+ async waitForMinPublishDuration(minMs) {
1426
+ if (minMs <= 0 || this.publishStartedAtMs <= 0) {
1427
+ return;
1428
+ }
1429
+ const elapsedMs = performance.now() - this.publishStartedAtMs;
1430
+ const remainingMs = minMs - elapsedMs;
1431
+ if (remainingMs <= 0) {
1432
+ return;
1433
+ }
1434
+ await new Promise((resolve) => {
1435
+ setTimeout(resolve, remainingMs);
1436
+ });
1437
+ }
1438
+ /**
1439
+ * Subscribe to the bot output stream before start — iOS bindRemoteVideoRenderer runs pre-request.
1440
+ */
1441
+ async prepareRemoteVideoForGeneration() {
1442
+ const botUserId = this.currentJoinInfo?.botName?.trim();
1443
+ if (!botUserId || !this.engine || !this.joined) {
1444
+ return;
1445
+ }
1446
+ this.pendingRemoteVideoUserId = botUserId;
1447
+ this.pendingRemoteVideoMediaType = VERTC.MediaType.VIDEO;
1448
+ await this.ensureRemoteStreamSubscribed(botUserId, VERTC.MediaType.VIDEO);
1449
+ }
1450
+ /**
1451
+ * Wait until outbound sentFrameRate reaches minFps and stays there for stableMs.
1452
+ * Avoids starting while Volcengine encoder is still ramping (causes early dropped frames).
1453
+ */
1454
+ async waitForStableLocalPublishReady(options) {
1455
+ const minFps = options?.minFps ?? COLD_START_MIN_OUTBOUND_FPS;
1456
+ const timeoutMs = options?.timeoutMs ?? COLD_START_OUTBOUND_TIMEOUT_MS;
1457
+ const stableMs = options?.stableMs ?? COLD_START_OUTBOUND_STABLE_MS;
1458
+ const publishStartedAtMs = this.publishStartedAtMs;
1459
+ if (!this.localVideoPublished || publishStartedAtMs <= 0) {
1460
+ return {
1461
+ ready: false,
1462
+ reason: "not-published",
1463
+ elapsedSincePublishMs: 0
1464
+ };
1465
+ }
1466
+ return new Promise((resolve) => {
1467
+ let settled = false;
1468
+ let stableTimer = null;
1469
+ const finish = (result) => {
1470
+ if (settled) {
1471
+ return;
1472
+ }
1473
+ settled = true;
1474
+ clearTimeout(timeoutTimer);
1475
+ if (stableTimer !== null) {
1476
+ clearTimeout(stableTimer);
1477
+ }
1478
+ const index = this.stablePublishWaiters.indexOf(check);
1479
+ if (index >= 0) {
1480
+ this.stablePublishWaiters.splice(index, 1);
1481
+ }
1482
+ resolve(result);
1483
+ };
1484
+ const check = () => {
1485
+ if (this.lastObservedSentFrameRate >= minFps) {
1486
+ if (stableTimer === null) {
1487
+ stableTimer = setTimeout(() => {
1488
+ finish({
1489
+ ready: true,
1490
+ reason: "sent-frame-rate",
1491
+ elapsedSincePublishMs: Number((performance.now() - publishStartedAtMs).toFixed(1))
1492
+ });
1493
+ }, stableMs);
1494
+ }
1495
+ return;
1496
+ }
1497
+ if (stableTimer !== null) {
1498
+ clearTimeout(stableTimer);
1499
+ stableTimer = null;
1500
+ }
1501
+ };
1502
+ this.stablePublishWaiters.push(check);
1503
+ check();
1504
+ const timeoutTimer = setTimeout(() => {
1505
+ finish({
1506
+ ready: this.lastObservedSentFrameRate > 0,
1507
+ reason: "timeout",
1508
+ elapsedSincePublishMs: Number((performance.now() - publishStartedAtMs).toFixed(1))
1509
+ });
1510
+ }, timeoutMs);
1511
+ });
1512
+ }
1513
+ /**
1514
+ * Wait until Volcengine reports non-zero sentFrameRate (~2s stats window).
1515
+ * Prefer this over local track frame counts before start — backend only processes from start.
1516
+ */
1517
+ async waitForLocalPublishReady(options) {
1518
+ const timeoutMs = options?.timeoutMs ?? PUBLISH_READY_TIMEOUT_MS;
1519
+ const publishStartedAtMs = this.publishStartedAtMs;
1520
+ if (!this.localVideoPublished || publishStartedAtMs <= 0) {
1521
+ return {
1522
+ ready: false,
1523
+ reason: "not-published",
1524
+ elapsedSincePublishMs: 0
1525
+ };
1526
+ }
1527
+ if (this.firstNonZeroSentFrameRateAtMs > 0) {
1528
+ return {
1529
+ ready: true,
1530
+ reason: "already-ready",
1531
+ elapsedSincePublishMs: Number((this.firstNonZeroSentFrameRateAtMs - publishStartedAtMs).toFixed(1))
1532
+ };
1533
+ }
1534
+ return new Promise((resolve) => {
1535
+ let settled = false;
1536
+ const finish = (result) => {
1537
+ if (settled) {
1538
+ return;
1539
+ }
1540
+ settled = true;
1541
+ clearTimeout(timeoutTimer);
1542
+ const index = this.publishReadyWaiters.indexOf(onReady);
1543
+ if (index >= 0) {
1544
+ this.publishReadyWaiters.splice(index, 1);
1545
+ }
1546
+ resolve(result);
1547
+ };
1548
+ const onReady = (result) => finish(result);
1549
+ this.publishReadyWaiters.push(onReady);
1550
+ const timeoutTimer = setTimeout(() => {
1551
+ finish({
1552
+ ready: false,
1553
+ reason: "timeout",
1554
+ elapsedSincePublishMs: Number((performance.now() - publishStartedAtMs).toFixed(1))
1555
+ });
1556
+ }, timeoutMs);
1557
+ });
1558
+ }
1559
+ async sendRoomEvent(event) {
1560
+ this.ensureReadyForMessaging();
1561
+ const payload = JSON.stringify(event);
1562
+ const eventName = typeof event === "object" && event !== null && "event" in event ? String(event.event ?? "unknown") : "unknown";
1563
+ if (eventName === "start") {
1564
+ this.lastRoomStartEventAtMs = performance.now();
1565
+ }
1566
+ await this.engine?.sendRoomMessage(payload);
1567
+ this.log("success", this.i18n.t("rtc.roomEventSent"), event);
1568
+ }
1569
+ clearRemoteVideo(options) {
1570
+ this.stopRemoteFrameWatch();
1571
+ if (!options?.preserveDom && !this.preserveRemoteDom) {
1572
+ this.clearContainer(this.remoteVideoContainer);
1573
+ }
1574
+ if (this.remoteVideoUserId) {
1575
+ this.pendingRemoteVideoUserId = this.remoteVideoUserId;
1576
+ this.remoteVideoUserId = null;
1577
+ this.emitState();
1578
+ }
1579
+ }
1580
+ async refreshRemoteVideoBinding(options) {
1581
+ const targetUserId = this.remoteVideoUserId ?? this.pendingRemoteVideoUserId;
1582
+ if (!this.engine || !targetUserId || !this.remoteVideoContainer) {
1583
+ return;
1584
+ }
1585
+ await this.ensureRemoteStreamSubscribed(targetUserId, this.pendingRemoteVideoMediaType);
1586
+ if (!this.canRenderRemoteVideo(targetUserId)) {
1587
+ this.debugLog("refreshRemoteVideoBinding blocked by SEI gate", {
1588
+ targetUserId,
1589
+ expectedRemoteSei: this.expectedRemoteSei,
1590
+ lastRemoteSei: this.lastRemoteSei
1591
+ });
1592
+ return;
1593
+ }
1594
+ if (!options?.force && this.remoteVideoUserId === targetUserId && this.remoteVideoContainer && this.remoteVideoContainer.childElementCount > 0) {
1595
+ this.debugLog("refreshRemoteVideoBinding skipped (already mounted)", { targetUserId });
1596
+ return;
1597
+ }
1598
+ try {
1599
+ await this.mountRemoteVideoPlayer(
1600
+ targetUserId,
1601
+ this.remoteVideoContainer,
1602
+ this.pendingRemoteVideoMediaType,
1603
+ options?.force
1604
+ );
1605
+ this.remoteVideoUserId = targetUserId;
1606
+ this.pendingRemoteVideoUserId = null;
1607
+ this.emitState();
1608
+ this.startRemoteFrameWatch(targetUserId);
1609
+ this.emitRemotePlayerMounted();
1610
+ this.log("info", this.i18n.t("rtc.remotePlayerRefreshed", { userId: targetUserId }));
1611
+ } catch (error) {
1612
+ this.log("warn", this.i18n.t("rtc.remotePlayerRefreshFailed"), error);
1613
+ }
1614
+ }
1615
+ bindEngineEvents(engine) {
1616
+ engine.on(VERTC__default.default.events.onUserJoined, (event) => {
1617
+ this.users.add(event.userInfo.userId);
1618
+ this.emitState();
1619
+ this.log("success", this.i18n.t("rtc.userJoined", { userId: event.userInfo.userId }), event);
1620
+ this.log("info", this.i18n.t("rtc.roomUsers", { users: this.snapshot.users.join(", ") }), this.snapshot.users);
1621
+ });
1622
+ engine.on(VERTC__default.default.events.onUserLeave, (event) => {
1623
+ this.users.delete(event.userInfo.userId);
1624
+ if (this.remoteVideoUserId === event.userInfo.userId) {
1625
+ this.remoteVideoUserId = null;
1626
+ this.clearContainer(this.remoteVideoContainer);
1627
+ }
1628
+ if (this.pendingRemoteVideoUserId === event.userInfo.userId) {
1629
+ this.pendingRemoteVideoUserId = null;
1630
+ }
1631
+ this.emitState();
1632
+ this.log("warn", this.i18n.t("rtc.userLeft", { userId: event.userInfo.userId }), event);
1633
+ this.log("info", this.i18n.t("rtc.roomUsers", { users: this.snapshot.users.join(", ") }), this.snapshot.users);
1634
+ });
1635
+ engine.on(VERTC__default.default.events.onConnectionStateChanged, (event) => {
1636
+ this.log("info", this.i18n.t("rtc.connectionStateChanged"), event);
1637
+ });
1638
+ engine.on(VERTC__default.default.events.onUserPublishStream, (event) => {
1639
+ this.log("success", this.i18n.t("rtc.userPublished", { userId: event.userId }), event);
1640
+ if (this.supportsVideo(event.mediaType)) {
1641
+ void this.attachRemoteVideo(event.userId, event.mediaType);
1642
+ }
1643
+ });
1644
+ engine.on(VERTC__default.default.events.onUserUnpublishStream, (event) => {
1645
+ this.log("warn", this.i18n.t("rtc.userUnpublished", { userId: event.userId }), event);
1646
+ this.debugLog("onUserUnpublishStream", {
1647
+ userId: event.userId,
1648
+ mediaType: event.mediaType,
1649
+ remoteVideoUserId: this.remoteVideoUserId,
1650
+ expectedRemoteSei: this.expectedRemoteSei,
1651
+ lastRemoteSei: this.lastRemoteSei
1652
+ });
1653
+ if (this.remoteVideoUserId === event.userId && this.supportsVideo(event.mediaType)) {
1654
+ this.pendingRemoteVideoUserId = event.userId;
1655
+ this.pendingRemoteVideoMediaType = event.mediaType;
1656
+ this.remoteVideoUserId = null;
1657
+ this.stopRemoteFrameWatch();
1658
+ if (!this.preserveRemoteDom) {
1659
+ this.clearContainer(this.remoteVideoContainer);
1660
+ }
1661
+ this.emitState();
1662
+ return;
1663
+ }
1664
+ });
1665
+ engine.on(VERTC__default.default.events.onSEIMessageReceived, (event) => {
1666
+ const userId = event.remoteStreamKey.userId;
1667
+ const sei = this.normalizeSei(this.decodeSeiMessage(event.sei));
1668
+ if (sei && this.isRemoteSeiUser(userId)) {
1669
+ this.onRemoteSeiReceived?.({ sei, userId });
1670
+ }
1671
+ this.lastRemoteSei = sei;
1672
+ this.lastRemoteSeiUserId = userId;
1673
+ const matched = this.isSeiMatched();
1674
+ const seiStateChanged = this.lastDebugSeiMatched !== matched || this.lastDebugRemoteSei !== sei;
1675
+ if (!this.debug || seiStateChanged) {
1676
+ this.log("info", this.i18n.t("rtc.seiReceived", { userId }), { userId, sei, matched });
1677
+ }
1678
+ if (this.debug && seiStateChanged) {
1679
+ this.debugLog("onSEIMessageReceived", {
1680
+ userId,
1681
+ sei,
1682
+ matched,
1683
+ expectedRemoteSei: this.expectedRemoteSei
1684
+ });
1685
+ this.lastDebugSeiMatched = matched;
1686
+ this.lastDebugRemoteSei = sei;
1687
+ }
1688
+ if (!matched) {
1689
+ this.onSeiGateChange?.({
1690
+ expectedSei: this.expectedRemoteSei,
1691
+ sei,
1692
+ userId,
1693
+ matched: false
1694
+ });
1695
+ return;
1696
+ }
1697
+ if (this.seiMatchedDelayTimer !== null) {
1698
+ return;
1699
+ }
1700
+ this.clearSeiGateFallbackTimer();
1701
+ this.stopSeiSending();
1702
+ this.seiMatchedDelayTimer = setTimeout(() => {
1703
+ this.seiMatchedDelayTimer = null;
1704
+ if (!this.isSeiMatched()) {
1705
+ return;
1706
+ }
1707
+ this.onSeiGateChange?.({
1708
+ expectedSei: this.expectedRemoteSei,
1709
+ sei,
1710
+ userId,
1711
+ matched: true
1712
+ });
1713
+ if (this.remoteVideoUserId === userId) {
1714
+ return;
1715
+ }
1716
+ void this.attachRemoteVideo(userId, this.pendingRemoteVideoMediaType);
1717
+ }, _RtcManager.SEI_MATCHED_DISPLAY_DELAY_MS);
1718
+ });
1719
+ engine.on(VERTC__default.default.events.onError, (event) => {
1720
+ this.log("error", this.i18n.t("rtc.error", { code: String(event.errorCode) }), event);
1721
+ });
1722
+ engine.on(VERTC__default.default.events.onPublishResult, (event) => {
1723
+ this.debugLog("onPublishResult", {
1724
+ ...event,
1725
+ elapsedSincePublishMs: this.publishStartedAtMs > 0 ? Number((performance.now() - this.publishStartedAtMs).toFixed(1)) : null
1726
+ });
1727
+ });
1728
+ engine.on(VERTC__default.default.events.onLocalStreamStats, (stats) => {
1729
+ this.logLocalStreamStats(stats);
1730
+ if (stats.videoStats) {
1731
+ const width = stats.videoStats.sentResolutionWidth ?? stats.videoStats.encodedFrameWidth ?? 0;
1732
+ const height = stats.videoStats.sentResolutionHeight ?? stats.videoStats.encodedFrameHeight ?? 0;
1733
+ stats.videoStats.sentFrameRate ?? 0;
1734
+ const res = `${width}x${height}`;
1735
+ if (width > 0 && height > 0 && res !== this.lastSentResolution) {
1736
+ this.lastSentResolution = res;
1737
+ }
1738
+ }
1739
+ });
1740
+ engine.on(VERTC__default.default.events.onRemoteStreamStats, (stats) => {
1741
+ if (stats?.videoStats) {
1742
+ const { width, height, receivedFrameRate } = stats.videoStats;
1743
+ const w = width ?? 0;
1744
+ const h = height ?? 0;
1745
+ const res = `${w}x${h}`;
1746
+ if (w > 0 && h > 0 && res !== this.lastReceivedResolution) {
1747
+ this.lastReceivedResolution = res;
1748
+ }
1749
+ }
1750
+ });
1751
+ engine.on(VERTC__default.default.events.onLocalStatsException, (exception) => {
1752
+ this.debugLog("onLocalStatsException", {
1753
+ exception,
1754
+ elapsedSincePublishMs: this.publishStartedAtMs > 0 ? Number((performance.now() - this.publishStartedAtMs).toFixed(1)) : null
1755
+ });
1756
+ });
1757
+ engine.on(VERTC__default.default.events.onTrackMute, (event) => {
1758
+ this.debugLog("onTrackMute", event);
1759
+ });
1760
+ engine.on(VERTC__default.default.events.onTrackUnmute, (event) => {
1761
+ this.debugLog("onTrackUnmute", event);
1762
+ });
1763
+ }
1764
+ emitState() {
1765
+ this.onStateChange?.(this.snapshot);
1766
+ }
1767
+ ensureReadyForMessaging() {
1768
+ if (!this.engine || !this.joined) {
1769
+ throw new Error(this.i18n.t("errors.rtcNotJoinedForMessaging"));
1770
+ }
1771
+ }
1772
+ bindLocalVideoPlayer(force = false) {
1773
+ if (!this.localVideoContainer) {
1774
+ return;
1775
+ }
1776
+ const container = this.getRequiredContainer(this.localVideoContainer, "local");
1777
+ if (!force && container.childElementCount > 0) {
1778
+ this.bindUploadPreviewLayout(container);
1779
+ return;
1780
+ }
1781
+ container.replaceChildren();
1782
+ this.engine?.setLocalVideoMirrorType(
1783
+ this.shouldMirrorLocalPreview ? VERTC.MirrorType.MIRROR_TYPE_RENDER : VERTC.MirrorType.MIRROR_TYPE_NONE
1784
+ );
1785
+ this.engine?.setLocalVideoPlayer(VERTC.StreamIndex.STREAM_INDEX_MAIN, {
1786
+ renderDom: container,
1787
+ renderMode: resolveVideoRenderMode(container)
1788
+ });
1789
+ this.bindUploadPreviewLayout(container);
1790
+ }
1791
+ bindUploadPreviewLayout(container) {
1792
+ this.uploadPreviewLayoutObserverStop?.();
1793
+ this.uploadPreviewLayoutObserverStop = null;
1794
+ applyUploadPreviewMediaLayout(container);
1795
+ this.uploadPreviewLayoutObserverStop = observeUploadPreviewMediaLayout(container);
1796
+ }
1797
+ clearUploadPreviewLayoutObserver() {
1798
+ this.uploadPreviewLayoutObserverStop?.();
1799
+ this.uploadPreviewLayoutObserverStop = null;
1800
+ }
1801
+ /** Re-bind local preview after the container becomes visible or is resized (mobile H5). */
1802
+ refreshLocalVideoPreview(options) {
1803
+ if (!this.engine || !this.localVideoPublished || !this.localVideoContainer) {
1804
+ return;
1805
+ }
1806
+ this.bindLocalVideoPlayer(options?.force);
1807
+ }
1808
+ async switchToExternalVideoSource(track) {
1809
+ if (!this.engine) {
1810
+ return;
1811
+ }
1812
+ this.externalVideoTrack = track;
1813
+ await this.engine.setVideoSourceType(VERTC.StreamIndex.STREAM_INDEX_MAIN, VERTC.VideoSourceType.VIDEO_SOURCE_TYPE_EXTERNAL);
1814
+ await this.engine.setExternalVideoTrack(VERTC.StreamIndex.STREAM_INDEX_MAIN, track);
1815
+ this.localVideoSourceType = VERTC.VideoSourceType.VIDEO_SOURCE_TYPE_EXTERNAL;
1816
+ }
1817
+ async switchToInternalVideoSource(shouldSetConfig = true) {
1818
+ if (!this.engine) {
1819
+ return;
1820
+ }
1821
+ if (this.localVideoSourceType === VERTC.VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL) {
1822
+ return;
1823
+ }
1824
+ await this.engine.setVideoSourceType(VERTC.StreamIndex.STREAM_INDEX_MAIN, VERTC.VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL);
1825
+ this.localVideoSourceType = VERTC.VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL;
1826
+ if (shouldSetConfig && this.externalVideoTrack) {
1827
+ this.externalVideoTrack = null;
1828
+ }
1829
+ }
1830
+ async applyLocalVideoCaptureConfig([width, height]) {
1831
+ if (!this.engine) {
1832
+ return;
1833
+ }
1834
+ await this.engine.setVideoCaptureConfig({
1835
+ width,
1836
+ height
1837
+ });
1838
+ this.log("info", this.i18n.t("rtc.captureSizeSet", { width, height }));
1839
+ }
1840
+ resolveEncoderMaxKbps(width, height, highQuality = true) {
1841
+ const pixels = width * height;
1842
+ if (highQuality) {
1843
+ if (pixels >= 2e6) {
1844
+ return 6e3;
1845
+ }
1846
+ if (pixels >= 1e6) {
1847
+ return 4e3;
1848
+ }
1849
+ return 3e3;
1850
+ }
1851
+ if (pixels >= 2e6) {
1852
+ return 5e3;
1853
+ }
1854
+ if (pixels >= 1e6) {
1855
+ return 3e3;
1856
+ }
1857
+ return 2e3;
1858
+ }
1859
+ async applyVideoEncoderPreference(size, publishOptions) {
1860
+ if (!this.engine) {
1861
+ return;
1862
+ }
1863
+ try {
1864
+ const width = size?.[0] ?? 1280;
1865
+ const height = size?.[1] ?? 720;
1866
+ if (publishOptions) {
1867
+ const fps = publishOptions.fps ?? 30;
1868
+ const maxKbps = this.resolveEncoderMaxKbps(width, height, publishOptions.highQuality);
1869
+ await this.engine.setVideoEncoderConfig({
1870
+ width,
1871
+ height,
1872
+ frameRate: fps,
1873
+ maxKbps,
1874
+ contentHint: "detail",
1875
+ preferCodecName: VERTC.VideoCodecType.H264
1876
+ });
1877
+ } else {
1878
+ await this.engine.setVideoEncoderConfig({
1879
+ width,
1880
+ height,
1881
+ frameRate: 24,
1882
+ maxKbps: -1,
1883
+ contentHint: "detail",
1884
+ // @ts-expect-error: encodePreference is supported by ByteRTC SDK but may be missing from current typings
1885
+ encodePreference: 1,
1886
+ preferCodecName: VERTC.VideoCodecType.H264
1887
+ });
1888
+ }
1889
+ this.log("info", this.i18n.t("rtc.encoderPreferenceSet"), size);
1890
+ } catch (error) {
1891
+ this.log("warn", this.i18n.t("rtc.encoderPreferenceFailed"), error);
1892
+ }
1893
+ }
1894
+ async attachRemoteVideo(userId, mediaType) {
1895
+ if (!this.engine || !this.currentJoinInfo) {
1896
+ return;
1897
+ }
1898
+ const preferredBotUserId = this.currentJoinInfo.botName?.trim() || null;
1899
+ const shouldReplaceCurrent = this.remoteVideoUserId === null || userId === preferredBotUserId || this.remoteVideoUserId !== preferredBotUserId;
1900
+ if (!shouldReplaceCurrent) {
1901
+ return;
1902
+ }
1903
+ this.pendingRemoteVideoUserId = userId;
1904
+ this.pendingRemoteVideoMediaType = mediaType;
1905
+ await this.ensureRemoteStreamSubscribed(userId, mediaType);
1906
+ if (!this.canRenderRemoteVideo(userId)) {
1907
+ this.log("info", this.i18n.t("rtc.remoteWaitSei", { userId }), {
1908
+ expectedRemoteSei: this.expectedRemoteSei,
1909
+ lastRemoteSei: this.lastRemoteSei,
1910
+ lastRemoteSeiUserId: this.lastRemoteSeiUserId
1911
+ });
1912
+ return;
1913
+ }
1914
+ const container = this.remoteVideoContainer;
1915
+ if (!container) {
1916
+ this.log("warn", this.i18n.t("rtc.remoteContainerNotReady"), { userId, mediaType });
1917
+ return;
1918
+ }
1919
+ await this.mountRemoteVideoPlayer(userId, container, mediaType);
1920
+ this.remoteVideoUserId = userId;
1921
+ this.pendingRemoteVideoUserId = null;
1922
+ this.emitState();
1923
+ this.startRemoteFrameWatch(userId);
1924
+ this.emitRemotePlayerMounted();
1925
+ this.log("success", this.i18n.t("rtc.remotePlayerBound", { userId }));
1926
+ }
1927
+ async ensureRemoteStreamSubscribed(userId, mediaType) {
1928
+ if (!this.engine) {
1929
+ return;
1930
+ }
1931
+ if (this.subscribedRemoteUserId === userId) {
1932
+ return;
1933
+ }
1934
+ await this.engine.subscribeStream(userId, mediaType);
1935
+ this.subscribedRemoteUserId = userId;
1936
+ this.subscribedRemoteMediaType = mediaType;
1937
+ this.debugLog("ensureRemoteStreamSubscribed", { userId, mediaType });
1938
+ }
1939
+ bindRemoteVideoPlayerToContainer(userId, container) {
1940
+ if (!this.engine) {
1941
+ return;
1942
+ }
1943
+ this.engine.setRemoteVideoPlayer(VERTC.StreamIndex.STREAM_INDEX_MAIN, {
1944
+ userId,
1945
+ renderDom: container,
1946
+ renderMode: resolveVideoRenderMode(container)
1947
+ });
1948
+ this.bindUploadPreviewLayout(container);
1949
+ }
1950
+ async mountRemoteVideoPlayer(userId, container, mediaType, force = false) {
1951
+ if (!this.engine) {
1952
+ return;
1953
+ }
1954
+ if (!force && this.remoteVideoUserId === userId && container === this.remoteVideoContainer && container.childElementCount > 0) {
1955
+ this.debugLog("mountRemoteVideoPlayer skipped (already mounted)", { userId });
1956
+ return;
1957
+ }
1958
+ this.debugLog(force ? "mountRemoteVideoPlayer (force)" : "mountRemoteVideoPlayer", {
1959
+ userId,
1960
+ mediaType,
1961
+ childCountBefore: container.childElementCount
1962
+ });
1963
+ this.stopRemoteFrameWatch();
1964
+ container.replaceChildren();
1965
+ await this.ensureRemoteStreamSubscribed(userId, mediaType);
1966
+ this.bindRemoteVideoPlayerToContainer(userId, container);
1967
+ }
1968
+ startRemoteFrameWatch(userId) {
1969
+ this.stopRemoteFrameWatch();
1970
+ let lastFrameAt = 0;
1971
+ let hadFirstFrame = false;
1972
+ let rvfcHandle = null;
1973
+ let watchedVideo = null;
1974
+ const onFrame = () => {
1975
+ lastFrameAt = Date.now();
1976
+ hadFirstFrame = true;
1977
+ };
1978
+ const bindRvfc = (video) => {
1979
+ if (watchedVideo === video && rvfcHandle !== null) {
1980
+ return;
1981
+ }
1982
+ if (watchedVideo && watchedVideo !== video) {
1983
+ watchedVideo.removeEventListener("timeupdate", onFrame);
1984
+ }
1985
+ watchedVideo = video;
1986
+ const rvfcVideo = video;
1987
+ if (rvfcVideo.requestVideoFrameCallback) {
1988
+ const schedule = () => {
1989
+ if (this.remoteVideoUserId !== userId) {
1990
+ return;
1991
+ }
1992
+ rvfcHandle = rvfcVideo.requestVideoFrameCallback(() => {
1993
+ onFrame();
1994
+ schedule();
1995
+ });
1996
+ };
1997
+ schedule();
1998
+ return;
1999
+ }
2000
+ video.addEventListener("timeupdate", onFrame);
2001
+ };
2002
+ const pollTimer = setInterval(() => {
2003
+ if (this.remoteVideoUserId !== userId || !this.remoteVideoContainer) {
2004
+ return;
2005
+ }
2006
+ const video = this.remoteVideoContainer.querySelector("video");
2007
+ if (video) {
2008
+ bindRvfc(video);
2009
+ }
2010
+ if (!hadFirstFrame || lastFrameAt === 0) {
2011
+ return;
2012
+ }
2013
+ const stallMs = Date.now() - lastFrameAt;
2014
+ if (stallMs < REMOTE_OUTPUT_STALL_MS) {
2015
+ return;
2016
+ }
2017
+ if (Date.now() < this.remoteOutputRecoveryCooldownUntil) {
2018
+ return;
2019
+ }
2020
+ this.remoteOutputRecoveryCooldownUntil = Date.now() + REMOTE_OUTPUT_RECOVERY_COOLDOWN_MS;
2021
+ this.debugLog("remote output stalled", { userId, stallMs });
2022
+ this.log("warn", this.i18n.t("rtc.remoteOutputStalled"), { userId, stallMs });
2023
+ this.onRemoteOutputStalled?.({ userId, stallMs });
2024
+ hadFirstFrame = false;
2025
+ lastFrameAt = 0;
2026
+ }, 3e3);
2027
+ this.remoteFrameWatchStop = () => {
2028
+ clearInterval(pollTimer);
2029
+ if (watchedVideo) {
2030
+ watchedVideo.removeEventListener("timeupdate", onFrame);
2031
+ }
2032
+ if (rvfcHandle !== null && watchedVideo) {
2033
+ const rvfcVideo = watchedVideo;
2034
+ rvfcVideo.cancelVideoFrameCallback?.(rvfcHandle);
2035
+ }
2036
+ rvfcHandle = null;
2037
+ watchedVideo = null;
2038
+ };
2039
+ }
2040
+ stopRemoteFrameWatch() {
2041
+ this.remoteFrameWatchStop?.();
2042
+ this.remoteFrameWatchStop = null;
2043
+ }
2044
+ isRemoteSeiUser(userId) {
2045
+ const localUserId = this.currentJoinInfo?.userId?.trim();
2046
+ if (localUserId && userId === localUserId) {
2047
+ return false;
2048
+ }
2049
+ const botUserId = this.currentJoinInfo?.botName?.trim();
2050
+ if (botUserId) {
2051
+ return userId === botUserId;
2052
+ }
2053
+ return true;
2054
+ }
2055
+ /** Always mount remote player — SEI gate is UI-only (see deriveOutputVisibility). */
2056
+ canRenderRemoteVideo(_userId) {
2057
+ return true;
2058
+ }
2059
+ /** Whether remote SEI matches the active task (display gate). */
2060
+ isSeiMatched() {
2061
+ if (this.seiSupport === "unsupported") {
2062
+ return true;
2063
+ }
2064
+ if (!this.expectedRemoteSei) {
2065
+ return true;
2066
+ }
2067
+ return this.lastRemoteSei === this.expectedRemoteSei;
2068
+ }
2069
+ /** Low-rate SEI until remote output SEI matches — avoids Volcengine sendSEIMessage timeouts. */
2070
+ startContinuousSeiSending() {
2071
+ this.stopSeiSending();
2072
+ if (!this.localSeiMessage || !this.engine || !this.joined || !this.localVideoPublished || this.seiSupport === "unsupported") {
2073
+ return;
2074
+ }
2075
+ const tick = () => {
2076
+ if (this.isSeiMatched() && this.expectedRemoteSei) {
2077
+ this.stopSeiSending();
2078
+ return;
2079
+ }
2080
+ this.flushSeiMessage();
2081
+ };
2082
+ this.seiSendInterval = setInterval(tick, SESSION_SEI_INTERVAL_MS);
2083
+ }
2084
+ stopSeiSending() {
2085
+ if (this.seiSendInterval !== null) {
2086
+ this.debugLog("stopSeiSending");
2087
+ clearInterval(this.seiSendInterval);
2088
+ this.seiSendInterval = null;
2089
+ }
2090
+ this.lastSeiSentAtMs = 0;
2091
+ }
2092
+ canSendLocalSeiNow() {
2093
+ if (this.firstNonZeroSentFrameRateAtMs <= 0) {
2094
+ return false;
2095
+ }
2096
+ const now = performance.now();
2097
+ return now - this.lastSeiSentAtMs >= SESSION_SEI_INTERVAL_MS - 16;
2098
+ }
2099
+ flushSeiMessage() {
2100
+ if (!this.engine || !this.joined || !this.localVideoPublished || !this.localSeiMessage || this.seiSupport === "unsupported") {
2101
+ return;
2102
+ }
2103
+ if (!this.canSendLocalSeiNow()) {
2104
+ return;
2105
+ }
2106
+ this.lastSeiSentAtMs = performance.now();
2107
+ try {
2108
+ this.engine.sendSEIMessage(VERTC.StreamIndex.STREAM_INDEX_MAIN, this.localSeiMessage, this.localSeiRepeatCount);
2109
+ } catch (error) {
2110
+ if (!this.hasLoggedSeiSendFailure) {
2111
+ this.log("warn", this.i18n.t("rtc.sendSeiFailed"), error);
2112
+ this.hasLoggedSeiSendFailure = true;
2113
+ }
2114
+ this.stopSeiSending();
2115
+ }
2116
+ }
2117
+ ensureSeiSupport() {
2118
+ if (this.seiSupport !== "unknown") {
2119
+ return;
2120
+ }
2121
+ void VERTC__default.default.getSupportedCodecs().then((codecs) => {
2122
+ const supported = codecs.some((c) => c.toLowerCase() === "h264" || c.toLowerCase() === "h.264");
2123
+ this.seiSupport = supported ? "supported" : "unsupported";
2124
+ if (this.seiSupport === "unsupported" && !this.hasLoggedSeiUnsupported) {
2125
+ this.hasLoggedSeiUnsupported = true;
2126
+ this.log("warn", this.i18n.t("rtc.seiUnsupported"), codecs);
2127
+ }
2128
+ if (this.seiSupport === "unsupported") {
2129
+ this.expectedRemoteSei = null;
2130
+ this.emitSeiGateState();
2131
+ void this.refreshRemoteVideoBinding();
2132
+ } else if (this.localSeiMessage) {
2133
+ this.startContinuousSeiSending();
2134
+ this.emitSeiGateState();
2135
+ void this.refreshRemoteVideoBinding();
2136
+ }
2137
+ }).catch((error) => {
2138
+ this.seiSupport = "unsupported";
2139
+ if (!this.hasLoggedSeiUnsupported) {
2140
+ this.hasLoggedSeiUnsupported = true;
2141
+ this.log("warn", this.i18n.t("rtc.getCodecsFailed"), error);
2142
+ }
2143
+ this.expectedRemoteSei = null;
2144
+ this.emitSeiGateState();
2145
+ void this.refreshRemoteVideoBinding();
2146
+ });
2147
+ }
2148
+ normalizeSei(value) {
2149
+ const normalized = value?.trim();
2150
+ if (!normalized) {
2151
+ return null;
2152
+ }
2153
+ return normalized.startsWith("task-") ? normalized.slice("task-".length) : normalized;
2154
+ }
2155
+ normalizeSeiRepeatCount(value) {
2156
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2157
+ return DEFAULT_SEI_REPEAT_COUNT;
2158
+ }
2159
+ return Math.max(0, Math.min(4, Math.floor(value)));
2160
+ }
2161
+ decodeSeiMessage(sei) {
2162
+ try {
2163
+ return new TextDecoder().decode(sei);
2164
+ } catch {
2165
+ return null;
2166
+ }
2167
+ }
2168
+ supportsVideo(mediaType) {
2169
+ return mediaType === VERTC.MediaType.VIDEO || mediaType === VERTC.MediaType.AUDIO_AND_VIDEO;
2170
+ }
2171
+ clearVideoContainers() {
2172
+ this.stopRemoteFrameWatch();
2173
+ this.clearContainer(this.localVideoContainer);
2174
+ this.clearContainer(this.remoteVideoContainer);
2175
+ }
2176
+ clearContainer(container) {
2177
+ container?.replaceChildren();
2178
+ }
2179
+ getRequiredContainer(container, containerType) {
2180
+ if (!container) {
2181
+ throw new Error(`missing ${containerType} video container`);
2182
+ }
2183
+ return container;
2184
+ }
2185
+ emitRemotePlayerMounted() {
2186
+ this.onRemotePlayerMounted?.();
2187
+ }
2188
+ log(level, message, data) {
2189
+ this.onLog?.({ level, message, data });
2190
+ }
2191
+ resolvePublishReadyWaiters(reason) {
2192
+ if (this.publishReadyWaiters.length === 0 || this.publishStartedAtMs <= 0) {
2193
+ return;
2194
+ }
2195
+ const result = {
2196
+ ready: true,
2197
+ reason,
2198
+ elapsedSincePublishMs: Number((performance.now() - this.publishStartedAtMs).toFixed(1))
2199
+ };
2200
+ const waiters = this.publishReadyWaiters.splice(0);
2201
+ for (const waiter of waiters) {
2202
+ waiter(result);
2203
+ }
2204
+ }
2205
+ clearPublishReadyWaiters() {
2206
+ if (this.publishReadyBufferTimer !== null) {
2207
+ clearTimeout(this.publishReadyBufferTimer);
2208
+ this.publishReadyBufferTimer = null;
2209
+ }
2210
+ this.publishReadyWaiters = [];
2211
+ this.stablePublishWaiters = [];
2212
+ }
2213
+ notifyStablePublishWaiters() {
2214
+ if (this.stablePublishWaiters.length === 0) {
2215
+ return;
2216
+ }
2217
+ const waiters = this.stablePublishWaiters.slice();
2218
+ for (const waiter of waiters) {
2219
+ waiter();
2220
+ }
2221
+ }
2222
+ schedulePublishReadyWhenStable(sentFrameRate) {
2223
+ if (this.publishReadyWaiters.length === 0 || this.publishReadyBufferTimer !== null) {
2224
+ return;
2225
+ }
2226
+ if (sentFrameRate <= 0) {
2227
+ return;
2228
+ }
2229
+ this.publishReadyBufferTimer = setTimeout(() => {
2230
+ this.publishReadyBufferTimer = null;
2231
+ this.resolvePublishReadyWaiters("sent-frame-rate");
2232
+ }, PUBLISH_READY_BUFFER_MS);
2233
+ }
2234
+ logLocalStreamStats(stats) {
2235
+ if (!this.localVideoPublished) {
2236
+ return;
2237
+ }
2238
+ this.localStreamStatsCount += 1;
2239
+ const nowMs = performance.now();
2240
+ const sentFrameRate = stats.videoStats?.sentFrameRate ?? 0;
2241
+ if (sentFrameRate > 0 && this.firstNonZeroSentFrameRateAtMs === 0) {
2242
+ this.firstNonZeroSentFrameRateAtMs = nowMs;
2243
+ }
2244
+ this.lastObservedSentFrameRate = sentFrameRate;
2245
+ this.schedulePublishReadyWhenStable(sentFrameRate);
2246
+ this.notifyStablePublishWaiters();
2247
+ }
2248
+ debugLog(message, data) {
2249
+ if (!this.debug) {
2250
+ return;
2251
+ }
2252
+ this.log("info", `[debug] ${message}`, data);
2253
+ }
2254
+ };
2255
+ _RtcManager.SEI_GATE_FALLBACK_MS = 6e3;
2256
+ _RtcManager.SEI_MATCHED_DISPLAY_DELAY_MS = 200;
2257
+ var RtcManager = _RtcManager;
2258
+
2259
+ // src/rtc/capture-host.ts
2260
+ var captureHostRoot = null;
2261
+ var CAPTURE_VIDEO_STYLE = "position:fixed;left:-10000px;top:0;width:640px;height:480px;opacity:1;pointer-events:none;z-index:-1;";
2262
+ function mountCaptureVideo(videoEl) {
2263
+ if (!captureHostRoot) {
2264
+ captureHostRoot = document.createElement("div");
2265
+ captureHostRoot.setAttribute("data-xmax-capture-host", "true");
2266
+ captureHostRoot.style.cssText = "position:fixed;left:0;top:0;width:0;height:0;overflow:hidden;pointer-events:none;z-index:-1;";
2267
+ document.body.appendChild(captureHostRoot);
2268
+ }
2269
+ videoEl.setAttribute("playsinline", "true");
2270
+ videoEl.setAttribute("webkit-playsinline", "true");
2271
+ videoEl.style.cssText = CAPTURE_VIDEO_STYLE;
2272
+ captureHostRoot.appendChild(videoEl);
2273
+ }
2274
+
2275
+ // src/rtc/camera-stream-v2.ts
2276
+ async function createNativeCameraStream(sourceStream, options = {}) {
2277
+ const i18n = options.i18n ?? createSDKI18n();
2278
+ const sourceTrack = sourceStream.getVideoTracks()[0];
2279
+ if (!sourceTrack) {
2280
+ throw new Error(i18n.t("errors.videoTrackMissing"));
2281
+ }
2282
+ const videoEl = document.createElement("video");
2283
+ videoEl.playsInline = true;
2284
+ videoEl.muted = true;
2285
+ videoEl.autoplay = true;
2286
+ videoEl.srcObject = sourceStream;
2287
+ mountCaptureVideo(videoEl);
2288
+ await new Promise((resolve, reject) => {
2289
+ const onOk = () => {
2290
+ cleanup();
2291
+ resolve();
2292
+ };
2293
+ const onErr = () => {
2294
+ cleanup();
2295
+ reject(new Error(`failed to load video metadata`));
2296
+ };
2297
+ const cleanup = () => {
2298
+ videoEl.removeEventListener("loadedmetadata", onOk);
2299
+ videoEl.removeEventListener("error", onErr);
2300
+ };
2301
+ videoEl.addEventListener("loadedmetadata", onOk, { once: true });
2302
+ videoEl.addEventListener("error", onErr, { once: true });
2303
+ });
2304
+ const playResult = videoEl.play();
2305
+ if (playResult && typeof playResult.then === "function") {
2306
+ await playResult;
2307
+ }
2308
+ const width = videoEl.videoWidth || 0;
2309
+ const height = videoEl.videoHeight || 0;
2310
+ if (!width || !height) {
2311
+ throw new Error(i18n.t("errors.videoTrackMissing"));
2312
+ }
2313
+ const destroy = () => {
2314
+ try {
2315
+ videoEl.pause();
2316
+ } catch {
2317
+ }
2318
+ try {
2319
+ videoTrack.stop();
2320
+ } catch {
2321
+ }
2322
+ try {
2323
+ sourceStream.getTracks().forEach((track) => {
2324
+ if (track.readyState !== "ended") {
2325
+ track.stop();
2326
+ }
2327
+ });
2328
+ } catch {
2329
+ }
2330
+ try {
2331
+ videoEl.srcObject = null;
2332
+ videoEl.remove();
2333
+ } catch {
2334
+ }
2335
+ };
2336
+ const videoTrack = sourceTrack;
2337
+ return {
2338
+ sourceStream,
2339
+ previewStream: sourceStream,
2340
+ // No canvas mirror, preview is the raw stream
2341
+ videoTrack,
2342
+ videoEl,
2343
+ width,
2344
+ height,
2345
+ destroy
2346
+ };
2347
+ }
2348
+
2349
+ // src/drag/map-viewport.ts
2350
+ function clamp(n, min, max) {
2351
+ return Math.min(Math.max(n, min), max);
2352
+ }
2353
+ function getFitContentRect(rect, targetSize, fitMode = "contain") {
2354
+ const [targetWidth, targetHeight] = targetSize;
2355
+ if (!targetWidth || !targetHeight || !rect.width || !rect.height) {
2356
+ return {
2357
+ left: rect.left,
2358
+ top: rect.top,
2359
+ width: rect.width,
2360
+ height: rect.height
2361
+ };
2362
+ }
2363
+ const scale = fitMode === "cover" ? Math.max(rect.width / targetWidth, rect.height / targetHeight) : Math.min(rect.width / targetWidth, rect.height / targetHeight);
2364
+ const width = targetWidth * scale;
2365
+ const height = targetHeight * scale;
2366
+ return {
2367
+ left: rect.left + (rect.width - width) / 2,
2368
+ top: rect.top + (rect.height - height) / 2,
2369
+ width,
2370
+ height
2371
+ };
2372
+ }
2373
+ function getContentRect(rect, targetSize) {
2374
+ return getFitContentRect(rect, targetSize, "contain");
2375
+ }
2376
+ function findDragMediaElement(dragSurface) {
2377
+ const root = dragSurface.parentElement ?? dragSurface;
2378
+ if (typeof root.querySelector !== "function") {
2379
+ return null;
2380
+ }
2381
+ const videoHost = root.querySelector("[data-xmax-remote-video]") ?? root;
2382
+ if (typeof videoHost.querySelector !== "function") {
2383
+ return null;
2384
+ }
2385
+ const media = videoHost.querySelector("video, canvas");
2386
+ return media ?? null;
2387
+ }
2388
+ function toContentRect(rect) {
2389
+ return {
2390
+ left: rect.left,
2391
+ top: rect.top,
2392
+ width: rect.width,
2393
+ height: rect.height
2394
+ };
2395
+ }
2396
+ function isMediaInsetInContainer(mediaRect, containerRect) {
2397
+ if (!mediaRect.width || !mediaRect.height || !containerRect.width || !containerRect.height) {
2398
+ return false;
2399
+ }
2400
+ const widthRatio = mediaRect.width / containerRect.width;
2401
+ const heightRatio = mediaRect.height / containerRect.height;
2402
+ return widthRatio < 0.98 || heightRatio < 0.98;
2403
+ }
2404
+ function resolveMappingContext(dragSurface, targetSize, fitMode = "cover") {
2405
+ const containerRect = dragSurface.getBoundingClientRect();
2406
+ const media = findDragMediaElement(dragSurface);
2407
+ if (media) {
2408
+ const mediaRect = media.getBoundingClientRect();
2409
+ if (mediaRect.width > 0 && mediaRect.height > 0 && isMediaInsetInContainer(mediaRect, containerRect)) {
2410
+ const content2 = toContentRect(mediaRect);
2411
+ return { content: content2, hitRect: content2, source: "media" };
2412
+ }
2413
+ }
2414
+ const content = getFitContentRect(containerRect, targetSize, fitMode);
2415
+ return {
2416
+ content,
2417
+ hitRect: toContentRect(containerRect),
2418
+ source: "fit"
2419
+ };
2420
+ }
2421
+ function mapViewportPointToCoordinateSpace(e, container, targetSize, options = {}) {
2422
+ const fitMode = options.fitMode ?? "cover";
2423
+ const mirrored = options.mirrored ?? false;
2424
+ const [targetWidth, targetHeight] = targetSize;
2425
+ if (!targetWidth || !targetHeight) {
2426
+ return null;
2427
+ }
2428
+ const { content, hitRect } = resolveMappingContext(container, targetSize, fitMode);
2429
+ if (!content.width || !content.height) {
2430
+ return null;
2431
+ }
2432
+ if (e.clientX < hitRect.left || e.clientY < hitRect.top || e.clientX > hitRect.left + hitRect.width || e.clientY > hitRect.top + hitRect.height) {
2433
+ return null;
2434
+ }
2435
+ const x = Math.round((e.clientX - content.left) * targetWidth / content.width);
2436
+ const y = Math.round((e.clientY - content.top) * targetHeight / content.height);
2437
+ const normalizedX = mirrored ? targetWidth - 1 - x : x;
2438
+ return [
2439
+ clamp(normalizedX, 0, targetWidth - 1),
2440
+ clamp(y, 0, targetHeight - 1)
2441
+ ];
2442
+ }
2443
+ function mapTargetPointToCanvas(point, container, targetSize, dpr, options = {}) {
2444
+ const fitMode = options.fitMode ?? "cover";
2445
+ const mirrored = options.mirrored ?? false;
2446
+ const containerRect = container.getBoundingClientRect();
2447
+ const [targetWidth, targetHeight] = targetSize;
2448
+ const { content } = resolveMappingContext(container, targetSize, fitMode);
2449
+ if (!targetWidth || !targetHeight || !content.width || !content.height) {
2450
+ return null;
2451
+ }
2452
+ const displayX = mirrored ? targetWidth - 1 - point[0] : point[0];
2453
+ const originX = (content.left - containerRect.left) * dpr;
2454
+ const originY = (content.top - containerRect.top) * dpr;
2455
+ const contentW = content.width * dpr;
2456
+ const contentH = content.height * dpr;
2457
+ return [
2458
+ originX + displayX / targetWidth * contentW,
2459
+ originY + point[1] / targetHeight * contentH
2460
+ ];
2461
+ }
2462
+
2463
+ // src/drag/drag-track-controller.ts
2464
+ function fadeCanvas(ctx) {
2465
+ ctx.save();
2466
+ ctx.globalCompositeOperation = "destination-out";
2467
+ ctx.fillStyle = "rgba(0,0,0,0.06)";
2468
+ ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
2469
+ ctx.restore();
2470
+ }
2471
+ function drawNeon(ctx, segs, container, targetSize, dpr, fitMode, mirrored) {
2472
+ if (segs.length === 0) {
2473
+ return;
2474
+ }
2475
+ const coreW = 3.6 * dpr;
2476
+ const lineW = 10 * dpr;
2477
+ const glowRadius = 28 * dpr;
2478
+ const color = { r: 20, g: 255, b: 80 };
2479
+ const rgba = (a) => `rgba(${color.r},${color.g},${color.b},${a})`;
2480
+ const strokeSegs = (opts) => {
2481
+ ctx.save();
2482
+ ctx.globalCompositeOperation = opts.composite;
2483
+ ctx.lineCap = "round";
2484
+ ctx.lineJoin = "round";
2485
+ ctx.lineWidth = opts.width;
2486
+ ctx.strokeStyle = rgba(opts.alpha);
2487
+ ctx.shadowBlur = opts.shadowBlur;
2488
+ ctx.shadowColor = rgba(opts.shadowAlpha);
2489
+ ctx.beginPath();
2490
+ for (const seg of segs) {
2491
+ const start = mapTargetPointToCanvas(seg.a, container, targetSize, dpr, { fitMode, mirrored });
2492
+ const end = mapTargetPointToCanvas(seg.b, container, targetSize, dpr, { fitMode, mirrored });
2493
+ if (!start || !end) {
2494
+ continue;
2495
+ }
2496
+ ctx.moveTo(start[0], start[1]);
2497
+ ctx.lineTo(end[0], end[1]);
2498
+ }
2499
+ ctx.stroke();
2500
+ ctx.restore();
2501
+ };
2502
+ strokeSegs({
2503
+ width: lineW,
2504
+ shadowBlur: glowRadius,
2505
+ shadowAlpha: 0.9,
2506
+ alpha: 0.45,
2507
+ composite: "lighter"
2508
+ });
2509
+ strokeSegs({
2510
+ width: lineW,
2511
+ shadowBlur: 12 * dpr,
2512
+ shadowAlpha: 0.9,
2513
+ alpha: 0.9,
2514
+ composite: "lighter"
2515
+ });
2516
+ strokeSegs({
2517
+ width: coreW,
2518
+ shadowBlur: 6 * dpr,
2519
+ shadowAlpha: 0.8,
2520
+ alpha: 1,
2521
+ composite: "source-over"
2522
+ });
2523
+ ctx.save();
2524
+ ctx.globalCompositeOperation = "lighter";
2525
+ ctx.fillStyle = rgba(1);
2526
+ ctx.shadowBlur = 10 * dpr;
2527
+ ctx.shadowColor = rgba(0.8);
2528
+ const head = segs[segs.length - 1];
2529
+ if (head) {
2530
+ const headPoint = mapTargetPointToCanvas(head.b, container, targetSize, dpr, { fitMode, mirrored });
2531
+ if (headPoint) {
2532
+ ctx.beginPath();
2533
+ ctx.arc(headPoint[0], headPoint[1], 3.2 * dpr, 0, Math.PI * 2);
2534
+ ctx.fill();
2535
+ }
2536
+ }
2537
+ ctx.restore();
2538
+ }
2539
+ var DragTrackController = class {
2540
+ constructor(container, options) {
2541
+ this.activePointers = /* @__PURE__ */ new Map();
2542
+ this.lastTargetPoints = /* @__PURE__ */ new Map();
2543
+ this.segQueue = [];
2544
+ this.sampleTimer = null;
2545
+ this.sampleInFlight = false;
2546
+ this.rafId = null;
2547
+ this.resizeObserver = null;
2548
+ this.container = container;
2549
+ this.onTracks = options.onTracks;
2550
+ this.targetSize = options.targetSize;
2551
+ this.fitMode = options.fitMode ?? "cover";
2552
+ this.mirrored = options.mirrored ?? false;
2553
+ this.enabled = options.enabled ?? true;
2554
+ this.emitEmptyTracksWhenIdle = options.emitEmptyTracksWhenIdle ?? false;
2555
+ this.canvas = document.createElement("canvas");
2556
+ this.canvas.className = "Xmax-drag-track-canvas";
2557
+ this.canvas.style.cssText = "position:absolute;inset:0;z-index:21;width:100%;height:100%;pointer-events:none;";
2558
+ container.appendChild(this.canvas);
2559
+ this.onWindowResize = () => this.resizeCanvas();
2560
+ this.onPointerDown = (e) => this.handlePointerDown(e);
2561
+ this.onPointerMove = (e) => this.handlePointerMove(e);
2562
+ this.onPointerUp = (e) => this.handlePointerUp(e);
2563
+ this.onPointerCancel = (e) => this.handlePointerCancel(e);
2564
+ this.resizeCanvas();
2565
+ this.bindResizeObserver();
2566
+ this.bindPointerEvents();
2567
+ this.applyInteractionStyle();
2568
+ if (this.enabled && this.emitEmptyTracksWhenIdle) {
2569
+ this.startSampling();
2570
+ }
2571
+ }
2572
+ setEnabled(enabled) {
2573
+ this.enabled = enabled;
2574
+ this.applyInteractionStyle();
2575
+ if (!enabled) {
2576
+ this.finishStroke({ stopIdleSampling: true });
2577
+ return;
2578
+ }
2579
+ if (this.emitEmptyTracksWhenIdle && this.activePointers.size === 0) {
2580
+ this.startSampling();
2581
+ }
2582
+ }
2583
+ setEmitEmptyTracksWhenIdle(emitEmptyTracksWhenIdle) {
2584
+ if (this.emitEmptyTracksWhenIdle === emitEmptyTracksWhenIdle) {
2585
+ return;
2586
+ }
2587
+ this.emitEmptyTracksWhenIdle = emitEmptyTracksWhenIdle;
2588
+ if (emitEmptyTracksWhenIdle && this.enabled && this.activePointers.size === 0) {
2589
+ this.startSampling();
2590
+ return;
2591
+ }
2592
+ if (!emitEmptyTracksWhenIdle && this.activePointers.size === 0) {
2593
+ this.stopSampling();
2594
+ }
2595
+ }
2596
+ setTargetSize(targetSize) {
2597
+ this.targetSize = targetSize;
2598
+ }
2599
+ setFitMode(fitMode) {
2600
+ this.fitMode = fitMode;
2601
+ }
2602
+ setMirrored(mirrored) {
2603
+ this.mirrored = mirrored;
2604
+ }
2605
+ destroy() {
2606
+ this.finishStroke({ stopIdleSampling: true });
2607
+ this.unbindPointerEvents();
2608
+ this.unbindResizeObserver();
2609
+ this.canvas.remove();
2610
+ }
2611
+ applyInteractionStyle() {
2612
+ this.container.style.pointerEvents = this.enabled ? "auto" : "none";
2613
+ this.container.style.touchAction = this.enabled ? "none" : "auto";
2614
+ }
2615
+ bindResizeObserver() {
2616
+ const ResizeObserverCtor = globalThis.ResizeObserver;
2617
+ if (ResizeObserverCtor) {
2618
+ this.resizeObserver = new ResizeObserverCtor(() => this.resizeCanvas());
2619
+ this.resizeObserver.observe(this.container);
2620
+ return;
2621
+ }
2622
+ window.addEventListener("resize", this.onWindowResize);
2623
+ }
2624
+ unbindResizeObserver() {
2625
+ this.resizeObserver?.disconnect();
2626
+ this.resizeObserver = null;
2627
+ window.removeEventListener("resize", this.onWindowResize);
2628
+ }
2629
+ bindPointerEvents() {
2630
+ this.container.addEventListener("pointerdown", this.onPointerDown);
2631
+ this.container.addEventListener("pointermove", this.onPointerMove);
2632
+ this.container.addEventListener("pointerup", this.onPointerUp);
2633
+ this.container.addEventListener("pointercancel", this.onPointerCancel);
2634
+ }
2635
+ unbindPointerEvents() {
2636
+ this.container.removeEventListener("pointerdown", this.onPointerDown);
2637
+ this.container.removeEventListener("pointermove", this.onPointerMove);
2638
+ this.container.removeEventListener("pointerup", this.onPointerUp);
2639
+ this.container.removeEventListener("pointercancel", this.onPointerCancel);
2640
+ this.activePointers.clear();
2641
+ this.lastTargetPoints.clear();
2642
+ this.sampleInFlight = false;
2643
+ }
2644
+ clearCanvas() {
2645
+ const ctx = this.canvas.getContext("2d");
2646
+ if (!ctx) {
2647
+ return;
2648
+ }
2649
+ ctx.save();
2650
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
2651
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
2652
+ ctx.restore();
2653
+ }
2654
+ resizeCanvas() {
2655
+ const rect = this.container.getBoundingClientRect();
2656
+ const dpr = window.devicePixelRatio || 1;
2657
+ const cssW = Math.max(1, Math.round(rect.width));
2658
+ const cssH = Math.max(1, Math.round(rect.height));
2659
+ const nextW = Math.max(1, Math.round(cssW * dpr));
2660
+ const nextH = Math.max(1, Math.round(cssH * dpr));
2661
+ if (this.canvas.width !== nextW || this.canvas.height !== nextH) {
2662
+ this.canvas.width = nextW;
2663
+ this.canvas.height = nextH;
2664
+ this.canvas.style.width = `${cssW}px`;
2665
+ this.canvas.style.height = `${cssH}px`;
2666
+ this.clearCanvas();
2667
+ }
2668
+ }
2669
+ toTargetPoint(e) {
2670
+ return mapViewportPointToCoordinateSpace(e, this.container, this.targetSize, {
2671
+ fitMode: this.fitMode,
2672
+ mirrored: this.mirrored
2673
+ });
2674
+ }
2675
+ stopDrawLoop() {
2676
+ if (this.rafId !== null) {
2677
+ window.cancelAnimationFrame(this.rafId);
2678
+ this.rafId = null;
2679
+ }
2680
+ }
2681
+ startDrawLoop() {
2682
+ if (this.rafId !== null) {
2683
+ return;
2684
+ }
2685
+ const tick = () => {
2686
+ const ctx = this.canvas.getContext("2d");
2687
+ if (!ctx) {
2688
+ this.rafId = null;
2689
+ return;
2690
+ }
2691
+ const dpr = window.devicePixelRatio || 1;
2692
+ fadeCanvas(ctx);
2693
+ drawNeon(ctx, this.segQueue.splice(0), this.container, this.targetSize, dpr, this.fitMode, this.mirrored);
2694
+ if (this.activePointers.size > 0) {
2695
+ this.rafId = window.requestAnimationFrame(tick);
2696
+ } else {
2697
+ this.rafId = null;
2698
+ }
2699
+ };
2700
+ this.rafId = window.requestAnimationFrame(tick);
2701
+ }
2702
+ stopSampling() {
2703
+ if (this.sampleTimer !== null) {
2704
+ window.clearInterval(this.sampleTimer);
2705
+ this.sampleTimer = null;
2706
+ }
2707
+ }
2708
+ collectCurrentTracks() {
2709
+ const tracks = [];
2710
+ for (const pointer of Array.from(this.activePointers.values())) {
2711
+ const point = this.toTargetPoint(pointer);
2712
+ if (point) {
2713
+ tracks.push(point);
2714
+ }
2715
+ }
2716
+ return tracks;
2717
+ }
2718
+ flushTracks() {
2719
+ if (this.sampleInFlight) {
2720
+ return;
2721
+ }
2722
+ const tracks = this.collectCurrentTracks();
2723
+ if (!tracks.length && !this.emitEmptyTracksWhenIdle) {
2724
+ return;
2725
+ }
2726
+ this.sampleInFlight = true;
2727
+ Promise.resolve(this.onTracks(tracks)).catch(() => {
2728
+ }).finally(() => {
2729
+ this.sampleInFlight = false;
2730
+ });
2731
+ }
2732
+ startSampling() {
2733
+ if (this.sampleTimer !== null) {
2734
+ return;
2735
+ }
2736
+ this.flushTracks();
2737
+ this.sampleTimer = window.setInterval(() => {
2738
+ this.flushTracks();
2739
+ }, 30);
2740
+ }
2741
+ finishStroke(options) {
2742
+ const stopIdleSampling = options?.stopIdleSampling ?? !this.emitEmptyTracksWhenIdle;
2743
+ if (stopIdleSampling) {
2744
+ this.stopSampling();
2745
+ }
2746
+ this.stopDrawLoop();
2747
+ this.segQueue.length = 0;
2748
+ this.clearCanvas();
2749
+ }
2750
+ handlePointerDown(e) {
2751
+ if (!this.enabled) {
2752
+ return;
2753
+ }
2754
+ try {
2755
+ this.container.setPointerCapture(e.pointerId);
2756
+ } catch {
2757
+ }
2758
+ this.resizeCanvas();
2759
+ if (this.activePointers.size === 0) {
2760
+ this.segQueue.length = 0;
2761
+ this.clearCanvas();
2762
+ }
2763
+ this.activePointers.set(e.pointerId, {
2764
+ pointerId: e.pointerId,
2765
+ clientX: e.clientX,
2766
+ clientY: e.clientY
2767
+ });
2768
+ const targetPoint = this.toTargetPoint(e);
2769
+ if (targetPoint) {
2770
+ this.lastTargetPoints.set(e.pointerId, targetPoint);
2771
+ }
2772
+ this.startDrawLoop();
2773
+ this.startSampling();
2774
+ }
2775
+ handlePointerMove(e) {
2776
+ if (!this.enabled || !this.activePointers.has(e.pointerId)) {
2777
+ return;
2778
+ }
2779
+ const nextTargetPoint = this.toTargetPoint(e);
2780
+ const last = this.lastTargetPoints.get(e.pointerId);
2781
+ if (last && nextTargetPoint) {
2782
+ this.segQueue.push({ a: last, b: nextTargetPoint });
2783
+ }
2784
+ if (nextTargetPoint) {
2785
+ this.lastTargetPoints.set(e.pointerId, nextTargetPoint);
2786
+ }
2787
+ this.activePointers.set(e.pointerId, {
2788
+ pointerId: e.pointerId,
2789
+ clientX: e.clientX,
2790
+ clientY: e.clientY
2791
+ });
2792
+ }
2793
+ handlePointerUp(e) {
2794
+ if (!this.activePointers.has(e.pointerId)) {
2795
+ return;
2796
+ }
2797
+ this.activePointers.delete(e.pointerId);
2798
+ this.lastTargetPoints.delete(e.pointerId);
2799
+ try {
2800
+ this.container.releasePointerCapture(e.pointerId);
2801
+ } catch {
2802
+ }
2803
+ if (this.activePointers.size === 0) {
2804
+ this.finishStroke();
2805
+ }
2806
+ }
2807
+ handlePointerCancel(e) {
2808
+ if (!this.activePointers.has(e.pointerId)) {
2809
+ return;
2810
+ }
2811
+ this.activePointers.delete(e.pointerId);
2812
+ this.lastTargetPoints.delete(e.pointerId);
2813
+ if (this.activePointers.size === 0) {
2814
+ this.finishStroke();
2815
+ }
2816
+ }
2817
+ };
2818
+ function createDragTrackController(container, options) {
2819
+ return new DragTrackController(container, options);
2820
+ }
2821
+
2822
+ // src/drag/remote-view-host.ts
2823
+ function createRemoteViewHost(wrapper) {
2824
+ const computed = getComputedStyle(wrapper);
2825
+ if (computed.position === "static") {
2826
+ wrapper.style.position = "relative";
2827
+ }
2828
+ const videoHost = document.createElement("div");
2829
+ videoHost.dataset.XmaxRemoteVideo = "true";
2830
+ videoHost.style.cssText = "position:relative;z-index:0;width:100%;height:100%;overflow:hidden;display:flex;align-items:center;justify-content:center;";
2831
+ const dragHost = document.createElement("div");
2832
+ dragHost.dataset.XmaxDragSurface = "true";
2833
+ dragHost.style.cssText = "position:absolute;inset:0;z-index:20;touch-action:none;";
2834
+ wrapper.replaceChildren();
2835
+ wrapper.appendChild(videoHost);
2836
+ wrapper.appendChild(dragHost);
2837
+ return { wrapper, videoHost, dragHost };
2838
+ }
2839
+
2840
+ // src/shared/ref-image.ts
2841
+ async function resolveRefImageUrl(client, input) {
2842
+ if (input instanceof File) {
2843
+ return (await client.uploadImage(input)).url;
2844
+ }
2845
+ const raw = input.trim();
2846
+ if (!raw) {
2847
+ return "";
2848
+ }
2849
+ if (/^https?:\/\//i.test(raw)) {
2850
+ return raw;
2851
+ }
2852
+ const res = await fetch(raw);
2853
+ if (!res.ok) {
2854
+ throw new Error(`Failed to load ref image: ${res.status}`);
2855
+ }
2856
+ const blob = await res.blob();
2857
+ const mime = blob.type || "image/png";
2858
+ const ext = mime === "image/jpeg" ? "jpg" : mime === "image/webp" ? "webp" : mime === "image/gif" ? "gif" : "png";
2859
+ const file = new File([blob], `ref.${ext}`, { type: mime });
2860
+ return (await client.uploadImage(file)).url;
2861
+ }
2862
+ var DEFAULT_SESSION_TARGET_SIZE = [1472, 832];
2863
+ var UPLOAD_VIDEO_BASE = 832;
2864
+ var UPLOAD_SIZE_ALIGN = 32;
2865
+ var MAX_LANDSCAPE_ASPECT = 16 / 9;
2866
+ var MAX_PORTRAIT_HEIGHT_RATIO = 16 / 9;
2867
+ function alignToSizeMultiple(value) {
2868
+ return Math.max(UPLOAD_SIZE_ALIGN, Math.round(value / UPLOAD_SIZE_ALIGN) * UPLOAD_SIZE_ALIGN);
2869
+ }
2870
+ function resolveUploadVideoSize(sourceWidth, sourceHeight) {
2871
+ if (sourceWidth <= 0 || sourceHeight <= 0) {
2872
+ return DEFAULT_SESSION_TARGET_SIZE;
2873
+ }
2874
+ if (sourceWidth === sourceHeight) {
2875
+ return [UPLOAD_VIDEO_BASE, UPLOAD_VIDEO_BASE];
2876
+ }
2877
+ const aspect = sourceWidth / sourceHeight;
2878
+ if (sourceHeight > sourceWidth) {
2879
+ const width2 = UPLOAD_VIDEO_BASE;
2880
+ let height2 = alignToSizeMultiple(width2 / aspect);
2881
+ const maxHeight = alignToSizeMultiple(width2 * MAX_PORTRAIT_HEIGHT_RATIO);
2882
+ if (height2 > maxHeight) {
2883
+ height2 = maxHeight;
2884
+ }
2885
+ return [width2, height2];
2886
+ }
2887
+ const height = UPLOAD_VIDEO_BASE;
2888
+ let width = alignToSizeMultiple(height * aspect);
2889
+ const maxWidth = alignToSizeMultiple(height * MAX_LANDSCAPE_ASPECT);
2890
+ if (width > maxWidth) {
2891
+ width = maxWidth;
2892
+ }
2893
+ return [width, height];
2894
+ }
2895
+ function resolveSessionTargetSize(input) {
2896
+ if (input.overrideSize) {
2897
+ return input.overrideSize;
2898
+ }
2899
+ if (input.localVideoSize?.width && input.localVideoSize?.height) {
2900
+ return [input.localVideoSize.width, input.localVideoSize.height];
2901
+ }
2902
+ return DEFAULT_SESSION_TARGET_SIZE;
2903
+ }
2904
+
2905
+ // src/realtime/initial-state.ts
2906
+ var DEFAULT_RTC_START_MODEL = "default";
2907
+ function resolveGenerationModel(model) {
2908
+ const trimmed = model?.trim();
2909
+ return trimmed || DEFAULT_RTC_START_MODEL;
2910
+ }
2911
+ function resolvePromptText(prompt) {
2912
+ if (typeof prompt === "string") {
2913
+ return prompt.trim();
2914
+ }
2915
+ return prompt?.text?.trim() ?? "";
2916
+ }
2917
+ function resolveInitialRefImage(initialState) {
2918
+ if (!initialState) {
2919
+ return void 0;
2920
+ }
2921
+ if (typeof initialState.refImage === "string" && initialState.refImage.trim()) {
2922
+ return initialState.refImage.trim();
2923
+ }
2924
+ if (typeof initialState.image === "string" && initialState.image.trim()) {
2925
+ return initialState.image.trim();
2926
+ }
2927
+ return void 0;
2928
+ }
2929
+ function shouldAutoStartOnConnect(initialState) {
2930
+ return initialState?.autoStart === true;
2931
+ }
2932
+
2933
+ // src/rtc/publish-fps.ts
2934
+ var MOBILE_PUBLISH_MAX_WIDTH_PX = 768;
2935
+ var RTC_PUBLISH_FPS_WEB = 30;
2936
+ var RTC_PUBLISH_FPS_MOBILE = 24;
2937
+ function isMobilePublishEnvironment() {
2938
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
2939
+ return false;
2940
+ }
2941
+ return window.matchMedia(`(max-width: ${MOBILE_PUBLISH_MAX_WIDTH_PX}px)`).matches;
2942
+ }
2943
+ function resolveRtcPublishFps(options = {}) {
2944
+ if (typeof options.fps === "number" && Number.isFinite(options.fps) && options.fps > 0) {
2945
+ return options.fps;
2946
+ }
2947
+ const mobile = options.mobile ?? isMobilePublishEnvironment();
2948
+ return mobile ? RTC_PUBLISH_FPS_MOBILE : RTC_PUBLISH_FPS_WEB;
2949
+ }
2950
+
2951
+ // src/realtime/prewarm.ts
2952
+ var RealtimePrewarmHandle = class _RealtimePrewarmHandle {
2953
+ constructor(client, rtc, sessionUid, modelName) {
2954
+ this.client = client;
2955
+ this.rtc = rtc;
2956
+ this.sessionUid = sessionUid;
2957
+ this.consumed = false;
2958
+ this.disposed = false;
2959
+ this.modelName = modelName;
2960
+ }
2961
+ static async create(model, clientOptions) {
2962
+ const modelName = model.name.trim();
2963
+ if (!modelName) {
2964
+ throw new Error("model.name is required");
2965
+ }
2966
+ const sdkI18n = createSDKI18n({});
2967
+ const client = new XmaxOpenClient({
2968
+ apiKey: clientOptions.apiKey,
2969
+ authToken: clientOptions.authToken,
2970
+ baseUrl: clientOptions.baseUrl ?? XMAX_OPEN_API_PRODUCTION_BASE_URL,
2971
+ onError: clientOptions.onError
2972
+ });
2973
+ const rtc = new RtcManager({
2974
+ onLog: (entry) => clientOptions.onLog?.({ scope: "rtc", ...entry }),
2975
+ onRemoteSeiReceived: clientOptions.onRemoteSei,
2976
+ onSeiGateChange: clientOptions.onSeiGateChange,
2977
+ i18n: sdkI18n
2978
+ });
2979
+ const createdSession = await client.createSession(modelName);
2980
+ const sessionUid = createdSession.sessionUid?.trim();
2981
+ if (!sessionUid) {
2982
+ throw new Error("Session uid is missing");
2983
+ }
2984
+ const joinInfo = client.getRtcJoinInfo(createdSession);
2985
+ await rtc.join(joinInfo);
2986
+ const handle = new _RealtimePrewarmHandle(client, rtc, sessionUid, modelName);
2987
+ client.startHeartbeat(
2988
+ sessionUid,
2989
+ clientOptions.heartbeatIntervalMs ?? 1e4,
2990
+ (heartbeatSession) => {
2991
+ if (heartbeatSession.status !== ACTIVE_STATUS) {
2992
+ void handle.cancel();
2993
+ }
2994
+ },
2995
+ () => {
2996
+ void handle.cancel();
2997
+ }
2998
+ );
2999
+ return handle;
3000
+ }
3001
+ consume(expectedModelName) {
3002
+ const normalized = expectedModelName.trim();
3003
+ if (this.consumed || this.disposed || !normalized || this.modelName !== normalized) {
3004
+ return null;
3005
+ }
3006
+ this.consumed = true;
3007
+ return {
3008
+ client: this.client,
3009
+ rtc: this.rtc,
3010
+ sessionUid: this.sessionUid
3011
+ };
3012
+ }
3013
+ async cancel() {
3014
+ if (this.consumed || this.disposed) {
3015
+ return;
3016
+ }
3017
+ this.disposed = true;
3018
+ this.client.beginTeardown();
3019
+ try {
3020
+ await this.client.closeSession(this.sessionUid);
3021
+ } catch {
3022
+ }
3023
+ try {
3024
+ await this.rtc.leave();
3025
+ } catch {
3026
+ }
3027
+ this.client.endTeardown();
3028
+ }
3029
+ };
3030
+
3031
+ // src/realtime/session.ts
3032
+ function resolvePromptInput(prompt) {
3033
+ if (typeof prompt === "string") {
3034
+ return prompt.trim();
3035
+ }
3036
+ return prompt?.text?.trim() ?? "";
3037
+ }
3038
+ function resolveRefImagePath(refImage) {
3039
+ const trimmed = refImage?.trim();
3040
+ return trimmed || void 0;
3041
+ }
3042
+ function getVideoTrack(stream) {
3043
+ const track = stream.getVideoTracks()[0];
3044
+ if (!track) {
3045
+ throw new Error("Missing video track in MediaStream");
3046
+ }
3047
+ return track;
3048
+ }
3049
+ function readVideoSize(track) {
3050
+ const settings = track.getSettings?.();
3051
+ if (typeof settings?.width === "number" && settings.width > 0 && typeof settings?.height === "number" && settings.height > 0) {
3052
+ return { width: settings.width, height: settings.height };
3053
+ }
3054
+ return null;
3055
+ }
3056
+ var RealtimeSession = class _RealtimeSession {
3057
+ constructor(client, rtc, reportError, options, clientOptions) {
3058
+ this.sessionUid = null;
3059
+ this.taskUid = null;
3060
+ this.localVideoSize = null;
3061
+ this.currentPrompt = "";
3062
+ this.currentGenerationModel = "";
3063
+ this.currentStaticGenerate = void 0;
3064
+ this.disconnected = false;
3065
+ this.remoteViewHost = null;
3066
+ this.dragController = null;
3067
+ this.dragTrackFitMode = "cover";
3068
+ this.sessionTargetSize = null;
3069
+ this.mirroredInputStream = null;
3070
+ this.hiddenRemoteElement = null;
3071
+ this.remoteStreamNotifyTimer = null;
3072
+ this.lastNotifiedRemoteStream = null;
3073
+ this.dragOption = void 0;
3074
+ this.client = client;
3075
+ this.rtc = rtc;
3076
+ this.reportError = reportError;
3077
+ this.onRemoteStream = options.onRemoteStream;
3078
+ this.onPublishPipelineWaitChange = clientOptions?.onPublishPipelineWaitChange;
3079
+ if (options.remoteElement) {
3080
+ this.remoteElement = options.remoteElement;
3081
+ if (options.drag !== false) {
3082
+ this.remoteViewHost = createRemoteViewHost(this.remoteElement);
3083
+ }
3084
+ return;
3085
+ }
3086
+ if (options.onRemoteStream) {
3087
+ const hidden = document.createElement("div");
3088
+ hidden.setAttribute("data-Xmax-remote-host", "true");
3089
+ hidden.style.cssText = "position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;overflow:hidden";
3090
+ document.body.appendChild(hidden);
3091
+ this.hiddenRemoteElement = hidden;
3092
+ this.remoteElement = hidden;
3093
+ this.remoteViewHost = createRemoteViewHost(hidden);
3094
+ this.rtc.setOnRemotePlayerMounted(() => {
3095
+ this.scheduleNotifyRemoteStream();
3096
+ });
3097
+ return;
3098
+ }
3099
+ this.remoteElement = null;
3100
+ }
3101
+ get remoteVideoContainer() {
3102
+ return this.remoteViewHost?.videoHost ?? this.remoteElement;
3103
+ }
3104
+ static async connect(stream, options, clientOptions) {
3105
+ const sdkI18n = createSDKI18n({});
3106
+ const notifyUserError = clientOptions.onError;
3107
+ const reportError = (error, fallbackMessage) => {
3108
+ throw notifyError(error, notifyUserError, fallbackMessage ?? sdkI18n.t("errors.requestFailed"));
3109
+ };
3110
+ const modelName = options.model.name.trim();
3111
+ const prewarmed = options.prewarmed instanceof RealtimePrewarmHandle ? options.prewarmed.consume(modelName) : null;
3112
+ const client = prewarmed?.client ?? new XmaxOpenClient({
3113
+ apiKey: clientOptions.apiKey,
3114
+ authToken: clientOptions.authToken,
3115
+ baseUrl: clientOptions.baseUrl ?? XMAX_OPEN_API_PRODUCTION_BASE_URL,
3116
+ onError: notifyUserError
3117
+ });
3118
+ const rtc = prewarmed?.rtc ?? new RtcManager({
3119
+ onLog: (entry) => clientOptions.onLog?.({ scope: "rtc", ...entry }),
3120
+ onRemoteSeiReceived: clientOptions.onRemoteSei,
3121
+ onSeiGateChange: clientOptions.onSeiGateChange,
3122
+ i18n: sdkI18n
3123
+ });
3124
+ const session = new _RealtimeSession(client, rtc, reportError, options, clientOptions);
3125
+ try {
3126
+ await session.bootstrap(stream, options, clientOptions.heartbeatIntervalMs ?? 1e4, prewarmed);
3127
+ return session;
3128
+ } catch (error) {
3129
+ await session.safeDisconnect();
3130
+ throw reportError(error);
3131
+ }
3132
+ }
3133
+ async bootstrap(stream, options, heartbeatIntervalMs, prewarmed = null) {
3134
+ this.dragOption = options.drag;
3135
+ const modelName = options.model.name.trim();
3136
+ if (!modelName) {
3137
+ throw new Error("model.name is required");
3138
+ }
3139
+ if (prewarmed) {
3140
+ this.sessionUid = prewarmed.sessionUid;
3141
+ } else {
3142
+ const createdSession = await this.client.createSession(modelName);
3143
+ if (!createdSession.sessionUid?.trim()) {
3144
+ throw new Error("Session uid is missing");
3145
+ }
3146
+ this.sessionUid = createdSession.sessionUid;
3147
+ this.client.startHeartbeat(
3148
+ createdSession.sessionUid,
3149
+ heartbeatIntervalMs,
3150
+ (heartbeatSession) => {
3151
+ if (heartbeatSession.status !== ACTIVE_STATUS) {
3152
+ void this.safeDisconnect();
3153
+ }
3154
+ },
3155
+ () => {
3156
+ void this.safeDisconnect();
3157
+ }
3158
+ );
3159
+ const joinInfo = this.client.getRtcJoinInfo(createdSession);
3160
+ await this.rtc.join(joinInfo);
3161
+ }
3162
+ if (options.localElement) {
3163
+ this.rtc.setLocalVideoContainer(options.localElement);
3164
+ }
3165
+ if (this.remoteVideoContainer) {
3166
+ this.rtc.setRemoteVideoContainer(this.remoteVideoContainer);
3167
+ }
3168
+ this.sessionSizeOverride = options.size;
3169
+ const videoTrack = getVideoTrack(stream);
3170
+ const isFileInput = options.inputKind === "file";
3171
+ this.dragTrackFitMode = isFileInput ? "contain" : "cover";
3172
+ const rawVideoSize = readVideoSize(videoTrack);
3173
+ const publishSize = options.size ?? (rawVideoSize ? resolveUploadVideoSize(rawVideoSize.width, rawVideoSize.height) : DEFAULT_SESSION_TARGET_SIZE);
3174
+ let publishTrack = videoTrack;
3175
+ if (isFileInput) {
3176
+ if (options.size) {
3177
+ this.localVideoSize = { width: options.size[0], height: options.size[1] };
3178
+ } else if (rawVideoSize) {
3179
+ this.localVideoSize = { width: rawVideoSize.width, height: rawVideoSize.height };
3180
+ } else {
3181
+ this.localVideoSize = null;
3182
+ }
3183
+ } else if (options.prebuiltCameraStream) {
3184
+ this.mirroredInputStream = options.prebuiltCameraStream;
3185
+ publishTrack = options.prebuiltCameraStream.videoTrack;
3186
+ this.localVideoSize = {
3187
+ width: options.prebuiltCameraStream.width,
3188
+ height: options.prebuiltCameraStream.height
3189
+ };
3190
+ } else {
3191
+ const processedStream = await createNativeCameraStream(stream);
3192
+ this.mirroredInputStream = processedStream;
3193
+ publishTrack = processedStream.videoTrack;
3194
+ this.localVideoSize = { width: processedStream.width, height: processedStream.height };
3195
+ }
3196
+ const initialState = options.initialState;
3197
+ this.currentPrompt = resolvePromptText(initialState?.prompt);
3198
+ this.currentGenerationModel = resolveGenerationModel(initialState?.model);
3199
+ this.currentStaticGenerate = initialState?.staticGenerate;
3200
+ const publishFpsOptions = {
3201
+ mobile: options.mobile,
3202
+ highQuality: true,
3203
+ fps: isFileInput ? options.model.fps : resolveRtcPublishFps({ mobile: options.mobile })
3204
+ };
3205
+ await this.rtc.startExternalVideoPublishing(publishTrack, publishSize, publishFpsOptions);
3206
+ let initialRefImageUrl;
3207
+ const initialRefImage = resolveInitialRefImage(initialState);
3208
+ if (initialRefImage) {
3209
+ initialRefImageUrl = await resolveRefImageUrl(this.client, initialRefImage);
3210
+ } else if (initialState?.image instanceof File) {
3211
+ initialRefImageUrl = await resolveRefImageUrl(this.client, initialState.image);
3212
+ } else if (initialState?.image instanceof Blob) {
3213
+ const mime = initialState.image.type || "image/png";
3214
+ const file = new File([initialState.image], "ref-image", { type: mime });
3215
+ initialRefImageUrl = await resolveRefImageUrl(this.client, file);
3216
+ }
3217
+ if (options.localElement) {
3218
+ queueMicrotask(() => this.refreshLocalPreview());
3219
+ }
3220
+ if (shouldAutoStartOnConnect(initialState)) {
3221
+ await this.runStart({
3222
+ prompt: this.currentPrompt || void 0,
3223
+ model: this.currentGenerationModel || void 0,
3224
+ size: options.size,
3225
+ refImage: initialRefImageUrl,
3226
+ staticGenerate: this.currentStaticGenerate
3227
+ });
3228
+ }
3229
+ }
3230
+ async setPrompt(prompt, _options) {
3231
+ try {
3232
+ const promptText = resolvePromptInput(prompt);
3233
+ this.currentPrompt = promptText;
3234
+ if (!this.taskUid) {
3235
+ await this.runStart({
3236
+ prompt: promptText || void 0
3237
+ });
3238
+ return;
3239
+ }
3240
+ await this.restart({ prompt: promptText });
3241
+ } catch (error) {
3242
+ this.reportError(error);
3243
+ }
3244
+ }
3245
+ async set(input) {
3246
+ try {
3247
+ let refImage;
3248
+ if (input.image === null) {
3249
+ refImage = void 0;
3250
+ } else if (input.image !== void 0) {
3251
+ refImage = await this.setRefImage(input.image);
3252
+ }
3253
+ const prompt = input.prompt !== void 0 ? resolvePromptInput(input.prompt) : void 0;
3254
+ if (prompt !== void 0) {
3255
+ this.currentPrompt = prompt;
3256
+ }
3257
+ await this.restart({
3258
+ prompt,
3259
+ refImage
3260
+ });
3261
+ } catch (error) {
3262
+ this.reportError(error);
3263
+ }
3264
+ }
3265
+ async setImage(image, options) {
3266
+ try {
3267
+ const refImage = await this.setRefImage(image);
3268
+ const prompt = options?.prompt ? resolvePromptInput(options.prompt) : void 0;
3269
+ if (prompt !== void 0) {
3270
+ this.currentPrompt = prompt;
3271
+ }
3272
+ await this.restart({
3273
+ prompt,
3274
+ refImage
3275
+ });
3276
+ } catch (error) {
3277
+ this.reportError(error);
3278
+ }
3279
+ }
3280
+ async setRefImage(input) {
3281
+ try {
3282
+ const resolved = await resolveRefImageUrl(this.client, input);
3283
+ return resolved;
3284
+ } catch (error) {
3285
+ this.reportError(error);
3286
+ }
3287
+ }
3288
+ async restart(options) {
3289
+ try {
3290
+ let resolvedRefImage;
3291
+ if (options && "refImage" in options) {
3292
+ const ref = options.refImage;
3293
+ if (ref === null || ref === "") {
3294
+ resolvedRefImage = void 0;
3295
+ } else if (typeof ref === "string" && ref.trim()) {
3296
+ resolvedRefImage = await this.setRefImage(ref.trim());
3297
+ }
3298
+ }
3299
+ if (options?.prompt !== void 0) {
3300
+ this.currentPrompt = options.prompt.trim();
3301
+ }
3302
+ if (options?.model !== void 0) {
3303
+ this.currentGenerationModel = resolveGenerationModel(options.model);
3304
+ }
3305
+ if (options?.staticGenerate !== void 0) {
3306
+ this.currentStaticGenerate = options.staticGenerate;
3307
+ }
3308
+ await this.runStart({
3309
+ prompt: this.currentPrompt || void 0,
3310
+ model: this.currentGenerationModel || void 0,
3311
+ size: options?.size,
3312
+ refImage: resolvedRefImage,
3313
+ staticGenerate: this.currentStaticGenerate
3314
+ });
3315
+ } catch (error) {
3316
+ this.reportError(error);
3317
+ }
3318
+ }
3319
+ async setMode(options) {
3320
+ return this.restart(options);
3321
+ }
3322
+ async refreshRemoteOutput(options) {
3323
+ if (!this.remoteVideoContainer) {
3324
+ return;
3325
+ }
3326
+ this.rtc.setRemoteVideoContainer(this.remoteVideoContainer);
3327
+ await this.rtc.refreshRemoteVideoBinding(options);
3328
+ this.scheduleNotifyRemoteStream();
3329
+ }
3330
+ refreshLocalPreview() {
3331
+ this.rtc.refreshLocalVideoPreview();
3332
+ }
3333
+ getInputPreviewStream() {
3334
+ return this.mirroredInputStream?.previewStream ?? null;
3335
+ }
3336
+ detachRemoteOutput() {
3337
+ this.rtc.setRemoteVideoContainer(null);
3338
+ }
3339
+ async sendTracks(tracks) {
3340
+ if (!this.rtc.snapshot.joined || !this.taskUid) {
3341
+ return;
3342
+ }
3343
+ try {
3344
+ await this.rtc.sendRoomEvent(
3345
+ createTracksRtcRoomEvent({
3346
+ userId: this.rtc.snapshot.userId ?? void 0,
3347
+ uid: this.taskUid,
3348
+ sessionUid: this.taskUid,
3349
+ tracks
3350
+ })
3351
+ );
3352
+ } catch {
3353
+ }
3354
+ }
3355
+ async stop(options) {
3356
+ this.updateDragController(false);
3357
+ const preserveRemoteDom = options?.preserveRemoteDom ?? false;
3358
+ if (preserveRemoteDom) {
3359
+ this.rtc.setPreserveRemoteDom(true);
3360
+ }
3361
+ if (!this.rtc.snapshot.joined) {
3362
+ this.rtc.clearRemoteVideo({ preserveDom: preserveRemoteDom });
3363
+ if (!preserveRemoteDom && this.remoteVideoContainer) {
3364
+ this.rtc.setRemoteVideoContainer(null);
3365
+ }
3366
+ this.taskUid = null;
3367
+ if (preserveRemoteDom) {
3368
+ this.rtc.setPreserveRemoteDom(false);
3369
+ }
3370
+ return;
3371
+ }
3372
+ try {
3373
+ await this.rtc.sendRoomEvent(
3374
+ createStopRtcRoomEvent({
3375
+ userId: this.rtc.snapshot.userId ?? void 0,
3376
+ sessionUid: this.taskUid ?? void 0
3377
+ })
3378
+ );
3379
+ } finally {
3380
+ this.rtc.clearRemoteVideo({ preserveDom: preserveRemoteDom });
3381
+ if (!preserveRemoteDom && this.remoteVideoContainer) {
3382
+ this.rtc.setRemoteVideoContainer(null);
3383
+ }
3384
+ this.taskUid = null;
3385
+ if (preserveRemoteDom) {
3386
+ this.rtc.setPreserveRemoteDom(false);
3387
+ }
3388
+ }
3389
+ }
3390
+ async disconnect(options) {
3391
+ if (this.disconnected) {
3392
+ return;
3393
+ }
3394
+ this.disconnected = true;
3395
+ await this.safeDisconnect(options);
3396
+ }
3397
+ getSessionUid() {
3398
+ return this.sessionUid;
3399
+ }
3400
+ async runStart(options) {
3401
+ if (!this.rtc.snapshot.joined) {
3402
+ return;
3403
+ }
3404
+ const startPrompt = options.prompt?.trim() ?? this.currentPrompt;
3405
+ if (options.model !== void 0) {
3406
+ this.currentGenerationModel = resolveGenerationModel(options.model);
3407
+ }
3408
+ if (options.staticGenerate !== void 0) {
3409
+ this.currentStaticGenerate = options.staticGenerate;
3410
+ }
3411
+ const modelLabel = resolveGenerationModel(this.currentGenerationModel);
3412
+ await this.rtc.ensurePublishedInputBeforeStart();
3413
+ this.rtc.setRemoteVideoContainer(null);
3414
+ this.rtc.clearRemoteVideo();
3415
+ const size = resolveSessionTargetSize({
3416
+ localVideoSize: this.localVideoSize,
3417
+ overrideSize: options.size ?? this.sessionSizeOverride
3418
+ });
3419
+ this.sessionTargetSize = size;
3420
+ const taskUid = createTaskUid();
3421
+ this.taskUid = taskUid;
3422
+ this.rtc.configureSessionSei(taskUid);
3423
+ try {
3424
+ if (this.remoteVideoContainer) {
3425
+ this.rtc.setRemoteVideoContainer(this.remoteVideoContainer);
3426
+ }
3427
+ const startRoomEvent = createStartRtcRoomEvent({
3428
+ userId: this.rtc.snapshot.userId ?? void 0,
3429
+ uid: taskUid,
3430
+ sessionUid: taskUid,
3431
+ model: modelLabel,
3432
+ size,
3433
+ prompt: startPrompt,
3434
+ refImagePath: resolveRefImagePath(options.refImage),
3435
+ staticGenerate: this.currentStaticGenerate
3436
+ });
3437
+ console.log("[Xmax SDK] \u771F\u5B9E\u53D1\u9001 start ->", startRoomEvent);
3438
+ await this.rtc.sendRoomEvent(startRoomEvent);
3439
+ await this.rtc.refreshRemoteVideoBinding({ force: true });
3440
+ this.updateDragController(true);
3441
+ this.scheduleNotifyRemoteStream();
3442
+ } catch (error) {
3443
+ this.rtc.configureSessionSei(null);
3444
+ throw error;
3445
+ }
3446
+ }
3447
+ isDragEnabled() {
3448
+ if (this.dragOption === false) {
3449
+ return false;
3450
+ }
3451
+ if (this.dragOption === true) {
3452
+ return true;
3453
+ }
3454
+ return Boolean(this.remoteElement && !this.hiddenRemoteElement);
3455
+ }
3456
+ updateDragController(active) {
3457
+ const dragHost = this.remoteViewHost?.dragHost;
3458
+ if (!dragHost || !active || !this.taskUid || !this.isDragEnabled()) {
3459
+ this.dragController?.setEnabled(false);
3460
+ return;
3461
+ }
3462
+ const targetSize = this.sessionTargetSize ?? DEFAULT_SESSION_TARGET_SIZE;
3463
+ if (!this.dragController) {
3464
+ this.dragController = createDragTrackController(dragHost, {
3465
+ targetSize,
3466
+ fitMode: this.dragTrackFitMode,
3467
+ mirrored: false,
3468
+ enabled: true,
3469
+ onTracks: (tracks) => {
3470
+ const media = this.remoteViewHost?.videoHost?.querySelector("video, canvas");
3471
+ const mediaRect = media?.getBoundingClientRect();
3472
+ const dragRect = dragHost.getBoundingClientRect();
3473
+ console.log("[Xmax] sendTracks", {
3474
+ taskUid: this.taskUid,
3475
+ targetSize: this.sessionTargetSize,
3476
+ fitMode: this.dragTrackFitMode,
3477
+ tracks,
3478
+ dragRect: dragRect ? { w: dragRect.width, h: dragRect.height } : null,
3479
+ mediaRect: mediaRect ? { w: mediaRect.width, h: mediaRect.height, l: mediaRect.left, t: mediaRect.top } : null
3480
+ });
3481
+ void this.sendTracks(tracks);
3482
+ }
3483
+ });
3484
+ return;
3485
+ }
3486
+ this.dragController.setTargetSize(targetSize);
3487
+ this.dragController.setFitMode(this.dragTrackFitMode);
3488
+ this.dragController.setMirrored(false);
3489
+ this.dragController.setEnabled(true);
3490
+ }
3491
+ scheduleNotifyRemoteStream() {
3492
+ if (!this.onRemoteStream) {
3493
+ return;
3494
+ }
3495
+ this.clearRemoteStreamNotifyTimer();
3496
+ if (this.notifyRemoteStreamOnce()) {
3497
+ return;
3498
+ }
3499
+ let attempts = 0;
3500
+ this.remoteStreamNotifyTimer = setInterval(() => {
3501
+ attempts += 1;
3502
+ if (this.notifyRemoteStreamOnce() || attempts >= 60) {
3503
+ this.clearRemoteStreamNotifyTimer();
3504
+ }
3505
+ }, 500);
3506
+ }
3507
+ notifyRemoteStreamOnce() {
3508
+ const videoRoot = this.remoteVideoContainer;
3509
+ if (!this.onRemoteStream || !videoRoot) {
3510
+ return false;
3511
+ }
3512
+ const video = videoRoot instanceof HTMLVideoElement ? videoRoot : videoRoot.querySelector("video");
3513
+ if (!(video instanceof HTMLVideoElement) || !(video.srcObject instanceof MediaStream)) {
3514
+ return false;
3515
+ }
3516
+ if (this.lastNotifiedRemoteStream === video.srcObject) {
3517
+ return true;
3518
+ }
3519
+ this.lastNotifiedRemoteStream = video.srcObject;
3520
+ this.onRemoteStream(video.srcObject);
3521
+ return true;
3522
+ }
3523
+ clearRemoteStreamNotifyTimer() {
3524
+ if (this.remoteStreamNotifyTimer !== null) {
3525
+ clearInterval(this.remoteStreamNotifyTimer);
3526
+ this.remoteStreamNotifyTimer = null;
3527
+ }
3528
+ }
3529
+ async safeDisconnect(options) {
3530
+ this.clearRemoteStreamNotifyTimer();
3531
+ this.lastNotifiedRemoteStream = null;
3532
+ const activeSessionUid = this.sessionUid;
3533
+ this.sessionUid = null;
3534
+ this.client.beginTeardown();
3535
+ if (activeSessionUid) {
3536
+ try {
3537
+ await this.client.closeSession(activeSessionUid);
3538
+ } catch {
3539
+ }
3540
+ }
3541
+ const generationActive = !!this.taskUid && this.rtc.snapshot.joined;
3542
+ try {
3543
+ if (generationActive) {
3544
+ await this.stop();
3545
+ } else {
3546
+ this.updateDragController(false);
3547
+ this.rtc.clearRemoteVideo();
3548
+ if (this.remoteVideoContainer) {
3549
+ this.rtc.setRemoteVideoContainer(null);
3550
+ }
3551
+ this.taskUid = null;
3552
+ }
3553
+ } catch {
3554
+ }
3555
+ try {
3556
+ if (this.rtc.snapshot.joined) {
3557
+ await this.rtc.stopVideoPublishing();
3558
+ }
3559
+ } catch {
3560
+ }
3561
+ try {
3562
+ if (this.rtc.snapshot.joined) {
3563
+ await this.rtc.leave();
3564
+ }
3565
+ } catch {
3566
+ }
3567
+ this.client.endTeardown();
3568
+ this.taskUid = null;
3569
+ this.currentPrompt = "";
3570
+ this.currentGenerationModel = "";
3571
+ this.currentStaticGenerate = void 0;
3572
+ this.sessionTargetSize = null;
3573
+ this.dragController?.destroy();
3574
+ this.dragController = null;
3575
+ this.mirroredInputStream?.destroy();
3576
+ this.mirroredInputStream = null;
3577
+ if (this.hiddenRemoteElement) {
3578
+ this.hiddenRemoteElement.remove();
3579
+ this.hiddenRemoteElement = null;
3580
+ }
3581
+ }
3582
+ };
3583
+
3584
+ // src/realtime/models.ts
3585
+ var DEFAULT_REALTIME_SIZE = {
3586
+ width: 1280,
3587
+ height: 960,
3588
+ fps: 24
3589
+ };
3590
+ var models = {
3591
+ realtime(name, options) {
3592
+ return {
3593
+ name,
3594
+ width: options?.width ?? DEFAULT_REALTIME_SIZE.width,
3595
+ height: options?.height ?? DEFAULT_REALTIME_SIZE.height,
3596
+ fps: options?.fps ?? DEFAULT_REALTIME_SIZE.fps
3597
+ };
3598
+ }
3599
+ };
3600
+
3601
+ // src/realtime/create-client.ts
3602
+ function resolveClientOptions(options) {
3603
+ return {
3604
+ ...options,
3605
+ baseUrl: options.baseUrl?.trim() || XMAX_OPEN_API_PRODUCTION_BASE_URL
3606
+ };
3607
+ }
3608
+ function createXmaxClient(options) {
3609
+ const resolved = resolveClientOptions(options);
3610
+ const httpClient = new XmaxOpenClient({
3611
+ apiKey: resolved.apiKey,
3612
+ authToken: resolved.authToken,
3613
+ baseUrl: resolved.baseUrl,
3614
+ onError: resolved.onError
3615
+ });
3616
+ return {
3617
+ files: {
3618
+ uploadImage: (file) => httpClient.uploadImage(file),
3619
+ uploadVideo: (file) => httpClient.uploadVideo(file),
3620
+ upload: (file) => httpClient.uploadImage(file)
3621
+ },
3622
+ realtime: {
3623
+ prewarm: (options2) => RealtimePrewarmHandle.create(options2.model, resolved),
3624
+ connect: (stream, connectOptions) => RealtimeSession.connect(stream, connectOptions, resolved)
3625
+ }
3626
+ };
3627
+ }
3628
+
3629
+ // src/drag/drag-video-sync.ts
3630
+ var DRAG_SYNCED_ATTR = "data-xmax-drag-synced";
3631
+ function toContentRect2(rect) {
3632
+ return {
3633
+ left: rect.left,
3634
+ top: rect.top,
3635
+ width: rect.width,
3636
+ height: rect.height
3637
+ };
3638
+ }
3639
+ function isMediaInsetInContainer2(mediaRect, containerRect) {
3640
+ if (!mediaRect.width || !mediaRect.height || !containerRect.width || !containerRect.height) {
3641
+ return false;
3642
+ }
3643
+ const widthRatio = mediaRect.width / containerRect.width;
3644
+ const heightRatio = mediaRect.height / containerRect.height;
3645
+ return widthRatio < 0.98 || heightRatio < 0.98;
3646
+ }
3647
+ function intersectContentRects(a, b) {
3648
+ const left = Math.max(a.left, b.left);
3649
+ const top = Math.max(a.top, b.top);
3650
+ const right = Math.min(a.left + a.width, b.left + b.width);
3651
+ const bottom = Math.min(a.top + a.height, b.top + b.height);
3652
+ return {
3653
+ left,
3654
+ top,
3655
+ width: Math.max(0, right - left),
3656
+ height: Math.max(0, bottom - top)
3657
+ };
3658
+ }
3659
+ function readMediaIntrinsicSize(media) {
3660
+ const video = media;
3661
+ if ("videoWidth" in video && video.videoWidth > 0 && video.videoHeight > 0) {
3662
+ return [video.videoWidth, video.videoHeight];
3663
+ }
3664
+ if (media.width > 0 && media.height > 0) {
3665
+ return [media.width, media.height];
3666
+ }
3667
+ return null;
3668
+ }
3669
+ function resolveMediaObjectFit(media, fallback) {
3670
+ if (typeof getComputedStyle !== "function") {
3671
+ return fallback;
3672
+ }
3673
+ const fit = getComputedStyle(media).objectFit;
3674
+ if (fit === "contain" || fit === "cover" || fit === "fill") {
3675
+ return fit;
3676
+ }
3677
+ return fallback;
3678
+ }
3679
+ function resolveRemoteDragLayout(input) {
3680
+ const { wrapperRect, mediaRect, media, targetSize, fitMode } = input;
3681
+ if (!wrapperRect.width || !wrapperRect.height || !mediaRect.width || !mediaRect.height) {
3682
+ return null;
3683
+ }
3684
+ const wrapper = toContentRect2(wrapperRect);
3685
+ if (fitMode === "contain") {
3686
+ const overlay2 = toContentRect2(mediaRect);
3687
+ const hitRect = intersectContentRects(overlay2, wrapper);
3688
+ const drawable2 = hitRect.width > 0 && hitRect.height > 0 ? hitRect : wrapper;
3689
+ return { overlay: drawable2, mapping: overlay2, hitRect: drawable2, source: "media" };
3690
+ }
3691
+ if (isMediaInsetInContainer2(mediaRect, wrapperRect)) {
3692
+ const overlay2 = toContentRect2(mediaRect);
3693
+ return { overlay: overlay2, mapping: overlay2, hitRect: overlay2, source: "media" };
3694
+ }
3695
+ const intrinsic = media ? readMediaIntrinsicSize(media) : null;
3696
+ const frameSize = intrinsic ?? targetSize;
3697
+ const objectFit = media ? resolveMediaObjectFit(media, fitMode) : fitMode;
3698
+ if (objectFit === "fill") {
3699
+ const overlay2 = toContentRect2(mediaRect);
3700
+ return { overlay: overlay2, mapping: overlay2, hitRect: wrapper, source: "intrinsic" };
3701
+ }
3702
+ const mapping = getFitContentRect(mediaRect, frameSize, objectFit);
3703
+ const overlay = intersectContentRects(mapping, wrapper);
3704
+ const drawable = overlay.width > 0 && overlay.height > 0 ? overlay : wrapper;
3705
+ return {
3706
+ overlay: drawable,
3707
+ mapping,
3708
+ hitRect: wrapper,
3709
+ source: intrinsic ? "intrinsic" : "fit"
3710
+ };
3711
+ }
3712
+ function resetDragSurfaceFullBleed(dragSurface) {
3713
+ dragSurface.style.inset = "0";
3714
+ dragSurface.style.left = "";
3715
+ dragSurface.style.top = "";
3716
+ dragSurface.style.right = "";
3717
+ dragSurface.style.bottom = "";
3718
+ dragSurface.style.width = "";
3719
+ dragSurface.style.height = "";
3720
+ dragSurface.removeAttribute(DRAG_SYNCED_ATTR);
3721
+ }
3722
+ function applyDragSurfaceOverlayStyles(dragSurface, overlay, wrapperRect) {
3723
+ const nextLeft = `${overlay.left - wrapperRect.left}px`;
3724
+ const nextTop = `${overlay.top - wrapperRect.top}px`;
3725
+ const nextWidth = `${overlay.width}px`;
3726
+ const nextHeight = `${overlay.height}px`;
3727
+ if (dragSurface.hasAttribute(DRAG_SYNCED_ATTR) && dragSurface.style.inset === "auto" && dragSurface.style.left === nextLeft && dragSurface.style.top === nextTop && dragSurface.style.width === nextWidth && dragSurface.style.height === nextHeight) {
3728
+ return false;
3729
+ }
3730
+ dragSurface.style.inset = "auto";
3731
+ dragSurface.style.left = nextLeft;
3732
+ dragSurface.style.top = nextTop;
3733
+ dragSurface.style.width = nextWidth;
3734
+ dragSurface.style.height = nextHeight;
3735
+ dragSurface.style.right = "";
3736
+ dragSurface.style.bottom = "";
3737
+ dragSurface.setAttribute(DRAG_SYNCED_ATTR, "");
3738
+ return true;
3739
+ }
3740
+ function syncDragSurfaceToVideo(dragSurface, targetSize, fitMode) {
3741
+ const wrapper = dragSurface.parentElement;
3742
+ if (!wrapper) {
3743
+ resetDragSurfaceFullBleed(dragSurface);
3744
+ return false;
3745
+ }
3746
+ const layout = resolveRemoteDragLayoutFromSurface(dragSurface, targetSize, fitMode);
3747
+ if (!layout || !layout.overlay.width || !layout.overlay.height) {
3748
+ resetDragSurfaceFullBleed(dragSurface);
3749
+ return false;
3750
+ }
3751
+ const wrapperRect = wrapper.getBoundingClientRect();
3752
+ const { overlay } = layout;
3753
+ return applyDragSurfaceOverlayStyles(dragSurface, overlay, wrapperRect);
3754
+ }
3755
+ function resolveRemoteDragLayoutFromSurface(dragSurface, targetSize, fitMode) {
3756
+ const wrapper = dragSurface.parentElement;
3757
+ if (!wrapper) {
3758
+ return null;
3759
+ }
3760
+ const videoHost = wrapper.querySelector("[data-xmax-remote-video]");
3761
+ if (!videoHost) {
3762
+ return null;
3763
+ }
3764
+ const media = videoHost.querySelector("video, canvas");
3765
+ if (!media) {
3766
+ return null;
3767
+ }
3768
+ return resolveRemoteDragLayout({
3769
+ wrapperRect: wrapper.getBoundingClientRect(),
3770
+ mediaRect: media.getBoundingClientRect(),
3771
+ media,
3772
+ targetSize,
3773
+ fitMode
3774
+ });
3775
+ }
3776
+ function observeDragSurfaceVideoSync(dragSurface, options) {
3777
+ const wrapper = dragSurface.parentElement;
3778
+ if (!wrapper) {
3779
+ return () => {
3780
+ };
3781
+ }
3782
+ const mediaListeners = /* @__PURE__ */ new Map();
3783
+ let rafId = null;
3784
+ let observedVideoHost = null;
3785
+ const bindMediaListeners = (media) => {
3786
+ if (mediaListeners.has(media)) {
3787
+ return;
3788
+ }
3789
+ const rerender = () => scheduleApply();
3790
+ const bindings = [];
3791
+ const add = (type) => {
3792
+ media.addEventListener(type, rerender);
3793
+ bindings.push(() => media.removeEventListener(type, rerender));
3794
+ };
3795
+ add("loadedmetadata");
3796
+ add("loadeddata");
3797
+ add("resize");
3798
+ mediaListeners.set(media, bindings);
3799
+ };
3800
+ const apply = () => {
3801
+ const changed = syncDragSurfaceToVideo(dragSurface, options.getTargetSize(), options.getFitMode());
3802
+ if (changed) {
3803
+ options.onSync?.();
3804
+ }
3805
+ const videoHost2 = wrapper.querySelector("[data-xmax-remote-video]");
3806
+ const media = videoHost2?.querySelector("video, canvas");
3807
+ if (media) {
3808
+ bindMediaListeners(media);
3809
+ }
3810
+ };
3811
+ const scheduleApply = () => {
3812
+ if (rafId !== null) {
3813
+ return;
3814
+ }
3815
+ rafId = requestAnimationFrame(() => {
3816
+ rafId = null;
3817
+ apply();
3818
+ });
3819
+ };
3820
+ const bindVideoHostObserver = (videoHostObserver) => {
3821
+ const host = wrapper.querySelector("[data-xmax-remote-video]");
3822
+ if (!host || host === observedVideoHost || !videoHostObserver) {
3823
+ return;
3824
+ }
3825
+ videoHostObserver.disconnect();
3826
+ observedVideoHost = host;
3827
+ videoHostObserver.observe(host, {
3828
+ childList: true,
3829
+ subtree: true,
3830
+ attributes: true,
3831
+ attributeFilter: ["style", "class", "width", "height"]
3832
+ });
3833
+ };
3834
+ apply();
3835
+ const resizeObserver = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => scheduleApply()) : null;
3836
+ resizeObserver?.observe(wrapper);
3837
+ const videoHost = wrapper.querySelector("[data-xmax-remote-video]");
3838
+ if (videoHost) {
3839
+ resizeObserver?.observe(videoHost);
3840
+ }
3841
+ const observeMedia = () => {
3842
+ const host = wrapper.querySelector("[data-xmax-remote-video]");
3843
+ const media = host?.querySelector("video, canvas");
3844
+ if (media) {
3845
+ resizeObserver?.observe(media);
3846
+ bindMediaListeners(media);
3847
+ }
3848
+ };
3849
+ observeMedia();
3850
+ const videoHostMutationObserver = typeof MutationObserver !== "undefined" ? new MutationObserver(() => {
3851
+ observeMedia();
3852
+ scheduleApply();
3853
+ }) : null;
3854
+ bindVideoHostObserver(videoHostMutationObserver);
3855
+ const wrapperMutationObserver = typeof MutationObserver !== "undefined" ? new MutationObserver(() => {
3856
+ bindVideoHostObserver(videoHostMutationObserver);
3857
+ observeMedia();
3858
+ scheduleApply();
3859
+ }) : null;
3860
+ wrapperMutationObserver?.observe(wrapper, {
3861
+ childList: true,
3862
+ subtree: true
3863
+ });
3864
+ const onWindowChange = () => scheduleApply();
3865
+ window.addEventListener("resize", onWindowChange);
3866
+ window.addEventListener("scroll", onWindowChange, true);
3867
+ return () => {
3868
+ if (rafId !== null) {
3869
+ cancelAnimationFrame(rafId);
3870
+ rafId = null;
3871
+ }
3872
+ resizeObserver?.disconnect();
3873
+ wrapperMutationObserver?.disconnect();
3874
+ videoHostMutationObserver?.disconnect();
3875
+ for (const unbind of Array.from(mediaListeners.values())) {
3876
+ unbind.forEach((dispose) => dispose());
3877
+ }
3878
+ mediaListeners.clear();
3879
+ window.removeEventListener("resize", onWindowChange);
3880
+ window.removeEventListener("scroll", onWindowChange, true);
3881
+ resetDragSurfaceFullBleed(dragSurface);
3882
+ };
3883
+ }
3884
+
3885
+ exports.RealtimePrewarmHandle = RealtimePrewarmHandle;
3886
+ exports.RealtimeSession = RealtimeSession;
3887
+ exports.createDragTrackController = createDragTrackController;
3888
+ exports.createRemoteViewHost = createRemoteViewHost;
3889
+ exports.createXmaxClient = createXmaxClient;
3890
+ exports.getContentRect = getContentRect;
3891
+ exports.mapTargetPointToCanvas = mapTargetPointToCanvas;
3892
+ exports.mapViewportPointToCoordinateSpace = mapViewportPointToCoordinateSpace;
3893
+ exports.models = models;
3894
+ exports.observeDragSurfaceVideoSync = observeDragSurfaceVideoSync;
3895
+ exports.syncDragSurfaceToVideo = syncDragSurfaceToVideo;
3896
+ //# sourceMappingURL=index.cjs.map
3897
+ //# sourceMappingURL=index.cjs.map