Skip to content

Commit b700b46

Browse files
committed
slice: MediaSessionInstance participant model + cleanup
1 parent 026c563 commit b700b46

3 files changed

Lines changed: 112 additions & 64 deletions

File tree

app/lib/services/voip/MediaSessionInstance.test.ts

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,27 @@ jest.mock('react-native-callkeep', () => ({
7979

8080
jest.mock('react-native-device-info', () => ({
8181
getUniqueId: jest.fn(() => 'test-device-id'),
82-
getUniqueIdSync: jest.fn(() => 'test-device-id')
82+
getUniqueIdSync: jest.fn(() => 'test-device-id'),
83+
getSystemVersion: jest.fn(() => '15.0'),
84+
getVersion: jest.fn(() => '4.0.0'),
85+
getBuildNumber: jest.fn(() => '1'),
86+
hasNotch: jest.fn(() => false),
87+
getReadableVersion: jest.fn(() => '4.0.0.1'),
88+
getBundleId: jest.fn(() => 'com.rocket.chat'),
89+
getModel: jest.fn(() => 'iPhone'),
90+
isTablet: jest.fn(() => false),
91+
default: {
92+
getUniqueId: jest.fn(() => 'test-device-id'),
93+
getUniqueIdSync: jest.fn(() => 'test-device-id'),
94+
getSystemVersion: jest.fn(() => '15.0'),
95+
getVersion: jest.fn(() => '4.0.0'),
96+
getBuildNumber: jest.fn(() => '1'),
97+
hasNotch: jest.fn(() => false),
98+
getReadableVersion: jest.fn(() => '4.0.0.1'),
99+
getBundleId: jest.fn(() => 'com.rocket.chat'),
100+
getModel: jest.fn(() => 'iPhone'),
101+
isTablet: jest.fn(() => false)
102+
}
83103
}));
84104

85105
jest.mock('../../native/NativeVoip', () => ({
@@ -105,7 +125,7 @@ type MockMediaSignalingSession = {
105125
processSignal: jest.Mock;
106126
setIceGatheringTimeout: jest.Mock;
107127
startCall: jest.Mock;
108-
getMainCall: jest.Mock;
128+
register: jest.Mock;
109129
};
110130

111131
const createdSessions: MockMediaSignalingSession[] = [];
@@ -124,7 +144,7 @@ jest.mock('@rocket.chat/media-signaling', () => ({
124144
this.processSignal = jest.fn().mockResolvedValue(undefined);
125145
this.setIceGatheringTimeout = jest.fn();
126146
this.startCall = jest.fn().mockResolvedValue(undefined);
127-
this.getMainCall = jest.fn();
147+
this.register = jest.fn();
128148
Object.defineProperty(this, 'sessionId', { value: `session-${config.userId}`, writable: false });
129149
createdSessions.push(this);
130150
})
@@ -165,9 +185,21 @@ function buildClientMediaCall(options: {
165185
const emitter = { on: jest.fn(), off: jest.fn(), emit: jest.fn() };
166186
return {
167187
callId: options.callId,
168-
role: options.role,
169188
hidden: options.hidden ?? false,
189+
localParticipant: {
190+
role: options.role,
191+
contact: { username: 'bob', sipExtension: '' },
192+
muted: false,
193+
held: false,
194+
setMuted: () => {},
195+
setHeld: () => {}
196+
},
197+
remoteParticipants: [],
198+
participants: [],
170199
reject,
200+
accept: jest.fn(),
201+
hangup: jest.fn(),
202+
sendDTMF: jest.fn(),
171203
emitter: emitter as unknown as IClientMediaCall['emitter']
172204
} as unknown as IClientMediaCall;
173205
}
@@ -505,9 +537,17 @@ describe('MediaSessionInstance', () => {
505537
newCallHandler({
506538
call: {
507539
hidden: false,
508-
role: 'caller',
509540
callId: 'c1',
510-
contact: { username: 'alice', sipExtension: '' },
541+
localParticipant: {
542+
role: 'caller',
543+
contact: { username: 'alice', sipExtension: '' },
544+
muted: false,
545+
held: false,
546+
setMuted: () => {},
547+
setHeld: () => {}
548+
},
549+
remoteParticipants: [],
550+
participants: [],
511551
emitter: { on: jest.fn(), off: jest.fn() }
512552
} as unknown as IClientMediaCall
513553
});
@@ -536,9 +576,17 @@ describe('MediaSessionInstance', () => {
536576
newCallHandler({
537577
call: {
538578
hidden: false,
539-
role: 'caller',
540579
callId: 'c1',
541-
contact: { username: 'alice', sipExtension: '' },
580+
localParticipant: {
581+
role: 'caller',
582+
contact: { username: 'alice', sipExtension: '' },
583+
muted: false,
584+
held: false,
585+
setMuted: () => {},
586+
setHeld: () => {}
587+
},
588+
remoteParticipants: [],
589+
participants: [],
542590
emitter: { on: jest.fn(), off: jest.fn() }
543591
} as unknown as IClientMediaCall
544592
});
@@ -557,9 +605,17 @@ describe('MediaSessionInstance', () => {
557605
newCallHandler({
558606
call: {
559607
hidden: false,
560-
role: 'caller',
561608
callId: 'c1',
562-
contact: { username: 'alice', sipExtension: '100' },
609+
localParticipant: {
610+
role: 'caller',
611+
contact: { username: 'alice', sipExtension: '100' },
612+
muted: false,
613+
held: false,
614+
setMuted: () => {},
615+
setHeld: () => {}
616+
},
617+
remoteParticipants: [],
618+
participants: [],
563619
emitter: { on: jest.fn(), off: jest.fn() }
564620
} as unknown as IClientMediaCall
565621
});
@@ -570,14 +626,17 @@ describe('MediaSessionInstance', () => {
570626

571627
it('answerCall resolves roomId from DM for non-SIP callee', async () => {
572628
mockGetDMSubscriptionByUsername.mockResolvedValue({ rid: 'dm-rid' } as any);
573-
mediaSessionInstance.init('user-1');
574-
const session = createdSessions[0];
575-
const mainCall = {
629+
const calleeCall = buildClientMediaCall({ callId: 'call-ans', role: 'callee' });
630+
mockUseCallStoreGetState.mockReturnValue({
631+
reset: mockCallStoreReset,
632+
setCall: jest.fn(),
633+
setRoomId: mockSetRoomId,
634+
resetNativeCallId: jest.fn(),
635+
call: calleeCall,
576636
callId: 'call-ans',
577-
accept: jest.fn().mockResolvedValue(undefined),
578-
contact: { username: 'bob', sipExtension: '' }
579-
};
580-
session.getMainCall.mockReturnValue(mainCall);
637+
nativeAcceptedCallId: null,
638+
roomId: null
639+
});
581640

582641
await mediaSessionInstance.answerCall('call-ans');
583642

@@ -586,14 +645,18 @@ describe('MediaSessionInstance', () => {
586645
});
587646

588647
it('answerCall skips DM lookup for SIP contact', async () => {
589-
mediaSessionInstance.init('user-1');
590-
const session = createdSessions[0];
591-
const mainCall = {
648+
const sipCall = buildClientMediaCall({ callId: 'call-sip', role: 'callee' });
649+
sipCall.localParticipant.contact.sipExtension = 'ext';
650+
mockUseCallStoreGetState.mockReturnValue({
651+
reset: mockCallStoreReset,
652+
setCall: jest.fn(),
653+
setRoomId: mockSetRoomId,
654+
resetNativeCallId: jest.fn(),
655+
call: sipCall,
592656
callId: 'call-sip',
593-
accept: jest.fn().mockResolvedValue(undefined),
594-
contact: { username: 'bob', sipExtension: 'ext' }
595-
};
596-
session.getMainCall.mockReturnValue(mainCall);
657+
nativeAcceptedCallId: null,
658+
roomId: null
659+
});
597660

598661
await mediaSessionInstance.answerCall('call-sip');
599662

app/lib/services/voip/MediaSessionInstance.ts

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ class MediaSessionInstance {
4848
iceGatheringTimeout: this.iceGatheringTimeout
4949
})
5050
);
51-
// TESTING: DDP signal transport — offer/answer/ICE stay on DDP
5251
mediaSessionStore.setSendSignalFn((signal: ClientMediaSignal) => {
5352
sdk.methodCall('stream-notify-user', `${userId}/media-calls`, JSON.stringify(signal));
5453
});
@@ -66,15 +65,13 @@ class MediaSessionInstance {
6665
console.error('[VoIP] Failed to fetch initial state signals:', error);
6766
}
6867

69-
// TESTING: DDP register side effects vs REST stateSignals — server renewCallId/hangupDetachedCall/onCallTrying still fire
7068
instance.register(false);
7169
}
7270

7371
this.mediaSessionStoreChangeUnsubscribe = mediaSessionStore.onChange(() => {
7472
this.instance = mediaSessionStore.getInstance(userId);
7573
});
7674

77-
// TESTING: DDP real-time signal subscription — stays for offer/answer/ICE/notifications
7875
this.mediaSignalListener = sdk.onStreamData('stream-notify-user', (ddpMessage: IDDPMessage) => {
7976
if (!this.instance) {
8077
return;
@@ -86,8 +83,6 @@ class MediaSessionInstance {
8683
const signal = ddpMessage.fields.args[0];
8784
this.instance.processSignal(signal);
8885

89-
console.log('🤙 [VoIP] Processed signal:', signal);
90-
9186
// Answer when native already accepted and stream matches device contract + callId.
9287
const storeSlice = useCallStore.getState();
9388
const { call, nativeAcceptedCallId } = storeSlice;
@@ -107,17 +102,13 @@ class MediaSessionInstance {
107102

108103
this.instance?.on('newCall', ({ call }: { call: IClientMediaCall }) => {
109104
if (call && !call.hidden) {
110-
call.emitter.on('stateChange', oldState => {
111-
console.log(`📊 ${oldState}${call.state}`);
112-
console.log('🤙 [VoIP] New call data:', call);
113-
});
105+
call.emitter.on('stateChange', () => {});
114106

115-
// role/contact removed in 0.2.0-rc.0 library, migrated from 0.1.3 API
116-
if ((call as any).role === 'caller') {
107+
if (call.localParticipant.role === 'caller') {
117108
useCallStore.getState().setCall(call);
118109
Navigation.navigate('CallView');
119110
if (useCallStore.getState().roomId == null) {
120-
this.resolveRoomIdFromContact((call as any).contact).catch(error => {
111+
this.resolveRoomIdFromContact(call.localParticipant.contact).catch(error => {
121112
console.error('[VoIP] Error resolving room id from contact (newCall):', error);
122113
});
123114
}
@@ -133,24 +124,17 @@ class MediaSessionInstance {
133124
public answerCall = async (callId: string) => {
134125
const { call: existingCall } = useCallStore.getState();
135126
if (existingCall != null && existingCall.callId === callId) {
136-
console.log('[VoIP] answerCall skipped — call already bound in store:', callId);
137127
return;
138128
}
139129

140-
console.log('[VoIP] Answering call:', callId);
141-
// @ts-expect-error — getMainCall is private in 0.2.0-rc.0 library, migrated from 0.1.3 API
142-
const mainCall = this.instance?.getMainCall();
143-
console.log('[VoIP] Main call:', mainCall);
130+
const call = useCallStore.getState().call;
144131

145-
if (mainCall && mainCall.callId === callId) {
146-
console.log('[VoIP] Accepting call:', callId);
147-
await mainCall.accept();
148-
console.log('[VoIP] Setting current call active:', callId);
132+
if (call && call.callId === callId) {
133+
await call.accept();
149134
RNCallKeep.setCurrentCallActive(callId);
150-
useCallStore.getState().setCall(mainCall);
135+
useCallStore.getState().setCall(call);
151136
Navigation.navigate('CallView');
152-
// contact removed in 0.2.0-rc.0 library, migrated from 0.1.3 API
153-
this.resolveRoomIdFromContact((mainCall as any).contact).catch(error => {
137+
this.resolveRoomIdFromContact(call.localParticipant.contact).catch(error => {
154138
console.error('[VoIP] Error resolving room id from contact (answerCall):', error);
155139
});
156140
} else {
@@ -159,7 +143,6 @@ class MediaSessionInstance {
159143
if (st.nativeAcceptedCallId === callId) {
160144
st.resetNativeCallId();
161145
}
162-
console.warn('[VoIP] Call not found:', callId); // TODO: Show error message?
163146
}
164147
};
165148

@@ -173,19 +156,17 @@ class MediaSessionInstance {
173156

174157
public startCall = (userId: string, actor: CallActorType) => {
175158
requestPhoneStatePermission();
176-
console.log('[VoIP] Starting call:', userId);
177159
this.instance?.startCall(actor, userId);
178160
};
179161

180162
public endCall = (callId: string) => {
181-
// @ts-expect-error — getMainCall is private in 0.2.0-rc.0 library, migrated from 0.1.3 API
182-
const mainCall = this.instance?.getMainCall();
163+
const call = useCallStore.getState().call;
183164

184-
if (mainCall && mainCall.callId === callId) {
185-
if (mainCall.state === 'ringing') {
186-
mainCall.reject();
165+
if (call && call.callId === callId) {
166+
if (call.state === 'ringing') {
167+
call.reject();
187168
} else {
188-
mainCall.hangup();
169+
call.hangup();
189170
}
190171
}
191172
RNCallKeep.endCall(callId);
@@ -230,7 +211,6 @@ class MediaSessionInstance {
230211
const currentIceServers = this.getIceServers();
231212
if (currentIceServers !== this.iceServers) {
232213
this.iceServers = currentIceServers;
233-
// this.instance?.setIceServers(this.iceServers);
234214
}
235215
});
236216
}

app/lib/services/voip/mockCall.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const DEFAULT_CONTACT = {
2828
/**
2929
* Build a fake `IClientMediaCall` good enough to render `CallView` without a real SIP/WebRTC stack.
3030
* No-op `setMuted/setHeld/hangup/sendDTMF` and a no-op event emitter so store subscriptions are safe.
31+
* Uses the 0.2.0-rc.0 participant model (localParticipant + remoteParticipants).
3132
*/
3233
export function createMockCall(overrides: MockCallOverrides = {}): IClientMediaCall {
3334
const contact = { ...DEFAULT_CONTACT, ...overrides.contact };
@@ -36,13 +37,17 @@ export function createMockCall(overrides: MockCallOverrides = {}): IClientMediaC
3637
const mock = {
3738
callId: 'mock-call-id',
3839
state: callState,
39-
muted: overrides.isMuted ?? false,
40-
held: overrides.isOnHold ?? false,
41-
remoteMute: false,
42-
remoteHeld: false,
43-
contact,
44-
setMuted: () => {},
45-
setHeld: () => {},
40+
localParticipant: {
41+
role: 'caller' as const,
42+
contact,
43+
muted: overrides.isMuted ?? false,
44+
held: overrides.isOnHold ?? false,
45+
setMuted: () => {},
46+
setHeld: () => {}
47+
},
48+
remoteParticipants: [],
49+
participants: [],
50+
accept: () => {},
4651
hangup: () => {},
4752
reject: () => {},
4853
sendDTMF: () => {},

0 commit comments

Comments
 (0)