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