@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trtc/calls-uikit-react",
3
- "version": "4.5.9",
3
+ "version": "4.5.11",
4
4
  "main": "./tuicall-uikit-react.umd.js",
5
5
  "module": "./tuicall-uikit-react.es.js",
6
6
  "types": "./types/index.d.ts",
@@ -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
- {!isGroupCall && <SingleCall />}
269
- {isGroupCall && <GroupCall />}
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={isGroupCall
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, isGroupCall, callRole, callType, isShowEnableVirtualBackground,
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 = isGroupCall ? 'groupCall' : 'singleCall';
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
- isGroupCall,
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 { isGroupCall } = useContext(CallInfoContext);
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]: isGroupCall,
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 (isGroupCall && !isPanelOpen) {
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']]: isGroupCall,
144
- [buttonPanelH5Style['singlecall-panel']]: !isGroupCall,
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 { isGroupCall } = useContext(CallInfoContext);
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
- isGroupCall
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 { isGroupCall } = useContext(CallInfoContext);
35
+ const { isMultiUserLayout } = useContext(CallInfoContext);
36
36
  const classnames = classNames(
37
37
  [
38
38
  streamInfoH5Style['stream-info-container-h5'],
39
39
  {
40
- [streamInfoH5Style.group]: isGroupCall,
41
- [streamInfoH5Style.single]: !isGroupCall,
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
- {isGroupCall ? renderGroupCallStreamInfo() : renderSingleCallStreamInfo()}
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
- isGroupCall, callStatus, callRole, allowedMinimized,
20
+ isMultiUserLayout, callStatus, callRole, allowedMinimized,
21
21
  } = useContext(CallInfoContext);
22
22
 
23
23
  const { isAITranscriberEnabled } = useAIAssistant();
24
24
 
25
- const showTip = (isGroupCall
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
- isGroupCall,
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 && !isGroupCall;
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.9';
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
  }
package/src/index.ts CHANGED
@@ -35,7 +35,7 @@ const TUICallType = {
35
35
  AUDIO_CALL: 1,
36
36
  VIDEO_CALL: 2,
37
37
  };
38
- const Version = '4.5.9'; // basic-demo 原来上报使用
38
+ const Version = '4.5.11'; // basic-demo 原来上报使用
39
39
 
40
40
  // 输出产物
41
41
  export {