@trtc/calls-uikit-react 4.5.9 → 4.5.11
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/package.json +1 -1
- package/src/Components/TUICallKit/TUICallKit.tsx +73 -3
- package/src/Components/components/common/ButtonPanel/ButtonPanel.tsx +3 -3
- package/src/Components/components/common/ButtonPanel/ButtonPanelH5/ButtonPanelH5.tsx +5 -5
- package/src/Components/components/common/FloatingWindow/FloatingWindow.tsx +2 -2
- package/src/Components/components/common/StreamInfo/StreamInfoH5/StreamInfoH5.tsx +4 -4
- package/src/Components/components/common/TopBar/h5/TopBarH5.tsx +2 -2
- package/src/Components/components/common/TopBar/pc/TopBarPC.tsx +2 -2
- package/src/Components/context/CallUserInfoContext.ts +3 -0
- package/src/TUICallService/CallService/index.ts +9 -1
- package/src/index.ts +1 -1
- package/tuicall-uikit-react.es.js +2565 -2519
- package/tuicall-uikit-react.umd.js +3 -3
- package/types/Components/context/CallUserInfoContext.d.ts +1 -0
package/package.json
CHANGED
|
@@ -69,6 +69,15 @@ export default function TUICallKit(props: ITUICallKitProps) {
|
|
|
69
69
|
remoteList,
|
|
70
70
|
setRemoteList,
|
|
71
71
|
] = useState(TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST));
|
|
72
|
+
// Sticky flag: once the call reaches a multi-user state, it stays multi-user until IDLE.
|
|
73
|
+
// Initialized from the current store snapshot so a re-mount during an in-progress call
|
|
74
|
+
// (e.g. group call, or 1v1 that has already escalated) keeps the correct layout.
|
|
75
|
+
const [hasEverBeenMulti, setHasEverBeenMulti] = useState(() => {
|
|
76
|
+
const initIsGroup = TUIStore.getData(StoreName.CALL, NAME.IS_GROUP);
|
|
77
|
+
const initRemoteList = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST);
|
|
78
|
+
return !!initIsGroup || (Array.isArray(initRemoteList) && initRemoteList.length >= 2);
|
|
79
|
+
});
|
|
80
|
+
const isMultiUserLayout = hasEverBeenMulti;
|
|
72
81
|
const [callType, setCallType] = useState(TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE));
|
|
73
82
|
const [callRole, setCallRole] = useState(TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE));
|
|
74
83
|
const [isEarPhone, setIsEarPhone] = useState(TUIStore.getData(StoreName.CALL, NAME.IS_EAR_PHONE));
|
|
@@ -92,6 +101,7 @@ export default function TUICallKit(props: ITUICallKitProps) {
|
|
|
92
101
|
callType,
|
|
93
102
|
isEarPhone,
|
|
94
103
|
isGroupCall,
|
|
104
|
+
isMultiUserLayout,
|
|
95
105
|
isFloat,
|
|
96
106
|
allowedMinimized,
|
|
97
107
|
allowedFullScreen,
|
|
@@ -104,6 +114,7 @@ export default function TUICallKit(props: ITUICallKitProps) {
|
|
|
104
114
|
callType,
|
|
105
115
|
isEarPhone,
|
|
106
116
|
isGroupCall,
|
|
117
|
+
isMultiUserLayout,
|
|
107
118
|
isFloat,
|
|
108
119
|
allowedFullScreen,
|
|
109
120
|
enableVirtualBackground,
|
|
@@ -130,9 +141,18 @@ export default function TUICallKit(props: ITUICallKitProps) {
|
|
|
130
141
|
|
|
131
142
|
const handleCallStatusChange = (value) => {
|
|
132
143
|
setCallStatus(value);
|
|
144
|
+
// Reset the sticky multi-user flag when the call fully ends so the next
|
|
145
|
+
// call starts fresh (a subsequent 1v1 will start in single layout).
|
|
146
|
+
if (value === CallStatus.IDLE) {
|
|
147
|
+
setHasEverBeenMulti(false);
|
|
148
|
+
}
|
|
133
149
|
};
|
|
134
150
|
const handleIsGroupChange = (value) => {
|
|
135
151
|
setIsGroupCall(value);
|
|
152
|
+
// Group calls always render in multi-user layout.
|
|
153
|
+
if (value) {
|
|
154
|
+
setHasEverBeenMulti(true);
|
|
155
|
+
}
|
|
136
156
|
};
|
|
137
157
|
const handleCallerUserInfoChange = (value) => {
|
|
138
158
|
setCallerUserInfo(value);
|
|
@@ -142,6 +162,11 @@ export default function TUICallKit(props: ITUICallKitProps) {
|
|
|
142
162
|
};
|
|
143
163
|
const handleRemoteUserInfoListChange = (value) => {
|
|
144
164
|
setRemoteList(value);
|
|
165
|
+
// Escalate to multi-user layout once the remote participant count reaches 2+.
|
|
166
|
+
// The flag is sticky and only reset on IDLE.
|
|
167
|
+
if (Array.isArray(value) && value.length >= 2) {
|
|
168
|
+
setHasEverBeenMulti(true);
|
|
169
|
+
}
|
|
145
170
|
};
|
|
146
171
|
const handleCallMediaTypeChange = (value) => {
|
|
147
172
|
setCallType(value);
|
|
@@ -258,6 +283,51 @@ export default function TUICallKit(props: ITUICallKitProps) {
|
|
|
258
283
|
onMinimized,
|
|
259
284
|
]);
|
|
260
285
|
|
|
286
|
+
// When the layout switches from single-user to multi-user mid-call, the old
|
|
287
|
+
// SingleCall unmounts and its <div id={domId}> nodes are destroyed along with
|
|
288
|
+
// the <video> elements the SDK attached inside them. The onUserVideoAvailable /
|
|
289
|
+
// camera-open events do NOT replay, so the new GroupCall would render empty
|
|
290
|
+
// video tiles.
|
|
291
|
+
//
|
|
292
|
+
// Fix: after the new DOM is mounted, re-bind local and remote streams to the
|
|
293
|
+
// new container nodes. The switch is one-way (sticky), so this runs at most
|
|
294
|
+
// once per call. Failures here should never block the UI, only log a warning.
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
if (!isMultiUserLayout) return;
|
|
297
|
+
let cancelled = false;
|
|
298
|
+
(async () => {
|
|
299
|
+
// 1. Re-attach local camera preview to the new local pusher <div>.
|
|
300
|
+
const localSnapshot = TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO_EXCLUDE_VOLUMN);
|
|
301
|
+
if (localSnapshot?.isVideoAvailable) {
|
|
302
|
+
try {
|
|
303
|
+
await TUICallKitAPI.openCamera(NAME.LOCAL_VIDEO);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.warn('[TUICallKit] re-openCamera after layout switch failed:', error);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (cancelled) return;
|
|
309
|
+
// 2. Re-attach each active remote video stream to its new player <div>.
|
|
310
|
+
const engine = TUICallKitAPI.getTUICallEngineInstance?.();
|
|
311
|
+
if (!engine?.startRemoteView) return;
|
|
312
|
+
const displayMode = TUIStore.getData(StoreName.CALL, NAME.DISPLAY_MODE);
|
|
313
|
+
const remoteSnapshot = TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_EXCLUDE_VOLUMN_LIST) || [];
|
|
314
|
+
for (const remoteUser of remoteSnapshot) {
|
|
315
|
+
if (cancelled) return;
|
|
316
|
+
if (!remoteUser?.isVideoAvailable || !remoteUser?.userId) continue;
|
|
317
|
+
try {
|
|
318
|
+
await engine.startRemoteView({
|
|
319
|
+
userID: remoteUser.userId,
|
|
320
|
+
videoViewDomID: remoteUser.userId,
|
|
321
|
+
options: { objectFit: displayMode },
|
|
322
|
+
});
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.warn(`[TUICallKit] re-startRemoteView for ${remoteUser.userId} failed:`, error);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
})();
|
|
328
|
+
return () => { cancelled = true; };
|
|
329
|
+
}, [isMultiUserLayout]);
|
|
330
|
+
|
|
261
331
|
return callStatus !== CallStatus.IDLE && (
|
|
262
332
|
<UserInfoContext.Provider value={UserInfoContextValue}>
|
|
263
333
|
<CallerUserInfoContext.Provider value={callerUserInfo}>
|
|
@@ -265,15 +335,15 @@ export default function TUICallKit(props: ITUICallKitProps) {
|
|
|
265
335
|
<CustomUIConfigContext.Provider value={customUIConfigContextValue}>
|
|
266
336
|
<TranslateContext.Provider value={translateContextValue}>
|
|
267
337
|
<div style={{ ...style }} className={classnames} id='tuicallkit-id'>
|
|
268
|
-
{!
|
|
269
|
-
{
|
|
338
|
+
{!isMultiUserLayout && <SingleCall />}
|
|
339
|
+
{isMultiUserLayout && <GroupCall />}
|
|
270
340
|
<AutoPlayDialog />
|
|
271
341
|
<AISubtitle />
|
|
272
342
|
</div>
|
|
273
343
|
<Portal attach="body">
|
|
274
344
|
<FloatingWindow
|
|
275
345
|
show={isFloat}
|
|
276
|
-
hasVideo={
|
|
346
|
+
hasVideo={isMultiUserLayout
|
|
277
347
|
? speakerIdList.length !== 0
|
|
278
348
|
: callType === CallMediaType.VIDEO}
|
|
279
349
|
isAudioAvailable={localUserInfo.isAudioAvailable}
|
|
@@ -26,7 +26,7 @@ const isMobile = !TUIGlobal.isPC;
|
|
|
26
26
|
|
|
27
27
|
export default function ButtonPanel() {
|
|
28
28
|
const {
|
|
29
|
-
callStatus,
|
|
29
|
+
callStatus, isMultiUserLayout, callRole, callType, isShowEnableVirtualBackground,
|
|
30
30
|
} = useContext(CallInfoContext);
|
|
31
31
|
const { localUserInfo } = useContext(UserInfoContext);
|
|
32
32
|
const customButtonUIConfig = useCustomButtonUIConfig();
|
|
@@ -98,7 +98,7 @@ export default function ButtonPanel() {
|
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
const platform = TUIGlobal.isPC ? 'pc' : 'h5';
|
|
101
|
-
const callScene =
|
|
101
|
+
const callScene = isMultiUserLayout ? 'groupCall' : 'singleCall';
|
|
102
102
|
const mediaType = callType === CallMediaType.AUDIO ? 'audio' : 'video';
|
|
103
103
|
const status = callStatus === CallStatus.CALLING
|
|
104
104
|
? callRole === CallRole.CALLER ? 'calling' : 'accept'
|
|
@@ -110,7 +110,7 @@ export default function ButtonPanel() {
|
|
|
110
110
|
|
|
111
111
|
return rs;
|
|
112
112
|
}, [
|
|
113
|
-
|
|
113
|
+
isMultiUserLayout,
|
|
114
114
|
callType,
|
|
115
115
|
callStatus,
|
|
116
116
|
callRole,
|
|
@@ -22,7 +22,7 @@ interface IButtonPanelH5Props {
|
|
|
22
22
|
}
|
|
23
23
|
export default function ButtonPanelH5(props: IButtonPanelH5Props) {
|
|
24
24
|
const { buttons = [] } = props;
|
|
25
|
-
const {
|
|
25
|
+
const { isMultiUserLayout } = useContext(CallInfoContext);
|
|
26
26
|
const { focusItem } = useContext(FocusItemContext);
|
|
27
27
|
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
|
28
28
|
useEffect(() => {
|
|
@@ -38,7 +38,7 @@ export default function ButtonPanelH5(props: IButtonPanelH5Props) {
|
|
|
38
38
|
buttonPanelH5Style['button-panel-h5-container'],
|
|
39
39
|
{
|
|
40
40
|
[buttonPanelH5Style.open]: isPanelOpen,
|
|
41
|
-
[buttonPanelH5Style.groupcall]:
|
|
41
|
+
[buttonPanelH5Style.groupcall]: isMultiUserLayout,
|
|
42
42
|
[buttonPanelH5Style['show-panel-background']]: showBtnPanelBackground,
|
|
43
43
|
},
|
|
44
44
|
]);
|
|
@@ -127,7 +127,7 @@ export default function ButtonPanelH5(props: IButtonPanelH5Props) {
|
|
|
127
127
|
return <Row>{renderButtonPanelCol()}</Row>;
|
|
128
128
|
};
|
|
129
129
|
const renderButtonPanel = () => {
|
|
130
|
-
if (
|
|
130
|
+
if (isMultiUserLayout && !isPanelOpen) {
|
|
131
131
|
return renderCloseButtonPanel();
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -140,8 +140,8 @@ export default function ButtonPanelH5(props: IButtonPanelH5Props) {
|
|
|
140
140
|
<div
|
|
141
141
|
className={classNames([
|
|
142
142
|
{
|
|
143
|
-
[buttonPanelH5Style['groupcall-panel']]:
|
|
144
|
-
[buttonPanelH5Style['singlecall-panel']]: !
|
|
143
|
+
[buttonPanelH5Style['groupcall-panel']]: isMultiUserLayout,
|
|
144
|
+
[buttonPanelH5Style['singlecall-panel']]: !isMultiUserLayout,
|
|
145
145
|
[buttonPanelH5Style.close]: !isPanelOpen,
|
|
146
146
|
},
|
|
147
147
|
])}
|
|
@@ -13,7 +13,7 @@ interface IFloatingWindowProps {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export default function FloatingWindow(props: IFloatingWindowProps) {
|
|
16
|
-
const {
|
|
16
|
+
const { isMultiUserLayout } = useContext(CallInfoContext);
|
|
17
17
|
// React 19's useRef removed the no-argument overload; pass undefined explicitly.
|
|
18
18
|
const nodeRef = useRef<any>(undefined);
|
|
19
19
|
const [clickable, setClickable] = useState(true);
|
|
@@ -28,7 +28,7 @@ export default function FloatingWindow(props: IFloatingWindowProps) {
|
|
|
28
28
|
>
|
|
29
29
|
<div ref={nodeRef} style={{ visibility }}>
|
|
30
30
|
{
|
|
31
|
-
|
|
31
|
+
isMultiUserLayout
|
|
32
32
|
? <FloatingWindowGroupCall clickable={clickable} {...props} />
|
|
33
33
|
: <FloatingWindowSingleCall clickable={clickable} {...props} />
|
|
34
34
|
}
|
|
@@ -32,13 +32,13 @@ export default function StreamInfoH5(props: IStreamInfoH5Props) {
|
|
|
32
32
|
showSwitchCamera = false,
|
|
33
33
|
showNetWorkStatus = false,
|
|
34
34
|
} = props;
|
|
35
|
-
const {
|
|
35
|
+
const { isMultiUserLayout } = useContext(CallInfoContext);
|
|
36
36
|
const classnames = classNames(
|
|
37
37
|
[
|
|
38
38
|
streamInfoH5Style['stream-info-container-h5'],
|
|
39
39
|
{
|
|
40
|
-
[streamInfoH5Style.group]:
|
|
41
|
-
[streamInfoH5Style.single]: !
|
|
40
|
+
[streamInfoH5Style.group]: isMultiUserLayout,
|
|
41
|
+
[streamInfoH5Style.single]: !isMultiUserLayout,
|
|
42
42
|
},
|
|
43
43
|
],
|
|
44
44
|
);
|
|
@@ -89,7 +89,7 @@ export default function StreamInfoH5(props: IStreamInfoH5Props) {
|
|
|
89
89
|
|
|
90
90
|
return (
|
|
91
91
|
<div className={classnames}>
|
|
92
|
-
{
|
|
92
|
+
{isMultiUserLayout ? renderGroupCallStreamInfo() : renderSingleCallStreamInfo()}
|
|
93
93
|
</div>
|
|
94
94
|
);
|
|
95
95
|
}
|
|
@@ -17,12 +17,12 @@ interface ITopBarH5Props {
|
|
|
17
17
|
export default function TopBarH5(props: ITopBarH5Props) {
|
|
18
18
|
const { callDuration } = props;
|
|
19
19
|
const {
|
|
20
|
-
|
|
20
|
+
isMultiUserLayout, callStatus, callRole, allowedMinimized,
|
|
21
21
|
} = useContext(CallInfoContext);
|
|
22
22
|
|
|
23
23
|
const { isAITranscriberEnabled } = useAIAssistant();
|
|
24
24
|
|
|
25
|
-
const showTip = (
|
|
25
|
+
const showTip = (isMultiUserLayout
|
|
26
26
|
&& (callStatus === CallStatus.CONNECTED || callRole === CallRole.CALLER));
|
|
27
27
|
const showAITranscriberSwitch = callStatus === CallStatus.CONNECTED && isAITranscriberEnabled;
|
|
28
28
|
|
|
@@ -15,7 +15,7 @@ interface ITopBarPCProps {
|
|
|
15
15
|
export default function TopBarPC(props: ITopBarPCProps) {
|
|
16
16
|
const { callDuration } = props;
|
|
17
17
|
const {
|
|
18
|
-
|
|
18
|
+
isMultiUserLayout,
|
|
19
19
|
callStatus,
|
|
20
20
|
allowedMinimized,
|
|
21
21
|
allowedFullScreen,
|
|
@@ -23,7 +23,7 @@ export default function TopBarPC(props: ITopBarPCProps) {
|
|
|
23
23
|
|
|
24
24
|
const { isAITranscriberEnabled } = useAIAssistant();
|
|
25
25
|
|
|
26
|
-
const showTime = callStatus === CallStatus.CONNECTED && !
|
|
26
|
+
const showTime = callStatus === CallStatus.CONNECTED && !isMultiUserLayout;
|
|
27
27
|
const showAITranscriberSwitch = callStatus === CallStatus.CONNECTED && isAITranscriberEnabled;
|
|
28
28
|
|
|
29
29
|
return (
|
|
@@ -8,6 +8,9 @@ interface ICallInfoContext {
|
|
|
8
8
|
isEarPhone?: boolean;
|
|
9
9
|
isClickable?: boolean;
|
|
10
10
|
isGroupCall?: boolean;
|
|
11
|
+
// Whether the UI should render in multi-user layout.
|
|
12
|
+
// Rule: true if isGroupCall, or remoteUserList size >= 2, once true it sticks until call ends.
|
|
13
|
+
isMultiUserLayout?: boolean;
|
|
11
14
|
isFloat?: boolean;
|
|
12
15
|
allowedMinimized?: boolean;
|
|
13
16
|
allowedFullScreen?: boolean;
|
|
@@ -27,7 +27,7 @@ const TUIGlobal: ITUIGlobal = TuiGlobal.getInstance();
|
|
|
27
27
|
const TUIStore: ITUIStore = TuiStore.getInstance();
|
|
28
28
|
const uiDesign = UIDesign.getInstance();
|
|
29
29
|
uiDesign.setTUIStore(TUIStore);
|
|
30
|
-
const version = '4.5.
|
|
30
|
+
const version = '4.5.11';
|
|
31
31
|
import AIAssistant from './AIAssistant'; // 仅 web 支持 AI 实时字幕
|
|
32
32
|
const frameWork = 'react';
|
|
33
33
|
export { TUIGlobal, TUIStore, uiDesign };
|
|
@@ -152,6 +152,14 @@ export default class TUICallService {
|
|
|
152
152
|
}
|
|
153
153
|
});
|
|
154
154
|
} catch (error) {
|
|
155
|
+
this._tuiCallEngine?.reportLog?.({
|
|
156
|
+
name: 'TUICallkit.initerror',
|
|
157
|
+
data: {
|
|
158
|
+
error,
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
this._isInitialized = false
|
|
162
|
+
this._tuiCallEngine = null
|
|
155
163
|
console.error(`${NAME.PREFIX}init failed, error: ${error}.`);
|
|
156
164
|
throw error;
|
|
157
165
|
}
|