개발야옹

[Web] WebRTC와 SIP를 활용하여 통화기능 구현하기 - React + Typescript 본문

프로그래밍

[Web] WebRTC와 SIP를 활용하여 통화기능 구현하기 - React + Typescript

kitez 2024. 8. 24. 15:14

WebRTC?

WebRTC(Web Real-Time Communication)는 웹 브라우저와 모바일에서 플러그인이나 추가 소프트웨어 없이 실시간으로 통신하게 해주는 기술로, WebRTC를 통해 음성 통화, 비디오 통화, 파일 공유 등을 웹 브라우저만으로 할 수 있게 해준다.

 

주요기능

1. 음성 및 비디오 통화

2. 데이터 공유

 

 

WebRTC 구성요소

1. getUserMedia: 사용자의 장치(카메라, 마이크 등)에서 미디어 스트림을 가져온다.

- 사용자에게 장치에 대한 권한을 요청하고, 비디오와 오디오 데이터를 캡쳐할 수 있다.

 

2. RTCPeerConnection: 두 브라우저 간의 직접 연결을 설정하고 관리한다.

- 피어 간의 미디어 스트림을 주고받을 수 있다.

 

3. ICE (Interactice Connectivity Establishment): 피어간의 연결을 설정하고 유지하는 데 도움을 준다.

- NAT 및 방화벽을 통과하여 두 브라우저가 서로 연결될 수 있도록 한다.

 

 

WebRTC의 작동 방식

1. 미디어 캡처 : 'getUserMedia'를 사용하여 사용자의 카메라와 마이크에서 미디어 스트림을 캡처한다

2. 피어 연결 설정: 'RTCPeerConnection'을 사용하여 두 브라우저 간의 연결을 설정한다. 이 과정에서 ICE 프로토콜을 사용하여 서로의 네트워크 환경에 맞는 최적의 경로를 찾는다.

3. 신호 교환: 두 브라우저가 서로 연결되기 위해 서버를 통해서 신호 교환을 한다.

- Offer/Answer: 한 브라우저가 연결 요청(Offer)을 보내면, 다른 브라우저는 이를 수락(Answer)한다.

- ICE Candidates: 연결을 설정하기 위해 서로의 네트워크 정보(ICE Candidates)를 교환한다.

4. 미디어 스트림 전송: 연결이 설정되면, 두 브라우저는 직접 미디어 스트림을 주고 받을 수 있다.

 

SIP?

SIP(Session Initiation Protocol)은 통신 세션을 제어하기 위한 프로토콜이다.

OSI 7계층의 응용계층에 속하며, TCP, UDP 모두 사용가능하며 Request/Response 구조이다.

인터넷전화, 멀티미디어 배포 회의가 포함이 되며 SIP는 이를 설정, 수정, 종료할 수 있는 시그널링 프로토콜이다.

 

React + Typescript 환경에서 WebRTC + SIP로 통화기능 구현하기

1. 프로젝트 생성

npx create-react-app webrtc-app --template typescript

 

 

2. 패키지 설치

npm install socket.io-client
npm install jssip // 통화 기능을 위한 라이브러리

 

 

3. Socket.io 설정

'src/socket.ts'

import { io } from 'socket.io-client';

const socket = io('http://localhost:5000');  // 서버 주소에 맞게 변경

export default socket;

 

 

4. 미디어 스트림 설정

사용자의 비디오와 오디오 스트림을 가져오는 방법 설정

// 'src/hooks/useMediaStream.ts'
import { useEffect, useState } from 'react';

const useMediaStream = () => {
    const [stream, setStream] = useState<MediaStream | null>(null);

    useEffect(() => {
        const getMediaStream = async () => {
            try {
                const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
                setStream(mediaStream);
            } catch (error) {
                console.error('Error accessing media devices.', error);
            }
        };

        getMediaStream();
    }, []);

    return stream;
};

export default useMediaStream;

 

 

5. RTCPeerConnection 설정

"RTCPeerConnection"을 설정하고 연결 과정을 관리하는 훅 생성

// 'src/hooks/usePeerConnection.ts'
import { useEffect, useRef } from 'react';
import socket from '../socket';

const usePeerConnection = (localStream: MediaStream | null) => {
    const peerConnection = useRef<RTCPeerConnection | null>(null);

    useEffect(() => {
        if (!localStream) return;

        peerConnection.current = new RTCPeerConnection({
            iceServers: [
                {
                    urls: 'stun:stun.l.google.com:19302'
                }
            ]
        });

        peerConnection.current.onicecandidate = event => {
            if (event.candidate) {
                socket.emit('ice-candidate', event.candidate);
            }
        };

        peerConnection.current.ontrack = event => {
            // 리모트 스트림을 설정하는 곳
        };

        localStream.getTracks().forEach(track => {
            peerConnection.current?.addTrack(track, localStream);
        });

        socket.on('ice-candidate', (candidate: RTCIceCandidate) => {
            peerConnection.current?.addIceCandidate(new RTCIceCandidate(candidate));
        });

        socket.on('offer', async (offer: RTCSessionDescriptionInit) => {
            if (peerConnection.current) {
                await peerConnection.current.setRemoteDescription(new RTCSessionDescription(offer));
                const answer = await peerConnection.current.createAnswer();
                await peerConnection.current.setLocalDescription(answer);
                socket.emit('answer', answer);
            }
        });

        socket.on('answer', async (answer: RTCSessionDescriptionInit) => {
            if (peerConnection.current) {
                await peerConnection.current.setRemoteDescription(new RTCSessionDescription(answer));
            }
        });

        return () => {
            peerConnection.current?.close();
            peerConnection.current = null;
        };
    }, [localStream]);

    const createOffer = async () => {
        if (peerConnection.current) {
            const offer = await peerConnection.current.createOffer();
            await peerConnection.current.setLocalDescription(offer);
            socket.emit('offer', offer);
        }
    };

    return { createOffer };
};

export default usePeerConnection;

 

6. SIP 설정

SIP서버와 연결하기 위해 필요한 설정 정의.

// src/sipConfig.ts
export const sipConfig = {
    uri: 'sip:your_sip_username@your_sip_server',
    password: 'your_sip_password',
    wsServers: 'wss://your_sip_server:8089/ws',
    display_name: 'Your Display Name'
};

 

7. SIP 초기화

SIP UA(User Agent)를 초기화하고 전화 기능 설정

// src/hooks/useSip.ts

import { useEffect, useRef } from 'react';
import JsSIP from 'jssip';
import { sipConfig } from '../sipConfig';

const useSip = () => {
    const uaRef = useRef<JsSIP.UA | null>(null);

    useEffect(() => {
        const socket = new JsSIP.WebSocketInterface(sipConfig.wsServers);
        const configuration = {
            sockets: [socket],
            uri: sipConfig.uri,
            password: sipConfig.password,
            display_name: sipConfig.display_name,
        };

        uaRef.current = new JsSIP.UA(configuration);
        uaRef.current.start();

        return () => {
            uaRef.current?.stop();
        };
    }, []);

    const makeCall = (target: string) => {
        if (uaRef.current) {
            const eventHandlers = {
                progress: () => {
                    console.log('call is in progress');
                },
                failed: () => {
                    console.log('call failed');
                },
                ended: () => {
                    console.log('call ended');
                },
                confirmed: () => {
                    console.log('call confirmed');
                },
            };

            const options = {
                eventHandlers,
                mediaConstraints: { audio: true, video: false },
            };

            uaRef.current.call(target, options);
        }
    };

    return { makeCall };
};

export default useSip;

 

6. 컴포넌트 설정

// src/App.tsx
import React, { useRef } from 'react';
import useMediaStream from './hooks/useMediaStream';
import usePeerConnection from './hooks/usePeerConnection';

const App: React.FC = () => {
    const localVideoRef = useRef<HTMLVideoElement | null>(null);
    const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
    const localStream = useMediaStream();
    const { createOffer } = usePeerConnection(localStream);

    if (localStream && localVideoRef.current && !localVideoRef.current.srcObject) {
        localVideoRef.current.srcObject = localStream;
    }

    return (
        <div>
            <video ref={localVideoRef} autoPlay playsInline muted />
            <video ref={remoteVideoRef} autoPlay playsInline />
            <button onClick={createOffer}>Start Call</button>
        </div>
    );
};

export default App;

 

728x90