-
Notifications
You must be signed in to change notification settings - Fork 726
Open
Description
react native when I am answering the call, call answer taking 5-6 seconds to answer. And once answered Caller can't hear the call receiver voice. all permissions granted and in debug i can see both side media stream is there, but caller cant hear any voice from receiver side
below is my code sample
import {generateUUID} from '@/utils/uuid';
import InCallManager from 'react-native-incall-manager';
import {
Invitation,
InvitationAcceptOptions,
Inviter,
InviterOptions,
Registerer,
RegistererState,
Session,
SessionState,
UserAgentOptions as UAOptions,
UserAgent, Web,
} from "sip.js";
type SessionEventCallback = (session: Session) => void;
export type SipConfig = {
uri: string;
wsServers: string;
authorizationUsername: string;
authorizationPassword: string;
logBuiltinEnabled?: boolean;
};
export class SipService {
private userAgent: UserAgent | null = null;
private registerer: Registerer | null = null;
private sessions: Map<string, Session> = new Map();
private sessionEventCallbacks: SessionEventCallback[] = [];
private hangupInProgress: Set<string> = new Set();
private isSpeakerOn: boolean = false;
private isOnHold: boolean = false;
constructor(private config: SipConfig) {
console.log("π― Initializing SipService");
}
async start() {
const {uri, wsServers, authorizationUsername, authorizationPassword, logBuiltinEnabled} = this.config;
const mediaStreamFactory: Web.MediaStreamFactory = (constraints: MediaStreamConstraints, sessionDescriptionHandler: Web.SessionDescriptionHandler): Promise<MediaStream> => {
const mediaStream = navigator.mediaDevices.getUserMedia({audio: true, video: false});
return Promise.resolve(mediaStream);
}
const userAgentOptions: UAOptions = {
uri: UserAgent.makeURI(uri),
transportOptions: {
server: wsServers,
connectionTimeout: 5000,
keepAliveInterval: 30,
keepAliveDebounce: 10
},
authorizationUsername,
authorizationPassword,
logBuiltinEnabled: logBuiltinEnabled || false,
sessionDescriptionHandlerFactoryOptions: {
constraints: navigator.mediaDevices.getUserMedia({audio: true, video: false})
},
sessionDescriptionHandlerFactory: Web.defaultSessionDescriptionHandlerFactory(mediaStreamFactory)
};
this.userAgent = new UserAgent(userAgentOptions);
this.registerer = new Registerer(this.userAgent);
// Setup event handlers
this.userAgent.transport.onConnect = () => {
console.log('SIP.js connected to server');
};
this.userAgent.transport.onDisconnect = (error?: Error) => {
console.log('SIP.js disconnected from server:', error?.message);
};
this.registerer.stateChange.addListener((newState: RegistererState) => {
console.log(`SIP.js registration state: ${newState}`);
});
// Handle incoming calls
this.userAgent.delegate = {
onInvite: (invitation: Invitation) => {
console.log('π Incoming call received');
this.handleIncomingCall(invitation);
}
};
await this.userAgent.start();
await this.registerer.register();
console.log('SIP.js started and registered successfully');
}
async stop() {
if (this.registerer) {
await this.registerer.unregister();
}
if (this.userAgent) {
await this.userAgent.stop();
}
// Stop InCallManager
InCallManager.stop();
console.log('SIP.js stopped and unregistered');
}
async call(target: string): Promise<Session> {
if (!this.userAgent) {
throw new Error('UserAgent not initialized');
}
let targetUri;
// Check if target is already a SIP URI
if (/^sips?:/i.test(target)) {
// Target is already a SIP URI, use it directly
targetUri = UserAgent.makeURI(target);
} else {
// Target is a phone number, add sip: prefix
targetUri = UserAgent.makeURI(`sip:${target}`);
}
if (!targetUri) {
throw new Error('Invalid target URI');
}
console.log(`π Initiating call to: ${targetUri}`);
// Start InCallManager for outgoing call
console.log("π Configuring audio for outgoing call");
InCallManager.start({
media: 'audio',
auto: false,
ringback: ''
});
const inviterOptions: InviterOptions = {
earlyMedia: true,
sessionDescriptionHandlerOptions: {
constraints: {
audio: true,
video: false
}
},
};
const inviter = new Inviter(this.userAgent, targetUri, inviterOptions);
const sessionId = generateUUID();
this.sessions.set(sessionId, inviter);
this.setupSessionEventHandlers(inviter, sessionId);
try {
await inviter.invite();
InCallManager.start({
media: 'audio',
auto: true,
ringback: ''
});
console.log(`β
Call successfully initiated: ${targetUri}`);
return inviter;
} catch (error) {
console.error('β Call failed:', error);
this.sessions.delete(sessionId);
throw error;
}
}
async answer(sessionId: string): Promise<void> {
const session = this.sessions.get(sessionId);
if (!session || !(session instanceof Invitation)) {
throw new Error('Session not found or not an invitation');
}
console.log(`π Answering call: ${sessionId}`);
// Stop ringtone first
InCallManager.stopRingtone();
const acceptOptions: InvitationAcceptOptions = {
sessionDescriptionHandlerOptions: {
constraints: navigator.mediaDevices.getUserMedia({audio: true, video: false})
},
};
try {
await session.accept(acceptOptions);
// Start InCallManager only once with proper configuration
InCallManager.start({
media: 'audio',
auto: true,
ringback: ''
});
console.log(`β
Call answered: ${sessionId}`);
} catch (error) {
console.error('β Call failed:', error);
this.sessions.delete(sessionId);
throw error;
}
}
async hangup(sessionId: string): Promise<void> {
if (this.hangupInProgress.has(sessionId)) {
console.log(`β οΈ Hangup already in progress for session: ${sessionId}`);
return;
}
this.hangupInProgress.add(sessionId);
try {
const session = this.sessions.get(sessionId);
if (!session) {
console.warn(`β οΈ Session ${sessionId} not found for hangup`);
return;
}
console.log(`π Hanging up call: ${sessionId}`);
if (session.state === SessionState.Established || session.state === SessionState.Establishing) {
await session.bye();
} else if (session instanceof Invitation && session.state === SessionState.Initial) {
await session.reject();
} else if (session instanceof Inviter) {
await session.cancel();
}
this.sessions.delete(sessionId);
// Stop InCallManager when call ends
InCallManager.stop();
console.log(`β
Call ended: ${sessionId}`);
} catch (error) {
console.error(`β Error during hangup: ${error}`);
} finally {
this.hangupInProgress.delete(sessionId);
}
}
async toggleSpeaker(): Promise<boolean> {
this.isSpeakerOn = !this.isSpeakerOn;
console.log(`π Toggling speaker: ${this.isSpeakerOn ? 'ON' : 'OFF'}`);
InCallManager.setSpeakerphoneOn(this.isSpeakerOn);
console.log(`β
Speaker set to: ${this.isSpeakerOn ? 'ON' : 'OFF'}`);
return this.isSpeakerOn;
}
async hold(sessionId: string): Promise<boolean> {
const session = this.sessions.get(sessionId);
if (!session) {
console.warn(`β [HOLD] No session found for ${sessionId}`);
return false;
}
if (this.isOnHold) {
console.log(`βΉοΈ [HOLD] Call ${sessionId} is already on hold, no action needed`);
return true;
}
try {
const options = {
sessionDescriptionHandlerModifiers: [Web.holdModifier]
}
await session.invite(options);
this.isOnHold = true;
console.log(`β
[HOLD] Call ${sessionId} placed on hold using SIP.js native hold`);
return true;
} catch (error) {
console.error(`β [HOLD] Failed to place call ${sessionId} on hold:`, error);
return false;
}
}
async unhold(sessionId: string): Promise<boolean> {
const session = this.sessions.get(sessionId);
if (!session) {
console.warn(`β [UNHOLD] No session found for ${sessionId}`);
return false;
}
if (!this.isOnHold) {
console.log(`βΉοΈ [UNHOLD] Call ${sessionId} is not on hold, no action needed`);
return true;
}
try {
const options = {
sessionDescriptionHandlerModifiers: []
}
await session.invite(options);
this.isOnHold = false;
console.log(`β
[UNHOLD] Call ${sessionId} resumed from hold using SIP.js native unhold`);
return true;
} catch (error) {
console.error(`β [UNHOLD] Failed to resume call ${sessionId} from hold:`, error);
return false;
}
}
// Helper method to check current hold status
isCallOnHold(): boolean {
return this.isOnHold;
}
// Helper method to get audio track status for debugging
getAudioTrackStatus(sessionId: string): { enabled: boolean; count: number } | null {
const session = this.sessions.get(sessionId);
if (!session || !session.sessionDescriptionHandler) {
return null;
}
const peerConnection = (session.sessionDescriptionHandler as any).peerConnection;
if (!peerConnection) {
return null;
}
const senders = peerConnection.getSenders();
let audioTrackCount = 0;
let enabledCount = 0;
senders.forEach((sender: any) => {
if (sender.track && sender.track.kind === 'audio') {
audioTrackCount++;
if (sender.track.enabled) {
enabledCount++;
}
}
});
return {
enabled: enabledCount > 0,
count: audioTrackCount
};
}
async mute(sessionId: string): Promise<void> {
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error('Session not found');
}
console.log(`π Muting call: ${sessionId}`);
const sessionDescriptionHandler = session.sessionDescriptionHandler;
if (sessionDescriptionHandler) {
const peerConnection = (sessionDescriptionHandler as any).peerConnection;
if (peerConnection) {
const senders = peerConnection.getSenders();
senders.forEach((sender: any) => {
if (sender.track && sender.track.kind === 'audio') {
sender.track.enabled = false;
console.log('π€ Audio track disabled');
}
});
console.log(`β
Call muted: ${sessionId}`);
}
}
}
async unmute(sessionId: string): Promise<void> {
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error('Session not found');
}
console.log(`π Unmuting call: ${sessionId}`);
const sessionDescriptionHandler = session.sessionDescriptionHandler;
if (sessionDescriptionHandler) {
const peerConnection = (sessionDescriptionHandler as any).peerConnection;
if (peerConnection) {
const senders = peerConnection.getSenders();
senders.forEach((sender: any) => {
if (sender.track && sender.track.kind === 'audio') {
sender.track.enabled = true;
console.log('π€ Audio track enabled');
}
});
console.log(`β
Call unmuted: ${sessionId}`);
}
}
}
private handleIncomingCall(invitation: Invitation) {
const sessionId = generateUUID();
console.error('π Handling incoming call with session ID:', sessionId);
this.sessions.set(sessionId, invitation);
this.setupSessionEventHandlers(invitation, sessionId);
console.log(`π Incoming call from: ${invitation.remoteIdentity?.uri}`);
// Ring for incoming call
InCallManager.startRingtone('_BUNDLE_', [500, 200, 500], 'default', 30);
// Notify listeners
this.sessionEventCallbacks.forEach(callback => callback(invitation));
}
private setupSessionEventHandlers(session: Session, sessionId: string) {
session.stateChange.addListener((newState: SessionState) => {
console.log(`π Session ${sessionId} state changed to: ${newState}`);
// Notify UI components about session state changes
this.sessionEventCallbacks.forEach(callback => callback(session));
if (newState === SessionState.Terminated) {
console.log(`π Session ${sessionId} terminated`);
this.sessions.delete(sessionId);
InCallManager.stop();
// Notify UI about session termination
this.sessionEventCallbacks.forEach(callback => callback(session));
} else if (newState === SessionState.Established) {
console.log(`π Session ${sessionId} established - call connected`);
// Notify UI about call establishment
this.sessionEventCallbacks.forEach(callback => callback(session));
}
});
// Handle session events
if (session instanceof Invitation) {
session.delegate = {
onCancel: () => {
console.log(`π Incoming call ${sessionId} was cancelled`);
InCallManager.stopRingtone();
this.sessions.delete(sessionId);
// Notify UI about cancellation
this.sessionEventCallbacks.forEach(callback => callback(session));
},
};
}
}
getSessions(): any[] {
const sessionArray: any[] = [];
this.sessions.forEach((session, sessionId) => {
const sessionInfo = {
id: sessionId,
session: session,
state: session.state,
direction: session instanceof Invitation ? 'inbound' : 'outbound',
remoteIdentity: session.remoteIdentity?.uri?.toString() || 'Unknown'
};
sessionArray.push(sessionInfo);
});
return sessionArray;
}
addSessionEventCallback(callback: SessionEventCallback) {
this.sessionEventCallbacks.push(callback);
}
removeSessionEventCallback(callback: SessionEventCallback) {
const index = this.sessionEventCallbacks.indexOf(callback);
if (index > -1) {
this.sessionEventCallbacks.splice(index, 1);
}
}
isConnected(): boolean {
return this.userAgent?.isConnected() || false;
}
isRegistered(): boolean {
return this.registerer?.state === RegistererState.Registered || false;
}
getConnectionStatus(): string {
if (!this.userAgent) return 'DISCONNECTED';
if (!this.isConnected()) return 'DISCONNECTED';
if (!this.isRegistered()) return 'UNREGISTERED';
return 'REGISTERED';
}
getRegistrationStatus(): string {
if (!this.registerer) return 'UNREGISTERED';
switch (this.registerer.state) {
case RegistererState.Initial:
return 'UNREGISTERED';
case RegistererState.Registered:
return 'REGISTERED';
case RegistererState.Unregistered:
return 'UNREGISTERED';
default:
return 'UNREGISTERED';
}
}
async decline(sessionId: string): Promise<void> {
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error('Session not found');
}
console.log(`π Declining call: ${sessionId}`);
if (session instanceof Invitation) {
await session.reject();
console.log(`β
Call declined: ${sessionId}`);
} else {
throw new Error('Cannot decline - session is not an incoming call');
}
this.sessions.delete(sessionId);
InCallManager.stopRingtone();
}
async setSpeakerMode(enabled: boolean): Promise<void> {
console.log(`π Setting speaker mode: ${enabled ? 'ON' : 'OFF'}`);
this.isSpeakerOn = enabled;
InCallManager.setSpeakerphoneOn(enabled);
console.log(`β
Speaker mode set to: ${enabled ? 'ON' : 'OFF'}`);
}
async sendDTMF(sessionId: string, digit: string): Promise<void> {
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error('Session not found');
}
console.log(`π Sending DTMF digit: ${digit} for session: ${sessionId}`);
// Check if session is established
if (session.state !== SessionState.Established) {
throw new Error('Cannot send DTMF - session is not established');
}
// Send DTMF using INFO method (RFC 2976) only
try {
await session.info({
requestOptions: {
body: {
contentDisposition: 'render',
contentType: 'application/dtmf-relay',
content: `Signal=${digit}\r\nDuration=100`
}
}
});
console.log(`β
DTMF digit sent via INFO method: ${digit}`);
} catch (error) {
console.error(`β Failed to send DTMF digit ${digit}:`, error);
throw error;
}
}
onSessionEvent(callback: SessionEventCallback): void {
this.addSessionEventCallback(callback);
}
}Metadata
Metadata
Assignees
Labels
No labels