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