Pjsip-pjsua python

From 탱이의 잡동사니
Jump to: navigation, search

Contents

Overview

Psjua 는 그자체로도 이미 훌륭한 SIP client 이지만 본격적인 진가는 지원 API 가 아닐까 싶다. C/Python 으로 library 모듈을 지원하는데, 이를 사용하면 강력한 SIP 테스팅 툴을 만들 수 있기 때문이다. 여기서는 pjsua-python 모듈을 사용한 프로그램 제작을 설명한다..

Pjsua-python module은 Signaling, media, Call control API, account management, buddy list management, presence, instant messaging, local conferencing, file streaming, local playback, voice recording, NAT traversal(STUN, TURN, ICE) 등을 지원한다.

전체 프로그래밍 가이드는 이곳[1] 에서 확인할 수 있다.

Installation

당연한 말이겠지만 pjsua-python 모듈 사용을 위해서는 해당 모듈 설치가 필요하다.

Linux

$ cd your-pjsip-root-dir
$ ./configure && make dep && make
$ cd pjsip-apps/src/python
$ sudo make

Mobiles

IOS, Android 를 비롯한 모바일 운영체제 역시 지원한다. 자세한 내용은 다음를 참조하자. IOS[2] , Android[3] , BlabkBerry[4] , Windows_Mobile[5] , Windows_phone_8[6] , Symbian[7]

Others

자세한 설치 내용은 이곳[8]을 참조하자.

Simple example

P2P 다이얼 방식으로 전화를 거는 예제. Asterisk 설정이 되어 있지 않다면 당연히 오류가 난다. 오류 메시지를 확인하며 정상적인 오류 메시지가 나타나는지를 확인하면 된다. Call is DISCONNCTD last code = 503 (Service Unavailable) 메시지가 나타나야 한다.

  • Code
import sys
import pjsua as pj
 
# Logging callback
def log_cb(level, str, len):
    print str,
 
# Callback to receive events from Call
class MyCallCallback(pj.CallCallback):
    def __init__(self, call=None):
        pj.CallCallback.__init__(self, call)
 
    # Notification when call state has changed
    def on_state(self):
        print "Call is ", self.call.info().state_text,
        print "last code =", self.call.info().last_code, 
        print "(" + self.call.info().last_reason + ")"
 
    # Notification when call's media state has changed.
    def on_media_state(self):
        global lib
        if self.call.info().media_state == pj.MediaState.ACTIVE:
            # Connect the call to sound device
            call_slot = self.call.info().conf_slot
            lib.conf_connect(call_slot, 0)
            lib.conf_connect(0, call_slot)
            print "Hello world, I can talk!"
 
 
# Check command line argument
if len(sys.argv) != 2:
    print "Usage: simplecall.py <dst-URI>"
    sys.exit(1)
 
try:
    # Create library instance
    lib = pj.Lib()
 
    # Init library with default config
    lib.init(log_cfg = pj.LogConfig(level=3, callback=log_cb))
 
    # Create UDP transport which listens to any available port
    transport = lib.create_transport(pj.TransportType.UDP)
 
    # Start the library
    lib.start()
 
    # Create local/user-less account
    acc = lib.create_account_for_transport(transport)
 
    # Make call
    call = acc.make_call(sys.argv[1], MyCallCallback())
 
    # Wait for ENTER before quitting
    print "Press <ENTER> to quit"
    input = sys.stdin.readline().rstrip("\r\n")
 
    # We're done, shutdown the library
    lib.destroy()
    lib = None
 
except pj.Error, e:
    print "Exception: " + str(e)
    lib.destroy()
    lib = None
    sys.exit(1)
  • Result
pchero@mywork:~/workspace/Study/Program/pjsip/scripts/pjsip_samples$ python simple.py sip:201@127.0.0.1
14:54:35.316 os_core_unix.c !pjlib 2.3 for POSIX initialized
14:54:35.316 sip_endpoint.c  .Creating endpoint instance...
14:54:35.316          pjlib  .select() I/O Queue created (0x1ff0250)
14:54:35.316 sip_endpoint.c  .Module "mod-msg-print" registered
14:54:35.316 sip_transport.  .Transport manager created.
14:54:35.316   pjsua_core.c  .PJSUA state changed: NULL --> CREATED
ALSA lib pcm_dsnoop.c:618:(snd_pcm_dsnoop_open) unable to open slave
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2239:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
bt_audio_service_open: connect() failed: Connection refused (111)
bt_audio_service_open: connect() failed: Connection refused (111)
bt_audio_service_open: connect() failed: Connection refused (111)
bt_audio_service_open: connect() failed: Connection refused (111)
ALSA lib pcm_dmix.c:1022:(snd_pcm_dmix_open) unable to open slave
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
14:54:35.340   pjsua_core.c  .pjsua version 2.3 for Linux-3.13.0.43/x86_64/glibc-2.19 initialized
Expression 'paInvalidSampleRate' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 2048
Expression 'PaAlsaStreamComponent_InitialConfigure( &self->capture, inParams, self->primeBuffers, hwParamsCapture, &realSr )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 2719
Expression 'PaAlsaStream_Configure( stream, inputParameters, outputParameters, sampleRate, framesPerBuffer, &inputLatency, &outputLatency, &hostBufferSizeMode )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 2843
Expression 'paInvalidSampleRate' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 2048
Expression 'PaAlsaStreamComponent_InitialConfigure( &self->capture, inParams, self->primeBuffers, hwParamsCapture, &realSr )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 2719
Expression 'PaAlsaStream_Configure( stream, inputParameters, outputParameters, sampleRate, framesPerBuffer, &inputLatency, &outputLatency, &hostBufferSizeMode )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 2843
Call is  CALLING last code = 0 ()
Press <ENTER> to quit

Call is  DISCONNCTD last code = 503 (Service Unavailable)
python: ../src/pjsua-lib/pjsua_call.c:1862: pjsua_call_get_user_data: Assertion `call_id>=0 && call_id<(int)pjsua_var.ua_cfg.max_calls' failed.
Aborted (core dumped)

Basic concepts

Asynchronous operations

Pjsip 에서는 Sending/Receiving 관련 항목들은 모두 Async 로 동작한다. 무슨뜻인가 하면, Sending/Receiving 함수를 호출하면 바로 리턴이 돌아온다는 뜻이다. 그리고 실제로 동작잉 끝났을 경우, 함께 등록한 Callback 함수가 동작하게 된다.

Account 클래스의 make_call() 메소드를 예로 들어보자. 이 함수는 외부로 발신할때 사용하는 함수이다. 이 함수를 호출 후, 정상적인 Return 을 받았을 때, 정상적으로 콜이 발신되었다는 것을 의미하지는 않는다. 단지 정상적으로 initiated 되었다는 것을 의미할 뿐이다. 실제로는 initiated 후 진행되는 사항들이 많을 것이다. 나중에 정상적인 발신이 되었을 경우/진행사항을 확인할 경우, CallCallback 클래스의 on_state() 메소드를 통해서 이를 알아볼 수 있다.

make_call(self, dst_uri, cb=None, hdr_list=None)
    Make outgoing call to the specified URI.
 
    Keyword arguments:
    dst_uri  -- Destination SIP URI.
    cb       -- CallCallback instance to be installed to the newly
                created Call object. If this CallCallback is not
                specified (i.e. None is given), it must be installed
                later using call.set_callback().
    hdr_list -- Optional list of headers to be sent with outgoing
                INVITE
 
    Return:
        Call instance.

Relationship between objects and handles

Pjsua 에서는 Account, call, buddy 와 관련된 항목들을 handle 로써 관리한다. 그리고 Python 모듈에서는 이를 다시 Class로 감싸는 형식으로 되어있다. 때문에 아래와 같이 단순히 해당 Object 들을 delete 하는 것 만으로는 정상적인 객체 삭제가 이루어지지 않는다.

del acc
del call
del buddy

따라서, 해당 Object들을 삭제하고 싶다면 다음과 같이 해야한다.

acc.delete()
del acc
call.hangup()
del call
buddy.delete
del buddy

The main classes

Lib class
Library 의 main class 이다. 반드시 단 하나의 객체만이 생성되도록 해야한다. 생성된 객체를 초기화하고 프로그램을 시작하게 된다. 또한 다른 객체(accounts, transports)들도 여기로부터 생성된다.

Transport class
Socket 관리 클래스. Send/Receive 를 하기 위해서는 하나 이상의 Transport 객체가 필요하다.

Account class
Endpoint 정보를 관리하는(SIP 계정 정보) 클래스. 발신/수신, buddy 기능 사용 전 최소 하나 이상의 클래스 Object가 생성되어야만 한다.

Call class
이 클래스는 콜을 관리하는데 사용된다. Answer/Hangup/Hold/Transfer/Etc..

Bussy class
자신의 상태 정보를 등록되어 있는 Buddy 들에게 전송할 때 사용한다.

Basic usage pattern

위에 나열된 클래스들을 사용하면 쉽게 명령 메소드들을 호출할 수 있다. 하지만 어떻게 events/notifications 들을 받아서 처리할 수 있을까?

답은 Callback 클래스이다.

위에 나열된 각각의 클래스들은(Lib 클래스 제외) 객체를 생성할 때 등롣한 Callback 객체를 통해 응답한다. 따라서 만약 events/notifications 등을 받아서 처리하고 싶다면 각각의 Callback 클래스(AccountCallback[9], CallCallback[10], BuddyCallback[11])들을 상속받아 적절한 메소드들을 구현한 다음에 해당 클래스에 Callback 객체로 등록하면 된다.

말이 좀 어려운데 이는 차차 살펴보기로 한다.

Error handling

Error 발생시, Pjsua 에서는 pjsua.Error 오류를 raise 한다.

import pjsua
 
try:
    call = acc.make_call('sip:buddy@example.org')
except pjsua.Error, err:
    print 'Exception has occured:', err
except:
    print 'Ouch..'

Threading and Concurrency

Pjsua는 자체 worker thread를 통해서 polling thread 를 지원하다(이미 내부적으로 Polling thread를 사용하고 있다). 따라서 Thread를 사용하기 위해서는 이를 위한 Callback 함수가 미리 준비되어 있어야 한다. 그리고 pjsua 모듈은 반드시 thread-safe 되어야 한다. 내부적으로 pjsua 모듈은 재진입이 불가능하다. 이는 deadlock 을 방기하기 위함인데, 이를 위해 단 하나의 Locking 을 사용한다.

lib.auto_lock() 은 library lock 읃 얻고, 해제하는데 사용되는 메소드이다. 한번 생성된 lock은 자동으로 해제되며 이를 사용하기 위해서는 다음과 같이 사용하면 된다.

def my_function():
    # this will acquire library lock:
    lck = pjsua.Lib.instance().auto_lock()
    # and when lck is destroyed, the lock will be released

Startup and shutdown

The Lib class

Lib class 는 싱글톤 클래스이다. 그리고 프로그램 작성시 가장먼저 생성되어야만 하며, 반드시 한개만의 객체 Object 만 생성해야 한다. 이 클래스는 Pjsua 모듈의 핵심이며 다음과 같은 기능을 제공한다.

  • pjsua library 초기화/종료
  • Customized pjsua 코어 설정.(SIP settings, media settings, logging settings)
  • Media 설정

Initialization

Instantiate the library

Pjsua library 사용 전 다음과 같이 객체 생성을 하자.

import pjsua
 
lib = Lib()

한번 객체를 생성하고나면 그 이후부터는 Lib.instance()[12] 를 통해 언제든지 참조가 가능하다.

Initialize the Library

Library 초기화는 init()[13] 메소드로 수행한다.

try:
    lib.init()
except pjsua.Error, err:
    print 'Initialization error:', err

초기화 수행중 Error 가 발생하면 exception raise 를 일으킨다. 에러 처리를 위해 try/except 를 잊지 말자. init() 메소드는 코어 설정을 위해 다음과 같은 인자들을 받는다.

  • UAConfig: SIP user agent settings
  • MediaConfig : Media settings including ICE and TURN
  • LogConfig : Customize logging settings.
try:
    my_ua_cfg = pjsua.UAConfig()
    my_ua_cfg.stun_host = "stun.pjsip.org"
    my_media_cfg = pjsua.MediaConfig()
    my_media_cfg.enable_ice = True
 
    lib.init(ua_cfg=my_ua_cfg, media_cfg=my_media_cfg)
 
except pjsua.Error, err:
    print 'Initialization error:', err

Create one or More Transports

Pjsua 프로그램 작성시, 하나 이상의 Transport 객체가 필요하다. Transport 객체가 있어야만 SIP message 의 Send/Receive 가 가능하기 때문이다.

try:
    udp = lib.create_transport(pj.TransportType.UDP)
except pj.Error, e:
    print "Error creating transport:", e

create_transport() 메소드는 Transport instance 를 돌려주는데, TransportConfig object 를 통해 추가적인 설정이 가능하다(Adreess 범위 지정, Listen port 지정). 별도로 TransportConfig object 를 지정하지 않으면 Default 값으로 INADDR_ANY 와 아무 Port 번호가 지정된다.

사실 아래의 경우를 제외하고Transport instance 가 직접적으로 사용되는 경우는 없다.

  • create userless account(with lib.create_account_for_transport())
  • display list of transports to user

Start the Library

이제 library 시작을 위한 준비가 끝났다. Library 시작을 위해서는 lib.start() 메소드를 호출하면 된다.

try:
    lib.start()
except pj.Error, e:
    print "Error starting pjsua:", e

Shutting Down the Library

프로그램 종료시, library 을 shutdown 해주어야 한다. 그래야만 사용되었던 자원들을 반환하기 때문이다. 간단히 lib.destroy() 메소드를 호출해주면 된다.

lib.destroy()
lib = None

SIP Accounts

Accounts 는 흔히 SIP account로도 여겨지는데, 여기서는 프로그램을 사용중인 User 를 의미한다. Account는 SIP URI(Uniform Resource Identifier)로 표현되며, SIP 프로토콜에서 From header 로 사용된다.

Account 는 그 자체 주소를 이용해서 Registration 이 가능한데, Registraion Server 의 설정에 따라 인증, 상태 메시지 설정 등이 가능할 수 있다.

만약 발신을 하고자한다면, 최소 하나 이상의 Account가 반드시 생성되어야 한다. 그리고 User 가 필요없는 상황이라면 lib.create_account_for_transport() 메소드 호출을 통해서 User-less account 설정도 가능하다. User-less account 는 account 설정 대신 local-endpoint 를 설정한다.

Creating Userless Accounts

User-less account 는 User 대신 사용하게되는 SIP Endpoint 이다. 특정 Softphone 들은 이 User-less 방식으로 Peer-to-peer 발신이 가능한데, User id 대신 등록된 Address 에 있는 컴퓨터로 바로 발신을 시도하는 방식이다.

예를 들면, sip:bennylp@pjsip.org 대신 sip:192.168.0.15 를 사용하는 것이다.

Pjsua에서 User-less account를 생성시, transport instance와 같이 연계되서 생성되어야 하는데, 구체적인 생성 방법은 다음과 같다.

acc_cb = MyAccountCallback()
acc = lib.create_account_for_transport(udp, cb=acc_cb)

Callback 메소드는 나중에 설명할텐데, 기본적으로 Account 에서 보내는 Incoming Call 같은 Event 들을 받아서 처리하는 모듈이라고 생각하면 된다.

이런 방식(User-less)으로 생성된 Account는 연동된 Transport address 의존적적인 URI를 가지게 된다. 예를 들어, 만약 Transport Address 가 "192.168.0.15:5080" 이라면, 생성된 Account의 URI는 "sip:192.168.0.15:5080", "sip:192.168.0.15:5080;transport=tcp"이 된다.

Creating Account

User-less 가 아닌 보통의 Account를 생성하고자 한다면 AccountConfig 를 설정하고 lib.create_account() 메소드를 호출하면 된다.

Account 생성시 필요한 최소한의 정보는 Account ID 이다(URI로 표현된.. 혹은 Called address of Record/AOR).

try:
    acc_cfg = pjsua.AccountConfig()
    acc_cfg.id = "sip:user@pjsip.org"
 
    acc_cb = MyAccountCallback()
    acc = lib.create_account(acc_cfg, cb=acc_cb)
 
except pjsua.Error, err:
    print "Error creating account:", err

callback 은 나중에 설명이 나오니, 지금은 그냥 넘어가도록 하자.

위의 코드에서 생성한 Account 는 발신시 From header 에 Account 정보가 설정되는 것 말고는 아무것도 하지 않는다. 즉, SIP Registration 도 하지 않는다는 것이다.

Incoming Call 을 수신하기 위해서는 SIP Registration 이 필요한데, 이를 위해서는 AccountConfig instance 에 조금 더 설정을 해주어야 한다.

try:
    acc_cfg = pjsua.AccountConfig()
    acc_cfg.id = "sip:someuser@pjsip.org"
    acc_cfg.reg_uri = "sip:pjsip.org"
    acc_cfg.proxy = [ "sip:pjsip.org;lr" ]
    acc_cfg.auth_cred = [ AuthCred("*", "someuser", "secretpass") ]
 
    acc_cb = MyAccountCallback()
    acc = lib.create_account(acc_cfg, cb = acc_cb)
 
except pjsua.Error, err:
    print "Error creating account:", err

혹은 다음과 같이 생성할 수도 있다.

acc_cfg = pjsua.AccountConfig("pjsip.org", "someuser", "secretpass")

More on Account Settings

AccountConfig 에서 설정한

  • Whether reliable provisional response (SIP 100rel) is required
  • Whether presence publication (PUBLISH) is enabled
  • Secure RTP(SRTP) related settings
  • etc..

여기[14]를 참조하면 AccountConfig에 관한 더 자세한 정보를 얻을 수 있다.

Account Operations

Account Ojbect 를 이용하면 다음의 작업들을 수행할 수 있다.

  • 외부 발신 콜
  • buddy object 추가
  • Account 상태 메시지 설정
  • Start/Stop SIP registration

Account Callbacks

Account object 들은 여러가지 Event들을 리턴하는데, 다음과 같다.

  • Status of SIP registration
  • Incoming calls
  • Incoming presence subscription requests
  • Incoming instant message not from buddy

이런 Evnet 들은 Instance 생성시에 등록한 Callback 함수로 전달되는데 이 Callback 함수는 AccountCallback 클래스를 Override 하여 사용된다. 다음 예제를 보자.

class MyAccountCallback(pjsua.AccountCallback):
    def __init__(self, account=None):
        pjsua.AccountCallback.__init__(self, account)
 
    def on_incoming_call(self, call):
        call.hangup(501, "Sorry, not ready to accept calls yet")
 
    def on_reg_state(self):
        print "Registration complete, status=", self.account.info().reg_status, "(" + self.account.info().reg_reason + ")"

Account Callback 에서 Account instance 를 참조하고 싶다면 self.account 를 참조하면 된다.

Installing Your Callback to Account Object

Callback 클래스를 생성하면 이를 생성하고자 하는 Account instance 에 같이 등록하면된다. 다음과 같이 사용한다.

my_cb = MyAccountCallback()
acc = lib.create_account_for_transport(udp, cb=mycb)

or

acc_cfg = pjsua.AccountConfig("pjsip.org", "someuser", "secretpass")
my_cb = MyAccountCallback()
acc = lib.create_account(acc_cfg, cb=my_cb)

보통의 경우, 위의 방법처럼 Account instance 생성과 동시에 Callback 함수를 등록한다. 하지만 특별한 이유로 Callback를 나중에 등록해야만 한다면 다음과 같이 set_callback() 함수를 이용하여 Callback 함수를 나중에 등록하는 방법도 있다. 하지만 이와 같은 방법을 사용할 경우, Callback 함수를 등록하기까지의 이벤트 발생을 잃어버리지 않기 위해 Lock 을 설정하는 것이 중요한다. 다음의 예제를 보자.

# Create account without callback. Protect the fragment with the
# library lock below
lck = lib.auto_lock()
acc_cfg = pjsua.AccountConfig("pjsip.org", "someuser", "secretpass")
acc = lib.create_account(acc_cfg)
 
# Do something else
...
 
# Create and install the callback
my_cb = MyAccountCallback()
acc.set_call_back(my_cb)
 
# Release library lock
del lck

만약 위와 같이 Lock 을 설정하지 않는다면 최초 Account 생성 후, Callback 함수를 등록하기까지의 Event를 잃어버릴 수 있다.

하지만 마찬가지의 이유로, Lock을 설정하게 되면 worker thread 가 멈추게 되고, 인입되는 SIP Message 들은 Socket Buffer Queue에 적재되며, 만약 해당 메시지의 Timer가 초과되었을 경우 해당 메시지는 삭제되기도 한다. 따라서 Lock을 사용할 때는 많은 주의를 요한다.

Registration sample

Registration 을 하는 예제 코드이다. 원본은 이곳[15]에서 확인할 수 있다.

import sys
import pjsua as pj
import threading
 
def log_cg(level, str, len):
    print str,
 
class MyAccountCallback(pj.AccountCallback):
    sem = None
 
    def __init__(self, account):
        pj.AccountCallback.__init__(self, account)
 
    def wait(self):
        self.sem = threading.Semaphore(0)
        self.sem.acquire()
 
    def on_reg_state(self):
        if self.sem:
            if self.account.info().reg_status >= 200:
                self.sem.release()
 
lib = pj.Lib()
 
try:
    lib.init(log_cfg = pj.LogConfig(level=4, callback=log_cb))
    lib.create_transport(pj.TransportType.UDP, pj.TransportConfig(5080))
    lib.start()
 
    acc = lib.create_account(pj.AccountConfig("pjsip.org", "bennylp", "***"))
 
 
    acc_cb = MyAccountCallback(acc)
    acc.set_callback(acc_cb)
    acc_cb.wait()
 
    print "\n"
    print "Registration complete, status=", acc.info().reg_status, "(" + acc.info().reg_reason + ")"
    print "\nPress ENTER to quit"
    sys.stdin.readline()
 
    lib.destroy()
    lib = None
 
except pj.Error, e:
    print "Exception: " + str(e)
    lib.destroy()

Media Concepts

Media Objects

Media object는 Pjsua에서 Media 를 생성하거나 수신하는 Object이다.

Pjsua 에서는 여러가지 타입의 Media 를 지원하는데, 다음과 같다.

  • Call, obviously, to transmit and receive media to/from remote person.
  • WAV file player to play WAV file.
  • WAV file recorder to record audio to a WAV file.
  • WAV playlist, to playback multiple WAV files sequentially.

여기서는 기본적인 Media Concept 만을 소개한다. 자세한 사용 설명은 다음 장에 계속된다.

The Conference Bridge

Pjsua 에서 모든 Media object 들은 Central conference bridge 에서 모여진다. 때문에 관리가 편하다. 각각의 Media Object 들이 Conference Bridge와 연결되면 식별 slot number 를 부여 받는다.

이 slot number를 부여받기 위해서는 각각의 Media type에 맞는 API를 호출해야 하는데, 호출 가능한 API는 다음과 같다.

  • for Call obejct, the slot number is in conf_slot of the CallInfo structure, so it can be fetch with call.info().conf_slo (once the media is active of course).
  • for WAV file player, use lib.player_get_slot()
  • for WAV playlist, use lib.playlist_get_slot()
  • for WAV file recorder, use lib.recorder_get_slot()

Conference Bridge 는 쉽고 단순하지만 강력한 Audio Routing 관리 기법을 제공한다. 개념은 간단하다. Audio source 와 Audio destination 을 서로 연결(Bridge)시키는 것이다. 그렇게 되면 Source 에서 Destination으로 향하는 단방향 Audio Stream 이 구성되는 것이다. 만약 두개 이상의 Source 에서 하나의 Destination으로 향하게 된다면, 목적지에서의 Audio는 서로 Mix 되어 나타난다. 만약 하나의 Source가 두개 이상의 Destination으로 향하게 되면, Audio 복제되어 전달된다.

Conference-bridge.jpg

위쪽에 소개된 그림과 같이 Conference bridge 는 다음의 Object들을 가진다.

  • Sound device, which by convention is always attached at slot number 0.
  • a WAV file to be played back at slot 1
  • a WAV file for recording at slot 2
  • an active call to Alice at slot 3
  • an active call to Bob at slot 4

만약 Media object가 Conference Bridge에 연결되었고, 아무 slot 과도 연결이 되지 않았다면, 아무런 Sound stream 이 없다는 뜻이다.

WAV File Playback

WAV file을 Speaker 로 출력하고자 한다면, 단지 WAV playback object를 Sound device와 연결시키면 된다. 즉, 다음과 같이 말이다.

Conference-bridge-wav-playback.jpg

빨간색 선으로 표시된 부분이 Media flow 를 나타낸다. Waf file player -> Sound device

구체적인 코드는 다음과 같다.

lib.conf_connect(1, 0)

conf_connect() 메소드는 다음의 인자값을 사용한다.

  • 첫번째 인자 Source media object slot number
  • 두번째 인자 Destination media object slot number

그리고, 연결을 해제하는 코드는 다음과 같다.

lib.conf_disconnect(1, 0)

Recording to WAV File

만약 Microphone 으로 인입되는 소리를 WAV File 로 저장하고 싶다면 다음과 같이 하면 된다.

lib.conf_connect(0, 2)

위와 같이 하고 나면, sound device 로 인입되는 소리가 WAV record file 로 저장된다. 다음의 그림을 참조하자.

Conference-bridge-snd-rec.jpg

Recording 을 정지하거나 중지하고 싶다면 다음과 같이 disconnect 만 하면 된다.

lib.conf_disconnect(0, 2)

위의 코드처럼 Disconnect 를 했다고 Recording이 종료 되는 것이 아니다. 왜냐하면 아직 Recording file 을 close 하지 않았기 때문이다. 해당 파일을 Close 하기전에 언제라도 conf_connect() 하게 되면 계속 해당 파일에 이어서 Recording 을 할 수 있는 것이다. 단, Recording 중인 파일을 Play 할 수는 없다. Play를 하고 싶다면 먼저 해당 파일을 close 해야만 한다.

Looping Audio

원한다면, Media object를 Loop 시킬수도 있다(Media object 스스로가 Input/Output이 되는 것이다). 예를 들어 Microphone 으로 인입되는 소리를 바로 Speaker로 듣고 싶다면 다음과 같이 하면 된다.

lib.conf_connect(0, 0)

다음 그림과 같은 구조가 된다.

Conference-bridge-loop.jpg

말했듯이, 위의 구조처럼 연결을 하게되면, Microphone 으로 인입되는 소리가 바로 Speaker로 흘러나오게 된다. 이것을 통해서 각각의 Microphone 과 Speaker 가 정상적으로 동작하는지 여부를 확인할 수 있다.

Media object가 단방향 Media 가 아닌이상 언제든지/어디든지 이 Loop-back 연결을 사용할 수 있다. 무슨 뜻인가 하면, 수신된 Audio 내용을 그대로 송신할 수는 있지만, Play 중인 WAV 파일에 Recording 을 할 수는 없다는 뜻이다.

Normal Call

일반적인 콜에서는 양 끝단간 양방향 Audio stream 이 필요하다. 다음과 같은 Connect 로 아주 쉽게 구현이 가능하다. Alice 와 통화를 하고 있다고 가정해보자.

# This will connect the sound device/mic to the call
lib.conf_connect(0, 3)
 
# And this will connect the call to the sound device/speaker
lib.conf_connect(3, 0)

이를 그림으로 나타내면 다음과 같다.

Conference-bridge-call.jpg

Second Call

여기서 Bob 과 Alice 와 동시에 통화를 하고 싶다고 가정해보자. 우리는 이미 Alice 와 양방향 연결을 가지고 있는 상태이다. 단지 여기서 아래의 코드를 이용해, Bob 과의 양방향 연결을 추가해주면 된다.

lib.conf_connect(0, 4)
lib.conf_connect(4, 0)

이를 그림으로 나타내면 다음과 같다.

Conference-bridge-2-calls.jpg

Conference Call

3자통화 Conference Call 을 하기 위해서는 Bob 과 Alice 역시 서로간에 양방향 연결을 수립해주면 된다. 다음을 보자.

lib.conf_connect(3, 4)
lib.conf_connect(4, 3)

그림으로 나타내면 다음과 같은 구조가 된다.

Conference-bridge-conf-call.jpg

이제 3자 통화가 가능해졌다.

Recroding Conference

3자 통화를 Recording 하려면 어떻게 해야할까? 각각의 Microphone 입력을 WAV recorder 로 연결시켜주면 된다.

lib.conf_connect(0, 2)
lib.conf_connect(3, 2)
lib.conf_connect(4, 2)

그러면 다음 그림과 같은 구조가 된다.

Conference-bridge-conf-call-record.jpg

상당히 복잡한 구조처럼 보인다. 하지만 좋은 소식은 우리가 일일이 연결의 유지 관리에 신경쓸 필요가 없다는 점이다. 연결의 유지와 관련된 모든 부분은 Conference Bridge 에서 담당한다. 단지 우리가 해야할 부분은 연결의 Routing뿐이란 것이다.

Media Objects

WAV Player

하나의 WAV player object 는 하나의 WAV file 만을 위해서만 작동한다. 관련 API는 다음과 같다.

  • lib.create_player() : WAV player 생성 API. 생성된 Player ID 를 리턴한다.
  • lib.player_get_slot() : WAV player 의 conference bridge slot number 를 리턴한다.
  • lib.player_set_pos() : Playback position 을 설정한다. 흔히 WAV file 되감기를 할 때 사용한다.
  • lib.player_destroy() : 생성한 Player 를 종료한다. 내부적으로 Conference birdge 와 연결된 모든 Source/Destination 연결을 종료한다.

WAV Playlist

WAV playlist 는 여러개의 WAV 파일들을 순차적으로 Play 할 수 있다. 다음의 API 들을 제공한다.

  • lib.create_playlist() : playlist 를 생성한다. WAV 파일 리스트를 인자값으로 받으며 리턴값으로 Playlist ID를 돌려준다.
  • lib.playlist_get_slot() : 해당 Playlist의 conference bridge slot number 를 리턴한다.
  • lib.playlist_destory() : playlist 를 종료한다. 내부적으로 Conference birdge 와 연결된 모든 Source/Destination 연결을 종료한다.

WAV Recorder

WAV recorder 는 Audio input 을 받아서 File에 Write 하는 역할을 한다. 관련 API는 다음과 같다.

  • lib.create_recorder() : Recorder 를 생성한다. 리턴값으로 Recorder ID 를 돌려준다.
  • lib.recorder_get_slot() : 해당 Recorder 의 conference bridge slot number 를 리턴한다.
  • lib.recorder_destroy() : 생성한 Recorder 를 종료한다. 내부적으로 Conference birdge 와 연결된 모든 Source/Destination 연결을 종료한다.

Call

Call 역시 Media object 의 한 종류이다. 다음 장에서 설명하도록 한다.

Calls

Call 클래스에 관련된 자세한 정보는 이곳[16]에서 확인할 수 있다.

Making Outgoing Calls

쉽게 발신 콜을 생성하기 위해서는 단지 Account ojbect 의 make_call() 메소드를 호출하기만 하면 된다. Account object acc 가 있고 des_uri 로 발신 콜을 생성한다고 가정해보자. 다음과 같은 코드로 작성하면 된다.

try:
    my_cb = MyCallCallback()
    call = acc.make_call(dst_uri, cb=my_cb)
except pjsua.Error, err:
    print 'Error making outgoing call:', err

위에 작성된 코드는 dst_uri 로 발신콜을 생성해서 생성된 Call instance 를 call object에 저장하는 예제이다. 콜 생성 이후에 따르는 명령들은 call instance 를 통해 호출이 가능하다. 그리고 수신되는 Event 들은 등록된 Callback 메소드에 의해 처리된 것이다.

Receiving Incoming Calls

수신되는 콜들은 AccountCallback 클래스의 on_incoming_call() 를 통해서 확인이 가능하다. AccountCallback 클래스를 사용하기 위해서는 다른 클래스로 상속을 받아 사용을 해야한다. 다음과 같이 사용한다.

class MyAccountCallback(pjsua.AccountCallback):
    def __init__(self, account=None):
        pjsua.AccountCallback.__init__(self, account)
 
    def on_incoming_call(self, call):
        my_cb = MyCallCallback()
        call.set_callback(my_cb)

위의 코드에서 처럼, 인입 콜에 한해서, call instance 가 Callback 인자값으로 전달되는 것을 확인할 수 있다. 우리가 해야 할 일은 단지 인입된 콜로부터의 Event 수신을 위한 Callback 메소드를 붙여주기만 하면 되는 것이다.

Call Operations

Call class[17]를 참조하자.

Call Properties

모든 콜들은 state, media state, remote peer information 와 같은 속성들을 CallInfo 클래스에 보관한다. 이 CallInfo 는 call instance 에서 info() 메소드를 호출함으로써 참조가 가능하다.

Handling Call Events

Call 과 관련된 Event 들은 CallCallback instance 가 수신받게 된다. 이 Event 들을 처리하고 싶다면 CallCallback 클래스를 상속받은 클래스에 Override 로 메소드를 구현하고 이를 call instance 생성시 같이 참조되게 하면 된다.

Call Disconnection

Call 종료 이벤트는 특수 이벤트로서, 한번 이 이벤트가 발생되면, 콜은 더이상 유효하지가 않으며, 이후 수행되는 모든 콜 명령에 대해서는 Error 를 발생시킨다.

콜 Disconnection 은 CallCallback 의 on_state() 메소드로 확인할 수 있는데, 다음과 같이 확인할 수 있다.

class MyCallCallback(pjsua.CallCallback):
    ...
    def on_state(self):
        if self.call.info().state == pjsua.CallState.DISCONNECTED:
            print "This call has been disconnected"

Working with Call's Media

Call Media가 Ready(혹은 Active) 상태일때만 Call media 사용이 가능하다. Call media 상태 변화는 on_media_state() 콜백으로 확인이 가능하고, 상태 정보는 CallInfo 클래스의 멤버변수 media_state 에 저장된다.

Media 상태는 MediaState 상수 중 하나가 될수 있다.(NULL, ACTIVE, LOCAL_HOLD, REMOTE_HOLD, ERROR) 다음은 Media 상태가 ACTIVE 가 되었을 때, Sound device 에 연결하는 예제이다.

class MyCallCallback(pjsua.CallCallback):
    ...
    # Notification when call's media state has changed.
    def on_media_state(self):
        if self.call.info().media_state == pj.MediaState.ACTIVE:
            # Connect the call to sound device
            call_slot = self.call.info().conf_slot
            pj.Lib.instance().conf_connect(call_slot, 0)
            pj.Lib.instance().conf_connect(0, call_slot)
            print "Media is now active"
        else:
            print "Media is inactive"

만약 media_state 가 active 에서 non-active 로 변경되면(예를 들어 Hold를 하게된 경우), 별도로 Conference bridge 를 해제해 줄 필요는 없다. 왜냐하면 Media_state 가 더이상 유효하지 않게되면 자동으로 Conference bridge 에서 삭제되기 때문이다.

Sample program

import sys
import pjsua as pj
 
LOG_LEVEL = 3
current_call = None
 
# Logging callback
def log_cb(level, str, len):
    print str,
 
# Callback to receive events from account
class MyAccountCallback(pj.AccountCallback):
 
    def __init__(self, account=None):
        pj.AccountCallback.__init__(self, account)
 
        # Notification on incoming call
    def on_incoming_call(self, call):
        global current_call
        if current_call:
            call.answer(486, "Busy")
            return
 
        print "Incoming call from", call.info().remote_uri
        print "Press 'a' to answer"
 
        current_call = call
 
        call_cb = MyCallCallback(current_call)
        current_call.set_callback(call_cb)
 
        current_call.answer(180)
 
# Callback to receive events from Call
class MyCallCallback(pj.CallCallback):
    def __init__(self, call=None):
        pj.CallCallback.__init__(self, call)
 
    # Notification when call state has changed
    def on_state(self):
        global current_call
        print "Call with", self.call.info().remote_uri,
        print "is", self.call.info().state_text,
        print "last code =", self.call.info().last_code,
        print "(" + self.call.info().last_reason + ")"
 
        if self.call.info().state == pj.CallState.DISCONNECTED:
            current_call = None
            print "Current call is", current_call
 
    # Notification when call's media state has changed.
    def on_media_state(self):
        if self.call.info().media_state == pj.MediaState.ACTIVE:
            # Connect the call to sound device
            call_slot = self.call.info().conf_slot
            pj.Lib.instance().conf_connect(call_slot, 0)
            pj.Lib.instance().conf_connect(0, call_slot)
            print "Media is now active"
        else:
            print "Media is inactive"
 
# Function to make call
def make_call(uri):
    try:
        print "Making call to", uri
        return acc.make_call(uri, cb=MyCallCallback())
    except pj.Error, e:
        print "Exception: " + str(e)
        return None
 
# Create library instance
lib = pj.Lib()
 
try:
    # Init library with default config and some customized
    # logging config
    lib.init(log_cfg = pj.LogConfig(level=LOG_LEVEL, callback = log_cb))
 
    # Create UDP transport which listens to any available port
    transport = lib.create_transport(pj.TransportType.UDP, pj.TransportConfig(0))
    print "\nListening on", transport.info().host,
    print "port", transport.info().port, "\n"
 
    # Start the library
    lib.start()
 
    # Create local account
    acc = lib.create_account_for_transport(transport, cb=MyAccountCallback())
 
    # If argument is specified then make call to the URI
    if len(sys.argv) > 1:
        lck = lib.auto_lock()
        current_call = make_call(sys.argv[1])
        print "Current call is", current_call
        del lck
 
    my_sip_uri = "sip:" + transport.info().host + ":" + str(transport.info().port)
 
    # Menu loop
    while True:
        print "My SIP URI is", my_sip_uri
        print "Menu: m=make call, h=hangup call, a=answer call, q=quit"
 
        input = sys.stdin.readline().rstrip("\r\n")
        if input == "m":
            if current_call:
                print "Already have another call"
                continue
            print "Enter destination URI to call: ",
            input = sys.stdin.readline().rstrip("\r\n")
            if input == "":
                continue
            lck = lib.auto_lock()
            current_call = make_call(input)
            del lck
 
        elif input == "h":
            if not current_call:
                print "There is no call"
                continue
            current_call.hangup()
 
        elif input == "a":
            if not current_call:
                print "There is no call"
                continue
            current_call.answer(200)
 
        elif input == "q":
            break
 
    # Shutdown the library
    transport = None
    acc.delete()
    acc = None
    lib.destroy()
    lib = None
 
except pj.Error, e:
    print "Exception: " + str(e)
    lib.destroy()
    lib = None

SIP Presence

Subscribing to Buddy's Presence Status

Buddy 목록의 상태 메시지를 확인하기 위해서는 먼저 Buddy ojbect 를 추가해야 한다. Buddy Event 관리를 위해 Callback 함수를 등록하고 Buddy 상태 정보를 읽어보자. 다음의 예제와 같이 진행하면 된다.

class MyBuddyCallback(pjsua.BuddyCallback):
    def __init__(self, buddy=None):
        pjsua.BuddyCallback.__init__(self, buddy)
 
    def on_state(self):
        print "Buddy", self.buddy.info().uri, "is",
        print self.buddy.info().online_text
 
try:
    uri = '"Alice" <sip:alice@example.com>'
    buddy = acc.add_buddy(uri, cb=MyBuddyCallback())
    buddy.subscribe()
except pjsua.Error, err:
    print 'Error adding buddy:', err

Responding to Presence Subscription Request

기본적으로, Account 로 요청되는 구독 요청은 자동으로 승인된다. 하지만 만약 특정 유저로부터의 요청만 승인할수록 설정을 변경할 수도 있다.

이렇게 설정을 변경하고자 한다면, AccountCallback 클래스의 on_incoming_subscribe() 메소드를 수정하면 된다.

Changing Accout's Presence Status

상태 메시지 변경을 위해서 Account 클래스 내, 두가지 메소드를 사용할 수 있다.

  • set_basic_status() : 기본 account 상태 메시지를 설정할 수 있다(avaiable/not available)
  • set_presence_status() : 기본 상태 메시지와 추가 정보를 설정할 수 있다.(busy, away, on the phone, etc..)

상태 메시지가 변경되면, Account 설정에 따라 PUBLISH Request, SUBSCRIBE Request 중 하나(혹은 모두)를 이용하여 상태 메시지를 전파한다.

Sample

import sys
import pjsua as pj
 
LOG_LEVEL = 3
pending_pres = None
pending_uri = None
 
def log_cb(level, str, len):
    print str,
 
class MyAccountCallback(pj.AccountCallback):
    def __init__(self, account=None):
        pj.AccountCallback.__init__(self, account)
 
    def on_incoming_subscribe(self, buddy, from_uri, contact_uri, pres):
        global pending_pres, pending_uri
 
        # Allow buddy to subscribe to our presence
        if buddy:
            return(200, None)
        print 'Incoming SUBSCRIBE request from', from_uri
        print 'Press "A" to accept and add, "R" to reject the request'
        pending_pres = pres
        pending_uri = from_uri
        return(202, None)
 
class MyBuddyCallback(pj.BuddyCallback):
    def __init__(self, buddy=None):
        pj.BuddyCallback.__init__(self, buddy)
 
    def on_state(self):
        print "Buddy", self.buddy.info().uri, "is",
        print self.buddy.info().online_text
 
    def on_pager(self, mime_type, body):
        print "Instant message from", self.buddy.info().uri,
        print "(", mime_type, "):"
        print body
 
    def on_pager_status(self, body, im_id, code, reason):
        if code >= 300:
            print "Message delivery failed for message",
            print body, "to", self.buddy.info().uri, ":", reason
 
    def on_typing(self, is_typing):
        if is_typing:
            print self.buddy.info().uri, "is typing"
        else:
            print self.buddy.info().uri, "stops typing"
 
lib = pj.Lib()
 
try:
    # Init library with default config and some customized
    # logging config.
    lib.init(log_cfg=pj.LogConfig(level=LOG_LEVEL, callback=log_cb))
 
    # Create UDP transport wich listens to any available port
    transport = lib.create_transport(pj.TransportType.UDP, pj.TransportConfig(0))
    print "\nListening on", transport.info().host,
    print "port", transport.info().port, "\n"
 
    # Start the library
    lib.start()
 
    # Create the library
    lib.start()
 
    # Create local account
    acc = lib.create_account_for_transport(transport, cb=MyAccountCallback())
    acc.set_basic_status(True)
 
    my_sip_uri = "sip:" + transport.info().host + ":" + str(transport.info().port)
    buddy = None
 
    # Menu loop
    while True:
        print "My SIP URI is", my_sip_uri
        print "Menu: a=add buddy, d=delete buddy, t=toggle online status, i=send IM, q=quit"
 
        input = sys.stdin.readline().rstrip("\r\n")
        if input == "a":
            # Add buddy
            print "Enter buddy URI:",
            input = sys.stdin.readline().rstrip("\r\n")
            if input == "":
                continue
 
            buddy = acc.add_buddy(input, cb=MyBuddyCallback())
            buddy.subscribe()
 
        elif input == "t":
            acc.set_basic_status(not acc.info().online_status)
 
        elif input == "i":
            if not buddy:
                print "Add buddy first"
                continue
 
            buddy.send_typing_ind(True)
 
            print "Type the message: ",
            input = sys.stdin.readline().rstrip("\r\n")
            if input == "":
                buddy.send_typing_ind(False)
                continue
 
            buddy.send_pager(input)
 
        elif input == "d":
            if buddy:
                buddy.delete()
                buddy = None
            else:
                print "No buddy was added"
 
        elif input == "A":
            if pending_pres:
                acc.pres_notify(pending_pres, pj.SubscriptionState.ACTIVE)
                buddy = acc.add_buddy(pending_uri, cb=MyBuddyCallback())
                buddy.subscribe()
                pending_pres = None
                pending_uri = None
            else:
                print "No pending request"
 
        elif input == "R":
            if pending_pres:
                acc.pres_notify(pending_pres, pj.SubscriptionState.TERMINATED, "rejected")
                pending_pres = None
                pending_uri = None
            else:
                print "No pending request"
 
        elif input == "q":
            break
 
    # Shutdown the library
    acc.delete()
    acc = None
    if pending_pres:
        acc.pres_notify(pending_pres, pj.SubscriptionState.TERMINATED, "rejected")
    transport = None
    lib.destroy()
    lib = None
 
except pj.Error, e:
    print "Exception: " + str(e)
    lib.destroy()
    lib = None

SIP Instant Messaging

Sending IM

IM(Instant Message)를 전송하는 방법에는 두가지가 있다. Buddy Ojbect의 Send_pager() 메소드와 Call Object의 send_pager() 메소드이다. 첫번째 방식은 별도의 콜 연결 없이도 메시지 전송이 가능하지만, 두번째 방식은 콜이 연결되어 있는 동안에만 메시지 전송이 가능한 방식이다.

Typing indication

메시지를 전송하는 도중에 send_typing_ind() 메소드를 이용하여 "메시지 입력중..." 정보를 전송할 수 있다.

Pager Status

IM 메시지의 전송 상태(전송 성공/실패)는 각각의 전송방식에 따라 BuddyCallback 클래스의 on_pager_status() 메소드 혹은 CallCallback 클래스의 on_pager_status() 메소드에 의해 수신된다.

Receiving IM

메시지를 수신하는 방법에는 세가지 방법이 있는데 누가/어떻게 보냈는지에 따라 나뉘어진다.

  • 통화중에 메시지를 수신하게 된다면 CallCallback object 의 on_pager() 메소드를 통해 확인이 가능하다.
  • 만약 Buddy list 에 있으면서 통화중이 아닌 상태에서 메시지를 수신한다면 BuddyCallback object 의 on_pager() 메소드로 확인이 가능하다.
  • 위의 방식들이 모두 실패했다면, AccountCallback object의 on_pager() 메소드로 확인이 가능하다.

마찬가지로 "메시지 입력중..." 정보를 수신하기 위해서는, 각각의 상황에 맞는 Callback object 에서 on_tyoing() 메소드를 호출하면 된다.

Advanced Settings

STUN

여기 UAConfig[18]를 참조하자.

ICE

여기 MediaConfig[19]를 참조하자.

Turn

여기 MediaConfig[20]를 참조하자.

SIP TCP Transport

TCP Transport object를 생성하고 Account 의 proxy list에 <sip:your-proxy;lr;transport=tcp>를 추가하자. 물론 "your-proxy" 부분은 적절한 host/IP address 로 변경해주자.

SIP TLS Transport

현재 Python 모듈에선 미지원.

References

  1. http://trac.pjsip.org/repos/wiki/Python_SIP_Tutorial
  2. http://trac.pjsip.org/repos/wiki/Getting-Started/iPhone
  3. http://trac.pjsip.org/repos/wiki/Getting-Started/Android
  4. http://trac.pjsip.org/repos/wiki/Getting-Started/BB10
  5. http://trac.pjsip.org/repos/wiki/Getting-Started/Windows-Mobile
  6. http://trac.pjsip.org/repos/wiki/Getting-Started/Windows-Phone
  7. http://trac.pjsip.org/repos/wiki/Getting-Started/Symbian
  8. http://trac.pjsip.org/repos/wiki/Python_SIP/Build_Install
  9. http://www.pjsip.org/python/pjsua.htm#AccountCallback
  10. http://www.pjsip.org/python/pjsua.htm#CallCallback
  11. http://www.pjsip.org/python/pjsua.htm#BuddyCallback
  12. http://www.pjsip.org/python/pjsua.htm#Lib-instance
  13. http://www.pjsip.org/python/pjsua.htm#Lib-init
  14. http://www.pjsip.org/python/pjsua.htm#AccountConfig
  15. http://trac.pjsip.org/repos/browser/pjproject/trunk/pjsip-apps/src/python/samples/registration.py
  16. http://www.pjsip.org/python/pjsua.htm#Call
  17. http://www.pjsip.org/python/pjsua.htm#Call
  18. http://www.pjsip.org/python/pjsua.htm#UAConfig
  19. http://www.pjsip.org/python/pjsua.htm#MediaConfig
  20. http://www.pjsip.org/python/pjsua.htm#MediaConfig