Govind ganeriwal
Govind ganeriwal

Reputation: 23

call multiple people using webrtc and socket, in next js and flask

The basic idea is i need to have a button call all members and ofc in prod the members will be fetched from the DB for now have hard coded the uuid's of 3 folks

const userIds = ['728b09ef4e71433396312e3dbc6a59da <user id of the user that initiates the call> ', '946e2428086047ff99c2cdafb6072436', '5c38e1b90cfb4e04bc3dcfc6e6c7ce08']

and what i want is all of the members in that array should be able to join that call where the unique identifier would be the board uuid and the moment any user initiates the call the other folks should see a message on the screen with 2 buttons decline and accept. if he accepts he joins the call if he declines the user1/ the user who initiated the call need to see a toast that his call has been declined by "x" user

backend flask code ->

@socketio.on('connect')
def handle_connect():
    user_id = request.args.get('user_id')
    # data = request.get_json()
    # user_id = data.get('user_id')  # Or however you retrieve the user's ID
    if user_id:
        join_room(user_id)
    print(f'Client connected and joined room {user_id}')


@socketio.on('offer')
def handle_offer(data):
    """
    Relay an offer to the specified recipient.
    """
    recipient_id = data['target']
    offer = data['offer']
    # Emitting offer to the specific recipient
    socketio.emit('offer', {'offer': offer, 'user_id': request.sid}, room=recipient_id)

@socketio.on('answer')
def handle_answer(data):
    """
    Relay an answer back to the initial sender.
    """
    sender_id = data['target']
    answer = data['answer']
    # Emitting answer to the initial sender
    socketio.emit('answer', {'answer': answer, 'user_id': request.sid}, room=sender_id)

@socketio.on('ice_candidate')
def handle_ice_candidate(data):
    """
    Exchange ICE candidates between peers.
    """
    peer_id = data['target']
    candidate = data['candidate']
    # Emitting ICE candidate to the intended peer
    socketio.emit('ice_candidate', {'candidate': candidate, 'user_id': request.sid}, room=peer_id)

@socketio.on('disconnect')
def handle_disconnect():
    print('Client disconnected')

@socketio.on('start_call')
def handle_start_call(data):
    board_uuid = data.get('board_uuid')
    user_id = data.get('user_id')

    specificBoard  = None
    user = None
    try:
        user = db['sharedBoards'].find_one({"membersWithMutuallySharedBoards.user_id": user_id})
        if not user:
            return jsonify({"error": "User not found"}), 404
        else:
                    specificBoard = db['sharedBoards'].find_one({"$and": [
                        {"board_uuid": board_uuid},
                        {"membersWithMutuallySharedBoards.user_id": user_id}
                    ]})
                    if not specificBoard:
                        return jsonify({"error": "Board not found"}), 404
                    else:
                        logging.info(f"Board found for starting call: {specificBoard}")
                        # board_members = get_board_members(board_uuid)
                        for member in specificBoard['membersWithMutuallySharedBoards']:
                            logging.info(f"Member found for starting call: {member}")
                            if member['user_id'] != user_id:
                                # socketio.emit('call_started', data, room=member['user_id'])
                                 call_data = {'board_uuid': board_uuid, 'caller_id': user_id}
                                 socketio.emit('call_started', call_data, room=member['user_id'])
    except Exception as err:
        logging.error(f"Database error for finding board to start call: {err}")
        return jsonify({"error": "Internal server error"}), 500

frontend code ->


    const socket = io("http://127.0.0.1:5000", {
        query: {
            user_id: getCookiesNext("userId"), 
        },
    });
    const [localStream, setLocalStream] = useState(null);
    const [isCalling, setIsCalling] = useState(false);
    const [isAudioEnabled, setIsAudioEnabled] = useState(true);
    const [isVideoEnabled, setIsVideoEnabled] = useState(false);
    const [availableDevices, setAvailableDevices] = useState({ audio: [], video: [] });
    const videoRef = useRef(null);
    const [peerConnection, setPeerConnection] = useState(null);

    const [currentPeerUserId, setCurrentPeerUserId] = useState(null);


    useEffect(() => {
        if (!peerConnection) {
            const newPeerConnection = new RTCPeerConnection({
                iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
            });

            newPeerConnection.onicecandidate = event => {
                if (event.candidate) {
                    socket.emit('ice_candidate', {
                        target: currentPeerUserId,
                        candidate: event.candidate,
                    });
                }
            };

            newPeerConnection.ontrack = event => {
                if (remoteVideoRef.current && event.streams[0]) {
                    remoteVideoRef.current.srcObject = event.streams[0];
                }
            };

            setPeerConnection(newPeerConnection);
        }

        const setupSocketListeners = () => {
            socket.on('call_started', handleReceiveCall);
            socket.on('offer', handleOffer);
            socket.on('answer', handleAnswer);
            socket.on('ice_candidate', handleNewICECandidateMsg);
        };

        setupSocketListeners();

        return () => {
            socket.off('call_started');
            socket.off('offer');
            socket.off('answer');
            socket.off('ice_candidate');
            peerConnection?.close(); 
        };
    }, [peerConnection, currentPeerUserId]); 

    const handleNewICECandidateMsg = (data) => {
        if (peerConnection) { 
            const candidate = new RTCIceCandidate(data.candidate);
            peerConnection.addIceCandidate(candidate).catch(console.error);
        } else {
            console.error("peerConnection is not initialized when trying to add ICE candidate");
        }
    };


    useEffect(() => {
        if (localStream) {
            localStream.getTracks().forEach(track => track.stop());
        }
        fetchAvailableDevices();
        initMediaStream(isAudioEnabled, isVideoEnabled ? availableDevices.video[0]?.deviceId : null);
    }, [isAudioEnabled, isVideoEnabled]);


    useEffect(() => {
        // Before calling getUserMedia again
        if (localStream) {
            localStream.getTracks().forEach(track => track.stop());
        }

        fetchAvailableDevices();
        navigator.mediaDevices.getUserMedia({ video: true, audio: true })
            .then(stream => {
                setLocalStream(stream);
                if (videoRef.current) {
                    videoRef.current.srcObject = stream;
                }
            }).catch(console.error);

        // WebRTC: Listening for signaling
        socket.on('call_started', handleReceiveCall);
        socket.on('offer', handleOffer);
        socket.on('answer', handleAnswer);
        socket.on('ice_candidate', handleNewICECandidateMsg);

        return () => {
            socket.off('call_started');
            socket.off('offer');
            socket.off('answer');
            socket.off('ice_candidate');
        };
    }, []);
    const [currentCallerId, setCurrentCallerId] = useState()

    const fetchAvailableDevices = async () => {
        try {
            const devices = await navigator.mediaDevices.enumerateDevices();
            const audioDevices = devices.filter(device => device.kind === 'audioinput');
            const videoDevices = devices.filter(device => device.kind === 'videoinput');
            setAvailableDevices({ audio: audioDevices, video: videoDevices });
        } catch (error) {
            console.error('Error enumerating devices:', error);
        }
    };
    const [showAcceptCallButton, setShowAcceptCallButton] = useState(false)


    const handleReceiveCall = (data) => {
        console.log('Call received:', data);
        setShowAcceptCallButton(true);
        setCurrentCallerId(data.caller_id);
    };

    const addLocalStreamToPeerConnection = (stream) => {
        stream.getTracks().forEach(track => {
            peerConnection.addTrack(track, stream);
        });
    };

    const acceptCall = callerId => {
        setCurrentPeerUserId(callerId);
        if (!localStream) {
            initMediaStream(true, true).then(stream => {
                addLocalStreamToPeerConnection(stream);
                createOffer(callerId);
            });
        } else {
            addLocalStreamToPeerConnection(localStream);
            createOffer(callerId);
        }
    };

    const createOffer = (callerId) => {
        peerConnection.createOffer().then(offer => {
            return peerConnection.setLocalDescription(offer);
        }).then(() => {
            socket.emit('offer', {
                target: callerId,
                offer: peerConnection.localDescription,
            });
        }).catch(console.error);
    };


    const handleOffer = data => {
        peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer)).then(() => {
            return peerConnection.createAnswer();
        }).then(answer => {
            return peerConnection.setLocalDescription(answer);
        }).then(() => {
            socket.emit('answer', {
                target: data.caller_id,
                answer: peerConnection.localDescription,
            });
        }).catch(console.error);
    };



    const handleAnswer = data => {
        const desc = new RTCSessionDescription(data.answer);
        peerConnection.setRemoteDescription(desc).catch(console.error);
    };


    const startCall = () => {

        const user_id = getCookiesNext("userId");
        const target_user_id = "946e2428086047ff99c2cdafb6072436";
        setCurrentPeerUserId(target_user_id);

        const eventData = {
            action: 'start_call',
            board_uuid: router.query.BoardUuid,
            user_id: getCookiesNext("userId")
        };


        socket.emit('start_call', eventData);

        setIsCalling(true);
        initMediaStream(true, null).catch(console.error); // Start with audio only
    };


    useEffect(() => {
        socket.on('start_call', (data) => {
            console.log('Call received:', data);
            setShowAcceptCallButton(true);
            setCurrentCallerId(data.caller_id);
        });

        return () => {
            socket.off('start_call');
        };
    }, [socket]);

    const handleAcceptCallClick = () => {
        acceptCall(currentCallerId);
        setShowAcceptCallButton(false); 
    };


    const endCall = () => {
        if (localStream) {
            localStream.getTracks().forEach(track => track.stop());
        }
        setIsCalling(false);
        setLocalStream(null);
        setIsAudioEnabled(true);
        setIsVideoEnabled(false);
    };

    const initMediaStream = async (audio, videoDeviceId) => {
        try {
            const constraints = {
                audio,
                video: videoDeviceId ? { deviceId: { exact: videoDeviceId } } : false,
            };
            const stream = await navigator.mediaDevices.getUserMedia(constraints);
            setLocalStream(stream);
            if (videoRef.current) {
                videoRef.current.srcObject = stream;
            }
        } catch (error) {
            console.error('Error accessing user media:', error);
        }
    };


    const toggleAudio = () => {
        setIsAudioEnabled(!isAudioEnabled);
        if (localStream) {
            localStream.getAudioTracks().forEach(track => {
                track.enabled = !track.enabled;
            });
        }
    };

    const toggleVideo = () => {
        setIsVideoEnabled(!isVideoEnabled);
    };

    const changeVideoSource = deviceId => {
        initMediaStream(isAudioEnabled, deviceId).catch(console.error);
    };

  <div className="container">
                <h1>WebRTC Calling</h1>
                {isCalling ? (
                    <>
                        <div className="flex gap-5 mb-10">
                            <button onClick={toggleAudio} className="control-Button">
                                {isAudioEnabled ? <FaMicrophone /> : <FaMicrophoneSlash />}
                            </button>
                            <button onClick={toggleVideo} className="control-Button">
                                {isVideoEnabled ? <FaVideo /> : <FaVideoSlash />}
                            </button>
                        </div>
                        <div className="device-list flex flex-wrap gap-5">
                            {availableDevices.audio.map(device => (
                                <button key={device.deviceId} onClick={() => changeVideoSource(device.deviceId)} className="device-Button">
                                    {device.label || "Unknown Device"}
                                </button>
                            ))}
                            {isVideoEnabled &&
                                availableDevices.video.map(device => (
                                    <button key={device.deviceId} onClick={() => changeVideoSource(device.deviceId)} className="device-Button">
                                        {device.label || "Unknown Device"}
                                    </button>
                                ))}
                        </div>
                        <button onClick={endCall} className="mt-10">
                            <FaPhoneSlash /> End Call
                        </button>
                    </>
                ) : (
                    <button onClick={startCall} className="">
                        <FaPhone /> Start Call
                    </button>
                )}
                <div className="video-container">
                    {localStream && <video ref={videoRef} autoPlay muted className="local-video" />}
                </div>
                {/* <Button>Accept call</Button> */}

                {showAcceptCallButton && (
                    <button onClick={handleAcceptCallClick}>Accept Call</button>
                )}

            </div>

the other users are not receiving the call and neither are they actually able to talk by being in a meeting

CAN A WEB RTC WIZARD PLEASE HELP ME FIX THIS 🙏

Upvotes: 0

Views: 52

Answers (0)

Related Questions