FreeSwitch 기반 웹기반 실시간 VoIP 전화 시스템 구현

웹 브라우저 기반 무설치 음성 통화 시스템 설계

최근 진행된 고객의 콜센터 프로젝트에서, 좌석 운영자는 반드시 PC와 전용 전화기 조합을 사용해야 하며, 특히 PC에 별도 소프트웨어나 브라우저 플러그인 설치가 불가능하다는 강제 조건이 제시되었습니다. 이 요구사항을 만족하기 위해 고민 끝에 유일한 해결책은 현대형 브라우저 내장 기술인 WebRTC를 활용하는 것이었습니다.

이미 FreeSWITCH는 웹 기반 음성 통신을 위한 확장성을 갖춘 서버로서, WebRTC 지원이 기본 제공되며 안정적인 성능을 보입니다. 따라서 주요 작업은 서버 설정보다는 클라이언트 측의 웹 애플리케이션 개발에 집중됩니다.

WebRTC란?

WebRTC(Web Real-Time Communications)는 중간 매개체 없이 브라우저 간 직접 연결을 통해 실시간 오디오/비디오 스트리밍 및 데이터 전송을 가능하게 하는 기술입니다. 사용자는 추가적인 플러그인 없이도 음성 통화나 화상 회의를 수행할 수 있습니다.

개발 기술 스택

  • 클라이언트 라이브러리: sip.js (SIP 프로토콜 기반의 웹 통신 처리)
  • 서버: FreeSWITCH (WebRTC SIP 트랜스포트 및 통화 제어)
  • 프로토콜: WebSocket (WS/WSS), SIP over WebRTC

개발 환경 구성: 보안 허용 설정

현대 브라우저는 HTTPS 또는 WSS만을 허용하여 미디어 자원 접근을 제한합니다. 개발 단계에서는 인증서 설정이 번거롭기 때문에, 크롬 브라우저에서 임시로 비보안 출처를 허용할 수 있도록 설정할 수 있습니다.

  1. 주소창에 chrome://flags/#unsafely-treat-insecure-origin-as-secure 입력
  2. 옵션 활성화 후 재시작
  3. localhost 또는 특정 IP를 허용 목록에 추가

클라이언트 초기화 및 등록

const extId = '1001';
const extPass = '1234';

const config = {
  uri: `sip:${extId}@119.1.2.3:5066`,
  authorizationUser: extId,
  password: extPass,
  displayName: extId,
  log: {
    builtinEnabled: true,
    level: 3
  },
  transportOptions: {
    wsServers: ['ws://119.1.2.3:5066'],
    traceSip: true
  },
  hackWssInTransport: false,
  hackIpInContact: '192.168.0.2',
  userAgentString: 'webphone-client',
  registerOptions: {
    expires: 300
  }
};

const userAgent = new SIP.UA(config);

통화 시작: 183 Early Media 처리 문제 해결

기존 sip.js는 100rel 기반의 초기 미디어만 공식적으로 지원하지만, 실제 환경에서는 183 상태 응답으로 미디어 정보를 포함하는 경우가 많습니다. 이로 인해 초기 오디오가 제대로 재생되지 않는 문제가 발생했습니다.

해결을 위해 session.on('progress') 이벤트 핸들러 내부에서 직접 SDP 파싱 및 peerConnection에 미디어 트랙을 수동으로 연결하는 로직을 구현하였습니다.

session.on('progress', function (response) {
  if (response.statusCode === 183 && response.body && !this.dialog) {
    const contentType = response.getHeader('Content-Type');
    if (this.sessionDescriptionHandler.hasDescription(contentType)) {
      this.createDialog(response, 'UAC');
      this.hasAnswer = true;
      this.status = SIP.Session.C.STATUS_EARLY_MEDIA;

      this.sessionDescriptionHandler.setDescription(response.body, this.sessionDescriptionHandlerOptions)
        .catch(err => {
          console.warn('Media description error:', err);
          this.failed(response, C.causes.BAD_MEDIA_DESCRIPTION);
          this.terminate({ status_code: 488, reason_phrase: 'Bad Media Description' });
        });
    }
  }
});

수신 통화 처리

입력된 호출을 수락하거나 거부하는 로직은 ua.on('invite') 이벤트로 처리됩니다.

ua.on('invite', function (incomingSession) {
  const caller = incomingSession.remoteIdentity.uri.toString();
  const shouldAccept = confirm(`${caller}로부터의 통화입니다. 수락하시겠습니까?`);

  if (shouldAccept) {
    incomingSession.accept({
      sessionDescriptionHandlerOptions: {
        constraints: { audio: true, video: false }
      }
    });

    // 미디어 스트림 처리
    incomingSession.on('accepted', function () {
      const pc = this.sessionDescriptionHandler.peerConnection;
      const remoteStream = new MediaStream();

      if (pc.getReceivers) {
        pc.getReceivers().forEach(r => r.track && remoteStream.addTrack(r.track));
      } else {
        remoteStream.addTrack(pc.getRemoteStreams()[0].getTracks()[0]);
      }

      document.getElementById('remoteVideo').srcObject = remoteStream;
    });

    sessionall = incomingSession;
  } else {
    incomingSession.reject();
  }
});

다른 기능

  • 통화 종료 (bye) 처리
  • 통화 실패 (failed, rejected) 감지
  • 재사용 가능한 미디어 스트림 관리

본 프로젝트의 전체 소스 코드는 공개되어 있으며, 필요하신 분들은 다음 링크에서 확인하실 수 있습니다.

GitHub - WebRTC Phone Demo

태그: FreeSwitch WebRTC sip.js JavaScript VoIP

7월 17일 19:52에 게시됨