Thanks to visit codestin.com
Credit goes to github.com

Skip to content

React Native incoimg oneway audio issueΒ #1114

@kstmostofa

Description

@kstmostofa

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

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions