@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,817 @@
|
|
|
1
|
+
import { browserToast, notifyError, RtcManager, XmaxOpenClient, ACTIVE_STATUS, createStopRtcRoomEvent, models, requestCameraMediaStream, createNativeCameraStream, createTaskUid, createStartRtcRoomEvent, createTracksRtcRoomEvent, createDragTrackController } from '../chunk-67XSD3QJ.js';
|
|
2
|
+
import { createVideoFileStream } from '../chunk-EGNRAZ5U.js';
|
|
3
|
+
import { createSDKI18n, resolveUploadVideoSize, isMobilePublishEnvironment, resolveSessionTargetSize, DEFAULT_SESSION_TARGET_SIZE } from '../chunk-PLU2YVAS.js';
|
|
4
|
+
import { createContext, useMemo, useRef, useCallback, useState, useEffect, useContext } from 'react';
|
|
5
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
// src/shared/session-debug.ts
|
|
8
|
+
function createSessionDebugLogger(enabled, onLog) {
|
|
9
|
+
return (category, level, message, data) => {
|
|
10
|
+
if (!enabled || !onLog) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const scope = category === "rtc" || category === "sei" ? "rtc" : "client";
|
|
14
|
+
onLog({
|
|
15
|
+
scope,
|
|
16
|
+
level,
|
|
17
|
+
message: `[${category}] ${message}`,
|
|
18
|
+
data
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function seiGateChanged(prev, next) {
|
|
23
|
+
if (!prev) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return prev.matched !== next.matched || prev.expectedSei !== next.expectedSei || prev.sei !== next.sei || prev.userId !== next.userId;
|
|
27
|
+
}
|
|
28
|
+
function outputVisibilityChanged(prev, next) {
|
|
29
|
+
if (!prev) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return prev.showOutputVideo !== next.showOutputVideo || prev.outputEnabled !== next.outputEnabled || prev.hasResult !== next.hasResult || prev.remoteVideoUserId !== next.remoteVideoUserId || prev.localVideoPublished !== next.localVideoPublished || prev.inputSource !== next.inputSource || prev.seiMatched !== next.seiMatched;
|
|
33
|
+
}
|
|
34
|
+
function isVideoInputUnhealthy(health) {
|
|
35
|
+
return health.stalled || health.paused || health.ended || health.trackReadyState === "ended";
|
|
36
|
+
}
|
|
37
|
+
function videoInputHealthChanged(prev, next) {
|
|
38
|
+
if (!prev) {
|
|
39
|
+
return isVideoInputUnhealthy(next);
|
|
40
|
+
}
|
|
41
|
+
const prevBad = isVideoInputUnhealthy(prev);
|
|
42
|
+
const nextBad = isVideoInputUnhealthy(next);
|
|
43
|
+
if (prevBad !== nextBad) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (nextBad) {
|
|
47
|
+
return prev.stalled !== next.stalled || prev.paused !== next.paused || prev.ended !== next.ended || prev.trackReadyState !== next.trackReadyState;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
var XmaxContext = createContext(null);
|
|
52
|
+
var createDefaultRtcSnapshot = () => ({
|
|
53
|
+
roomId: null,
|
|
54
|
+
userId: null,
|
|
55
|
+
appId: null,
|
|
56
|
+
botUserId: null,
|
|
57
|
+
joined: false,
|
|
58
|
+
localVideoPublished: false,
|
|
59
|
+
remoteVideoUserId: null,
|
|
60
|
+
users: []
|
|
61
|
+
});
|
|
62
|
+
var createDefaultSeiGateState = () => ({
|
|
63
|
+
expectedSei: null,
|
|
64
|
+
sei: null,
|
|
65
|
+
userId: null,
|
|
66
|
+
matched: true
|
|
67
|
+
});
|
|
68
|
+
function XmaxProvider({ config, children }) {
|
|
69
|
+
const sdkI18n = useMemo(() => config.i18n ?? createSDKI18n({ locale: config.locale }), [config.i18n, config.locale]);
|
|
70
|
+
const debugEnabled = config.debug ?? false;
|
|
71
|
+
const debugLog = useMemo(
|
|
72
|
+
() => createSessionDebugLogger(debugEnabled, config.onLog),
|
|
73
|
+
[config.onLog, debugEnabled]
|
|
74
|
+
);
|
|
75
|
+
const debugLogRef = useRef(debugLog);
|
|
76
|
+
debugLogRef.current = debugLog;
|
|
77
|
+
const debugEnabledRef = useRef(debugEnabled);
|
|
78
|
+
debugEnabledRef.current = debugEnabled;
|
|
79
|
+
const prevSeiGateRef = useRef(null);
|
|
80
|
+
const prevOutputVisibilityRef = useRef(null);
|
|
81
|
+
const prevVideoHealthRef = useRef(null);
|
|
82
|
+
const resetDebugSnapshots = useCallback(() => {
|
|
83
|
+
prevSeiGateRef.current = null;
|
|
84
|
+
prevOutputVisibilityRef.current = null;
|
|
85
|
+
prevVideoHealthRef.current = null;
|
|
86
|
+
}, []);
|
|
87
|
+
const externalOnError = config.onError;
|
|
88
|
+
const clientRef = useRef(null);
|
|
89
|
+
const sessionUidRef = useRef(null);
|
|
90
|
+
const taskUidRef = useRef(null);
|
|
91
|
+
const lastStartPromptRef = useRef(void 0);
|
|
92
|
+
const lastStartModelRef = useRef("");
|
|
93
|
+
const lastStartRefImageRef = useRef(void 0);
|
|
94
|
+
const videoFileStreamRef = useRef(null);
|
|
95
|
+
const cameraStreamRef = useRef(null);
|
|
96
|
+
const localVideoSizeRef = useRef(null);
|
|
97
|
+
const inputSourceRef = useRef("none");
|
|
98
|
+
const disconnectRef = useRef(null);
|
|
99
|
+
const disconnectPromiseRef = useRef(null);
|
|
100
|
+
const localContainerRef = useRef(null);
|
|
101
|
+
const remoteContainerRef = useRef(null);
|
|
102
|
+
const outputEnabledRef = useRef(false);
|
|
103
|
+
const activeModeCodeRef = useRef(null);
|
|
104
|
+
const onRemoteOutputStalledRef = useRef(null);
|
|
105
|
+
const [rtcState, setRtcState] = useState(createDefaultRtcSnapshot);
|
|
106
|
+
const [inputSource, setInputSource] = useState("none");
|
|
107
|
+
const [isCameraOn, setIsCameraOn] = useState(false);
|
|
108
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
109
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
110
|
+
const [hasResult, setHasResult] = useState(false);
|
|
111
|
+
const [outputEnabled, setOutputEnabled] = useState(false);
|
|
112
|
+
const [activeModeCode, setActiveModeCode] = useState(null);
|
|
113
|
+
const [localVideoSize, setLocalVideoSize] = useState(null);
|
|
114
|
+
const [sessionTargetSize, setSessionTargetSize] = useState(null);
|
|
115
|
+
const [seiGate, setSeiGate] = useState(createDefaultSeiGateState);
|
|
116
|
+
inputSourceRef.current = inputSource;
|
|
117
|
+
localVideoSizeRef.current = localVideoSize;
|
|
118
|
+
outputEnabledRef.current = outputEnabled;
|
|
119
|
+
activeModeCodeRef.current = activeModeCode;
|
|
120
|
+
const ensureLocalVideoContainer = useCallback(() => {
|
|
121
|
+
const rtc = rtcManagerRef.current;
|
|
122
|
+
const container = localContainerRef.current;
|
|
123
|
+
if (!rtc || !container) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
rtc.setLocalVideoContainer(container);
|
|
127
|
+
}, []);
|
|
128
|
+
const bindRemoteContainer = useCallback(() => {
|
|
129
|
+
const rtc = rtcManagerRef.current;
|
|
130
|
+
const container = remoteContainerRef.current;
|
|
131
|
+
if (!rtc || !container) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
rtc.setRemoteVideoContainer(container);
|
|
135
|
+
void rtc.refreshRemoteVideoBinding();
|
|
136
|
+
}, []);
|
|
137
|
+
const detachRemoteContainer = useCallback(() => {
|
|
138
|
+
rtcManagerRef.current?.setRemoteVideoContainer(null);
|
|
139
|
+
}, []);
|
|
140
|
+
const notifyUserError = useCallback((message, error) => {
|
|
141
|
+
if (externalOnError) {
|
|
142
|
+
externalOnError(message, error);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
146
|
+
browserToast.error(message);
|
|
147
|
+
}
|
|
148
|
+
}, [externalOnError]);
|
|
149
|
+
const reportError = useCallback((error, fallbackMessage) => {
|
|
150
|
+
return notifyError(error, notifyUserError, fallbackMessage ?? sdkI18n.t("errors.requestFailed"));
|
|
151
|
+
}, [notifyUserError, sdkI18n]);
|
|
152
|
+
const rtcManagerRef = useRef(null);
|
|
153
|
+
if (!rtcManagerRef.current) {
|
|
154
|
+
rtcManagerRef.current = new RtcManager({
|
|
155
|
+
onStateChange: (state) => setRtcState(state),
|
|
156
|
+
onSeiGateChange: (state) => {
|
|
157
|
+
setSeiGate(state);
|
|
158
|
+
if (debugEnabledRef.current && seiGateChanged(prevSeiGateRef.current, state)) {
|
|
159
|
+
debugLogRef.current("sei", state.matched ? "success" : "warn", "gate change", state);
|
|
160
|
+
prevSeiGateRef.current = state;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
onLog: (entry) => config.onLog?.({ scope: "rtc", ...entry }),
|
|
164
|
+
onRemoteOutputStalled: (info) => onRemoteOutputStalledRef.current?.(info),
|
|
165
|
+
debug: debugEnabled,
|
|
166
|
+
i18n: sdkI18n
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const getClient = useCallback(() => {
|
|
170
|
+
const existing = clientRef.current;
|
|
171
|
+
if (existing) {
|
|
172
|
+
return existing;
|
|
173
|
+
}
|
|
174
|
+
const created = new XmaxOpenClient({
|
|
175
|
+
apiKey: config.apiKey,
|
|
176
|
+
authToken: config.authToken,
|
|
177
|
+
baseUrl: config.baseUrl,
|
|
178
|
+
locale: config.locale,
|
|
179
|
+
i18n: sdkI18n,
|
|
180
|
+
onError: notifyUserError
|
|
181
|
+
});
|
|
182
|
+
clientRef.current = created;
|
|
183
|
+
return created;
|
|
184
|
+
}, [config.apiKey, config.authToken, config.baseUrl, config.locale, notifyUserError, sdkI18n]);
|
|
185
|
+
const ensureConnected = useCallback(async () => {
|
|
186
|
+
const client = getClient();
|
|
187
|
+
const rtc = rtcManagerRef.current;
|
|
188
|
+
if (!rtc) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (rtc.snapshot.joined && sessionUidRef.current) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const model = config.model ?? "x2.0";
|
|
195
|
+
const session = await client.createSession(model);
|
|
196
|
+
if (!session.sessionUid?.trim()) {
|
|
197
|
+
throw new Error(sdkI18n.t("errors.sessionUidMissing"));
|
|
198
|
+
}
|
|
199
|
+
sessionUidRef.current = session.sessionUid;
|
|
200
|
+
client.startHeartbeat(
|
|
201
|
+
session.sessionUid,
|
|
202
|
+
config.heartbeatIntervalMs ?? 1e4,
|
|
203
|
+
(heartbeatSession) => {
|
|
204
|
+
if (heartbeatSession.status !== ACTIVE_STATUS) {
|
|
205
|
+
void disconnectRef.current?.();
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
() => {
|
|
209
|
+
void disconnectRef.current?.();
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
const joinInfo = client.getRtcJoinInfo(session);
|
|
213
|
+
await rtc.join(joinInfo);
|
|
214
|
+
ensureLocalVideoContainer();
|
|
215
|
+
}, [config.heartbeatIntervalMs, config.model, ensureLocalVideoContainer, getClient, sdkI18n]);
|
|
216
|
+
const connect = useCallback(async () => {
|
|
217
|
+
try {
|
|
218
|
+
await ensureConnected();
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw reportError(error);
|
|
221
|
+
}
|
|
222
|
+
}, [ensureConnected, reportError]);
|
|
223
|
+
const stopPublishing = useCallback(async () => {
|
|
224
|
+
const rtc = rtcManagerRef.current;
|
|
225
|
+
if (!rtc) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
await rtc.stopVideoPublishing();
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
rtc.clearRemoteVideo();
|
|
233
|
+
detachRemoteContainer();
|
|
234
|
+
setOutputEnabled(false);
|
|
235
|
+
setHasResult(false);
|
|
236
|
+
if (videoFileStreamRef.current) {
|
|
237
|
+
videoFileStreamRef.current.destroy();
|
|
238
|
+
videoFileStreamRef.current = null;
|
|
239
|
+
}
|
|
240
|
+
if (cameraStreamRef.current) {
|
|
241
|
+
cameraStreamRef.current.destroy();
|
|
242
|
+
cameraStreamRef.current = null;
|
|
243
|
+
}
|
|
244
|
+
setInputSource("none");
|
|
245
|
+
setIsCameraOn(false);
|
|
246
|
+
setIsRecording(false);
|
|
247
|
+
setLocalVideoSize(null);
|
|
248
|
+
localVideoSizeRef.current = null;
|
|
249
|
+
setSessionTargetSize(null);
|
|
250
|
+
setSeiGate(createDefaultSeiGateState());
|
|
251
|
+
resetDebugSnapshots();
|
|
252
|
+
debugLogRef.current("lifecycle", "info", "stopPublishing", { inputSource: inputSourceRef.current });
|
|
253
|
+
}, [detachRemoteContainer, resetDebugSnapshots]);
|
|
254
|
+
const stop = useCallback(async () => {
|
|
255
|
+
try {
|
|
256
|
+
const rtc = rtcManagerRef.current;
|
|
257
|
+
taskUidRef.current = null;
|
|
258
|
+
rtc?.configureSessionSei(null);
|
|
259
|
+
setSeiGate(createDefaultSeiGateState());
|
|
260
|
+
resetDebugSnapshots();
|
|
261
|
+
debugLogRef.current("lifecycle", "info", "stop", {});
|
|
262
|
+
if (!rtc?.snapshot.joined) {
|
|
263
|
+
rtc?.clearRemoteVideo();
|
|
264
|
+
detachRemoteContainer();
|
|
265
|
+
setHasResult(false);
|
|
266
|
+
setOutputEnabled(false);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const stopEvent = createStopRtcRoomEvent({
|
|
271
|
+
userId: rtc.snapshot.userId ?? void 0,
|
|
272
|
+
sessionUid: sessionUidRef.current ?? void 0
|
|
273
|
+
});
|
|
274
|
+
await rtc.sendRoomEvent(stopEvent);
|
|
275
|
+
} finally {
|
|
276
|
+
rtc.clearRemoteVideo();
|
|
277
|
+
detachRemoteContainer();
|
|
278
|
+
setHasResult(false);
|
|
279
|
+
setOutputEnabled(false);
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw reportError(error);
|
|
283
|
+
}
|
|
284
|
+
}, [detachRemoteContainer, reportError, resetDebugSnapshots]);
|
|
285
|
+
const disconnect = useCallback(async () => {
|
|
286
|
+
if (disconnectPromiseRef.current) {
|
|
287
|
+
return disconnectPromiseRef.current;
|
|
288
|
+
}
|
|
289
|
+
const run = async () => {
|
|
290
|
+
const client = clientRef.current;
|
|
291
|
+
const rtc = rtcManagerRef.current;
|
|
292
|
+
const activeSessionUid = sessionUidRef.current;
|
|
293
|
+
client?.beginTeardown();
|
|
294
|
+
rtc?.configureSessionSei(null);
|
|
295
|
+
setSeiGate(createDefaultSeiGateState());
|
|
296
|
+
taskUidRef.current = null;
|
|
297
|
+
resetDebugSnapshots();
|
|
298
|
+
rtc?.clearRemoteVideo();
|
|
299
|
+
try {
|
|
300
|
+
await stop();
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
await stopPublishing();
|
|
304
|
+
if (rtc) {
|
|
305
|
+
try {
|
|
306
|
+
rtc.configureSessionSei(null);
|
|
307
|
+
await rtc.leave();
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (client && activeSessionUid) {
|
|
312
|
+
try {
|
|
313
|
+
await client.closeSession(activeSessionUid);
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
client?.endTeardown();
|
|
318
|
+
sessionUidRef.current = null;
|
|
319
|
+
taskUidRef.current = null;
|
|
320
|
+
setHasResult(false);
|
|
321
|
+
setOutputEnabled(false);
|
|
322
|
+
setActiveModeCode(null);
|
|
323
|
+
setSessionTargetSize(null);
|
|
324
|
+
setIsRecording(false);
|
|
325
|
+
detachRemoteContainer();
|
|
326
|
+
setRtcState(createDefaultRtcSnapshot());
|
|
327
|
+
setSeiGate(createDefaultSeiGateState());
|
|
328
|
+
};
|
|
329
|
+
const promise = run().finally(() => {
|
|
330
|
+
if (disconnectPromiseRef.current === promise) {
|
|
331
|
+
disconnectPromiseRef.current = null;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
disconnectPromiseRef.current = promise;
|
|
335
|
+
return promise;
|
|
336
|
+
}, [detachRemoteContainer, stop, stopPublishing, resetDebugSnapshots]);
|
|
337
|
+
disconnectRef.current = disconnect;
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
return () => {
|
|
340
|
+
void disconnectRef.current?.();
|
|
341
|
+
};
|
|
342
|
+
}, []);
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
if (!debugEnabled) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const snapshot = {
|
|
348
|
+
outputEnabled,
|
|
349
|
+
hasResult,
|
|
350
|
+
showOutputVideo: outputEnabled && (hasResult || !!rtcState.remoteVideoUserId),
|
|
351
|
+
remoteVideoUserId: rtcState.remoteVideoUserId,
|
|
352
|
+
localVideoPublished: rtcState.localVideoPublished,
|
|
353
|
+
inputSource,
|
|
354
|
+
seiMatched: seiGate.matched,
|
|
355
|
+
expectedSei: seiGate.expectedSei,
|
|
356
|
+
lastSei: seiGate.sei
|
|
357
|
+
};
|
|
358
|
+
if (!outputVisibilityChanged(prevOutputVisibilityRef.current, snapshot)) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const level = snapshot.showOutputVideo ? "info" : "warn";
|
|
362
|
+
debugLog("output", level, "visibility change", snapshot);
|
|
363
|
+
prevOutputVisibilityRef.current = snapshot;
|
|
364
|
+
}, [
|
|
365
|
+
debugEnabled,
|
|
366
|
+
debugLog,
|
|
367
|
+
outputEnabled,
|
|
368
|
+
hasResult,
|
|
369
|
+
rtcState.remoteVideoUserId,
|
|
370
|
+
rtcState.localVideoPublished,
|
|
371
|
+
inputSource,
|
|
372
|
+
seiGate.matched,
|
|
373
|
+
seiGate.expectedSei,
|
|
374
|
+
seiGate.sei
|
|
375
|
+
]);
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
const rtc = rtcManagerRef.current;
|
|
378
|
+
if (!rtc) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (!outputEnabled) {
|
|
382
|
+
rtc.clearRemoteVideo();
|
|
383
|
+
detachRemoteContainer();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
bindRemoteContainer();
|
|
387
|
+
}, [bindRemoteContainer, detachRemoteContainer, outputEnabled]);
|
|
388
|
+
const openCamera = useCallback(async (options) => {
|
|
389
|
+
try {
|
|
390
|
+
await ensureConnected();
|
|
391
|
+
const rtc = rtcManagerRef.current;
|
|
392
|
+
if (!rtc) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
await stopPublishing();
|
|
396
|
+
ensureLocalVideoContainer();
|
|
397
|
+
const defaultCamera = models.realtime("x2.0");
|
|
398
|
+
const cameraHint = options?.size ?? [defaultCamera.width, defaultCamera.height];
|
|
399
|
+
const precomputedSize = resolveUploadVideoSize(cameraHint[0], cameraHint[1]);
|
|
400
|
+
setSessionTargetSize(precomputedSize);
|
|
401
|
+
const rawStream = await requestCameraMediaStream({
|
|
402
|
+
width: cameraHint[0],
|
|
403
|
+
height: cameraHint[1],
|
|
404
|
+
frameRate: 30,
|
|
405
|
+
facingMode: "user"
|
|
406
|
+
});
|
|
407
|
+
const publishStream = await createNativeCameraStream(rawStream);
|
|
408
|
+
cameraStreamRef.current = publishStream;
|
|
409
|
+
await rtc.startExternalVideoPublishing(publishStream.videoTrack, precomputedSize, {
|
|
410
|
+
mobile: isMobilePublishEnvironment(),
|
|
411
|
+
highQuality: true
|
|
412
|
+
});
|
|
413
|
+
const sessionDims = { width: publishStream.width, height: publishStream.height };
|
|
414
|
+
setLocalVideoSize(sessionDims);
|
|
415
|
+
localVideoSizeRef.current = sessionDims;
|
|
416
|
+
setSessionTargetSize([sessionDims.width, sessionDims.height]);
|
|
417
|
+
setIsCameraOn(true);
|
|
418
|
+
setIsRecording(true);
|
|
419
|
+
setInputSource("camera");
|
|
420
|
+
} catch (error) {
|
|
421
|
+
throw reportError(error);
|
|
422
|
+
}
|
|
423
|
+
}, [ensureConnected, ensureLocalVideoContainer, reportError, sdkI18n, stopPublishing]);
|
|
424
|
+
const closeCamera = useCallback(async () => {
|
|
425
|
+
await disconnect();
|
|
426
|
+
}, [disconnect]);
|
|
427
|
+
const loadVideoFile = useCallback(async (file, options) => {
|
|
428
|
+
try {
|
|
429
|
+
await ensureConnected();
|
|
430
|
+
const rtc = rtcManagerRef.current;
|
|
431
|
+
if (!rtc) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
await stopPublishing();
|
|
435
|
+
ensureLocalVideoContainer();
|
|
436
|
+
prevVideoHealthRef.current = null;
|
|
437
|
+
const stream = await createVideoFileStream(file, {
|
|
438
|
+
muted: true,
|
|
439
|
+
loop: true,
|
|
440
|
+
mobile: isMobilePublishEnvironment(),
|
|
441
|
+
targetSize: options?.size,
|
|
442
|
+
i18n: sdkI18n,
|
|
443
|
+
onHealthReport: debugEnabled ? (health) => {
|
|
444
|
+
if (!videoInputHealthChanged(prevVideoHealthRef.current, health)) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
prevVideoHealthRef.current = health;
|
|
448
|
+
const level = health.stalled || health.paused || health.ended ? "warn" : "info";
|
|
449
|
+
debugLogRef.current("video-input", level, "health change", health);
|
|
450
|
+
} : void 0
|
|
451
|
+
});
|
|
452
|
+
if (videoFileStreamRef.current) {
|
|
453
|
+
videoFileStreamRef.current.destroy();
|
|
454
|
+
}
|
|
455
|
+
videoFileStreamRef.current = stream;
|
|
456
|
+
await rtc.startExternalVideoPublishing(stream.videoTrack, [stream.width, stream.height], {
|
|
457
|
+
mobile: isMobilePublishEnvironment(),
|
|
458
|
+
fps: stream.fps,
|
|
459
|
+
highQuality: true
|
|
460
|
+
});
|
|
461
|
+
setInputSource("video");
|
|
462
|
+
setIsCameraOn(true);
|
|
463
|
+
setIsRecording(true);
|
|
464
|
+
debugLogRef.current("lifecycle", "info", "loadVideoFile", {
|
|
465
|
+
width: stream.width,
|
|
466
|
+
height: stream.height,
|
|
467
|
+
fileName: file.name
|
|
468
|
+
});
|
|
469
|
+
if (stream.width && stream.height) {
|
|
470
|
+
const size = { width: stream.width, height: stream.height };
|
|
471
|
+
setLocalVideoSize(size);
|
|
472
|
+
localVideoSizeRef.current = size;
|
|
473
|
+
setSessionTargetSize([stream.width, stream.height]);
|
|
474
|
+
} else {
|
|
475
|
+
setLocalVideoSize(null);
|
|
476
|
+
localVideoSizeRef.current = null;
|
|
477
|
+
}
|
|
478
|
+
const targetSize = [stream.width, stream.height];
|
|
479
|
+
return {
|
|
480
|
+
url: stream.url,
|
|
481
|
+
width: stream.width,
|
|
482
|
+
height: stream.height,
|
|
483
|
+
previewStream: stream.previewStream,
|
|
484
|
+
videoEl: stream.videoEl,
|
|
485
|
+
targetSize
|
|
486
|
+
};
|
|
487
|
+
} catch (error) {
|
|
488
|
+
throw reportError(error);
|
|
489
|
+
}
|
|
490
|
+
}, [ensureConnected, ensureLocalVideoContainer, reportError, sdkI18n, stopPublishing, debugEnabled]);
|
|
491
|
+
const clearInput = useCallback(async () => {
|
|
492
|
+
await disconnect();
|
|
493
|
+
}, [disconnect]);
|
|
494
|
+
const uploadImage = useCallback(async (file) => {
|
|
495
|
+
try {
|
|
496
|
+
const client = getClient();
|
|
497
|
+
return await client.uploadImage(file);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
throw reportError(error);
|
|
500
|
+
}
|
|
501
|
+
}, [getClient, reportError]);
|
|
502
|
+
const setRefImage = useCallback(async (input) => {
|
|
503
|
+
try {
|
|
504
|
+
if (typeof input !== "string") {
|
|
505
|
+
const result2 = await uploadImage(input);
|
|
506
|
+
return result2.url;
|
|
507
|
+
}
|
|
508
|
+
const raw = input.trim();
|
|
509
|
+
if (!raw) {
|
|
510
|
+
return "";
|
|
511
|
+
}
|
|
512
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
513
|
+
return raw;
|
|
514
|
+
}
|
|
515
|
+
const res = await fetch(raw);
|
|
516
|
+
if (!res.ok) {
|
|
517
|
+
throw new Error(`Failed to load ref image: ${res.status}`);
|
|
518
|
+
}
|
|
519
|
+
const blob = await res.blob();
|
|
520
|
+
const mime = blob.type || "image/png";
|
|
521
|
+
const ext = mime === "image/jpeg" ? "jpg" : mime === "image/webp" ? "webp" : mime === "image/gif" ? "gif" : "png";
|
|
522
|
+
const file = new File([blob], `ref.${ext}`, { type: mime });
|
|
523
|
+
const result = await uploadImage(file);
|
|
524
|
+
return result.url;
|
|
525
|
+
} catch (error) {
|
|
526
|
+
throw reportError(error);
|
|
527
|
+
}
|
|
528
|
+
}, [reportError, uploadImage]);
|
|
529
|
+
const start = useCallback(async (options) => {
|
|
530
|
+
try {
|
|
531
|
+
await ensureConnected();
|
|
532
|
+
const rtc = rtcManagerRef.current;
|
|
533
|
+
if (!rtc?.snapshot.joined) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const refForEvent = options.refImage;
|
|
537
|
+
const resolvedPrompt = options.prompt?.trim() ?? "";
|
|
538
|
+
setIsGenerating(true);
|
|
539
|
+
rtc.clearRemoteVideo();
|
|
540
|
+
rtc.setRemoteVideoContainer(null);
|
|
541
|
+
setHasResult(false);
|
|
542
|
+
setOutputEnabled(true);
|
|
543
|
+
outputEnabledRef.current = true;
|
|
544
|
+
const size = resolveSessionTargetSize({
|
|
545
|
+
localVideoSize: localVideoSizeRef.current,
|
|
546
|
+
overrideSize: options.size
|
|
547
|
+
});
|
|
548
|
+
setSessionTargetSize(size);
|
|
549
|
+
const taskUid = createTaskUid();
|
|
550
|
+
taskUidRef.current = taskUid;
|
|
551
|
+
lastStartPromptRef.current = resolvedPrompt || void 0;
|
|
552
|
+
lastStartModelRef.current = options.model;
|
|
553
|
+
lastStartRefImageRef.current = refForEvent?.trim() || void 0;
|
|
554
|
+
prevSeiGateRef.current = null;
|
|
555
|
+
rtc.configureSessionSei(taskUid);
|
|
556
|
+
debugLogRef.current("lifecycle", "info", "start", {
|
|
557
|
+
taskUid,
|
|
558
|
+
size,
|
|
559
|
+
inputSource: inputSourceRef.current
|
|
560
|
+
});
|
|
561
|
+
const startEvent = createStartRtcRoomEvent({
|
|
562
|
+
userId: rtc.snapshot.userId ?? void 0,
|
|
563
|
+
uid: taskUid,
|
|
564
|
+
sessionUid: taskUid,
|
|
565
|
+
model: options.model,
|
|
566
|
+
size,
|
|
567
|
+
prompt: resolvedPrompt || void 0,
|
|
568
|
+
extraParams: options.extraParams,
|
|
569
|
+
refImagePath: refForEvent?.trim() || void 0
|
|
570
|
+
});
|
|
571
|
+
if (remoteContainerRef.current) {
|
|
572
|
+
rtc.setRemoteVideoContainer(remoteContainerRef.current);
|
|
573
|
+
}
|
|
574
|
+
await rtc.ensurePublishedInputBeforeStart();
|
|
575
|
+
await rtc.sendRoomEvent(startEvent);
|
|
576
|
+
await rtc.refreshRemoteVideoBinding({ force: true });
|
|
577
|
+
setHasResult(true);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
setOutputEnabled(false);
|
|
580
|
+
throw reportError(error);
|
|
581
|
+
} finally {
|
|
582
|
+
setIsGenerating(false);
|
|
583
|
+
}
|
|
584
|
+
}, [ensureConnected, reportError]);
|
|
585
|
+
const sendTracks = useCallback(async (options) => {
|
|
586
|
+
const rtc = rtcManagerRef.current;
|
|
587
|
+
if (!rtc?.snapshot.joined) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const activeTaskUid = taskUidRef.current;
|
|
591
|
+
if (!activeTaskUid) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
console.log("[Xmax] sendTracks", {
|
|
595
|
+
taskUid: activeTaskUid,
|
|
596
|
+
targetSize: sessionTargetSize,
|
|
597
|
+
tracks: options.tracks
|
|
598
|
+
});
|
|
599
|
+
try {
|
|
600
|
+
const event = createTracksRtcRoomEvent({
|
|
601
|
+
userId: rtc.snapshot.userId ?? void 0,
|
|
602
|
+
uid: activeTaskUid,
|
|
603
|
+
sessionUid: activeTaskUid,
|
|
604
|
+
tracks: options.tracks
|
|
605
|
+
});
|
|
606
|
+
await rtc.sendRoomEvent(event);
|
|
607
|
+
} catch {
|
|
608
|
+
}
|
|
609
|
+
}, [sessionTargetSize]);
|
|
610
|
+
useCallback(async () => {
|
|
611
|
+
const rtc = rtcManagerRef.current;
|
|
612
|
+
if (!rtc?.snapshot.joined || !outputEnabledRef.current) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
debugLogRef.current("lifecycle", "warn", "recoverGeneration", {
|
|
617
|
+
inputSource: inputSourceRef.current
|
|
618
|
+
});
|
|
619
|
+
const taskUid = createTaskUid();
|
|
620
|
+
taskUidRef.current = taskUid;
|
|
621
|
+
prevSeiGateRef.current = null;
|
|
622
|
+
const size = resolveSessionTargetSize({
|
|
623
|
+
localVideoSize: localVideoSizeRef.current
|
|
624
|
+
});
|
|
625
|
+
const startEvent = createStartRtcRoomEvent({
|
|
626
|
+
userId: rtc.snapshot.userId ?? void 0,
|
|
627
|
+
uid: taskUid,
|
|
628
|
+
sessionUid: taskUid,
|
|
629
|
+
model: lastStartModelRef.current,
|
|
630
|
+
size,
|
|
631
|
+
prompt: lastStartPromptRef.current,
|
|
632
|
+
refImagePath: lastStartRefImageRef.current
|
|
633
|
+
});
|
|
634
|
+
if (remoteContainerRef.current) {
|
|
635
|
+
rtc.setRemoteVideoContainer(remoteContainerRef.current);
|
|
636
|
+
}
|
|
637
|
+
await rtc.sendRoomEvent(startEvent);
|
|
638
|
+
if (remoteContainerRef.current) {
|
|
639
|
+
rtc.setRemoteVideoContainer(remoteContainerRef.current);
|
|
640
|
+
}
|
|
641
|
+
await rtc.refreshRemoteVideoBinding({ force: true });
|
|
642
|
+
} catch (error) {
|
|
643
|
+
reportError(error);
|
|
644
|
+
}
|
|
645
|
+
}, [reportError]);
|
|
646
|
+
useEffect(() => {
|
|
647
|
+
onRemoteOutputStalledRef.current = (info) => {
|
|
648
|
+
debugLogRef.current("lifecycle", "warn", "remote output stalled", info);
|
|
649
|
+
};
|
|
650
|
+
}, []);
|
|
651
|
+
const changeModel = useCallback(async () => {
|
|
652
|
+
return;
|
|
653
|
+
}, []);
|
|
654
|
+
const bindLocalView = useCallback((el) => {
|
|
655
|
+
localContainerRef.current = el;
|
|
656
|
+
rtcManagerRef.current?.setLocalVideoContainer(el);
|
|
657
|
+
}, []);
|
|
658
|
+
const bindRemoteView = useCallback((el) => {
|
|
659
|
+
remoteContainerRef.current = el;
|
|
660
|
+
if (!el) {
|
|
661
|
+
detachRemoteContainer();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (outputEnabledRef.current) {
|
|
665
|
+
bindRemoteContainer();
|
|
666
|
+
}
|
|
667
|
+
}, [bindRemoteContainer, detachRemoteContainer]);
|
|
668
|
+
const value = useMemo(() => ({
|
|
669
|
+
snapshot: {
|
|
670
|
+
sessionUid: sessionUidRef.current,
|
|
671
|
+
rtc: rtcState,
|
|
672
|
+
inputSource,
|
|
673
|
+
isCameraOn,
|
|
674
|
+
isRecording,
|
|
675
|
+
isGenerating,
|
|
676
|
+
isOutputVerifying: !!seiGate.expectedSei && !seiGate.matched,
|
|
677
|
+
hasResult,
|
|
678
|
+
outputEnabled,
|
|
679
|
+
dragEnabled: outputEnabled && isRecording,
|
|
680
|
+
showOutputVideo: outputEnabled && (hasResult || !!rtcState.remoteVideoUserId),
|
|
681
|
+
localVideoSize,
|
|
682
|
+
sessionTargetSize,
|
|
683
|
+
seiGate
|
|
684
|
+
},
|
|
685
|
+
connect,
|
|
686
|
+
disconnect,
|
|
687
|
+
openCamera,
|
|
688
|
+
closeCamera,
|
|
689
|
+
loadVideoFile,
|
|
690
|
+
clearInput,
|
|
691
|
+
uploadImage,
|
|
692
|
+
setRefImage,
|
|
693
|
+
start,
|
|
694
|
+
sendTracks,
|
|
695
|
+
stop,
|
|
696
|
+
changeModel,
|
|
697
|
+
bindLocalView,
|
|
698
|
+
bindRemoteView
|
|
699
|
+
}), [
|
|
700
|
+
rtcState,
|
|
701
|
+
inputSource,
|
|
702
|
+
isCameraOn,
|
|
703
|
+
isRecording,
|
|
704
|
+
isGenerating,
|
|
705
|
+
seiGate,
|
|
706
|
+
hasResult,
|
|
707
|
+
outputEnabled,
|
|
708
|
+
localVideoSize,
|
|
709
|
+
sessionTargetSize,
|
|
710
|
+
connect,
|
|
711
|
+
disconnect,
|
|
712
|
+
openCamera,
|
|
713
|
+
closeCamera,
|
|
714
|
+
loadVideoFile,
|
|
715
|
+
clearInput,
|
|
716
|
+
uploadImage,
|
|
717
|
+
setRefImage,
|
|
718
|
+
start,
|
|
719
|
+
sendTracks,
|
|
720
|
+
stop,
|
|
721
|
+
changeModel,
|
|
722
|
+
bindLocalView,
|
|
723
|
+
bindRemoteView
|
|
724
|
+
]);
|
|
725
|
+
return /* @__PURE__ */ jsx(XmaxContext.Provider, { value, children });
|
|
726
|
+
}
|
|
727
|
+
function useXmax() {
|
|
728
|
+
const ctx = useContext(XmaxContext);
|
|
729
|
+
if (!ctx) {
|
|
730
|
+
throw new Error("useXmax must be used within XmaxProvider");
|
|
731
|
+
}
|
|
732
|
+
return ctx;
|
|
733
|
+
}
|
|
734
|
+
function DragTrackSurface({
|
|
735
|
+
enabled,
|
|
736
|
+
targetSize,
|
|
737
|
+
onTracks,
|
|
738
|
+
className,
|
|
739
|
+
style,
|
|
740
|
+
fitMode = "cover"
|
|
741
|
+
}) {
|
|
742
|
+
const containerRef = useRef(null);
|
|
743
|
+
const controllerRef = useRef(null);
|
|
744
|
+
const onTracksRef = useRef(onTracks);
|
|
745
|
+
onTracksRef.current = onTracks;
|
|
746
|
+
useEffect(() => {
|
|
747
|
+
const container = containerRef.current;
|
|
748
|
+
if (!container) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const controller = createDragTrackController(container, {
|
|
752
|
+
targetSize,
|
|
753
|
+
fitMode,
|
|
754
|
+
enabled,
|
|
755
|
+
onTracks: (tracks) => onTracksRef.current(tracks)
|
|
756
|
+
});
|
|
757
|
+
controllerRef.current = controller;
|
|
758
|
+
return () => {
|
|
759
|
+
controller.destroy();
|
|
760
|
+
controllerRef.current = null;
|
|
761
|
+
};
|
|
762
|
+
}, []);
|
|
763
|
+
useEffect(() => {
|
|
764
|
+
controllerRef.current?.setEnabled(enabled);
|
|
765
|
+
}, [enabled]);
|
|
766
|
+
useEffect(() => {
|
|
767
|
+
controllerRef.current?.setTargetSize(targetSize);
|
|
768
|
+
}, [targetSize]);
|
|
769
|
+
useEffect(() => {
|
|
770
|
+
controllerRef.current?.setFitMode(fitMode);
|
|
771
|
+
}, [fitMode]);
|
|
772
|
+
return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style });
|
|
773
|
+
}
|
|
774
|
+
function XmaxView({
|
|
775
|
+
kind,
|
|
776
|
+
className,
|
|
777
|
+
style
|
|
778
|
+
}) {
|
|
779
|
+
const ref = useRef(null);
|
|
780
|
+
const { bindLocalView, bindRemoteView, snapshot, sendTracks } = useXmax();
|
|
781
|
+
useEffect(() => {
|
|
782
|
+
const el = ref.current;
|
|
783
|
+
if (!el) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (kind === "local") {
|
|
787
|
+
bindLocalView(el);
|
|
788
|
+
return () => bindLocalView(null);
|
|
789
|
+
}
|
|
790
|
+
bindRemoteView(el);
|
|
791
|
+
return () => bindRemoteView(null);
|
|
792
|
+
}, [bindLocalView, bindRemoteView, kind]);
|
|
793
|
+
if (kind === "remote") {
|
|
794
|
+
const dragTargetSize = snapshot.sessionTargetSize ?? DEFAULT_SESSION_TARGET_SIZE;
|
|
795
|
+
return /* @__PURE__ */ jsxs("div", { className, style: { position: "relative", ...style }, children: [
|
|
796
|
+
/* @__PURE__ */ jsx("div", { ref, style: { width: "100%", height: "100%" } }),
|
|
797
|
+
/* @__PURE__ */ jsx(
|
|
798
|
+
DragTrackSurface,
|
|
799
|
+
{
|
|
800
|
+
enabled: snapshot.dragEnabled,
|
|
801
|
+
targetSize: dragTargetSize,
|
|
802
|
+
fitMode: "contain",
|
|
803
|
+
onTracks: (tracks) => {
|
|
804
|
+
void sendTracks({ tracks });
|
|
805
|
+
},
|
|
806
|
+
className: "absolute inset-0 touch-none",
|
|
807
|
+
style: { zIndex: 10 }
|
|
808
|
+
}
|
|
809
|
+
)
|
|
810
|
+
] });
|
|
811
|
+
}
|
|
812
|
+
return /* @__PURE__ */ jsx("div", { ref, className, style });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export { DragTrackSurface, XmaxProvider, XmaxView, useXmax };
|
|
816
|
+
//# sourceMappingURL=index.js.map
|
|
817
|
+
//# sourceMappingURL=index.js.map
|