Pjsip-pjsua python

From 탱이의 잡동사니
Revision as of 17:30, 12 January 2015 by Pchero (talk | contribs)
Jump to navigation Jump to search

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) 등을 지원한다.

전체 프로그래밍 가이드는 이곳<ref>http://trac.pjsip.org/repos/wiki/Python_SIP_Tutorial</ref> 에서 확인할 수 있다.

Installation

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

Linux

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

Mobiles

IOS, Android 를 비롯한 모바일 운영체제 역시 지원한다. 자세한 내용은 다음를 참조하자. IOS<ref>http://trac.pjsip.org/repos/wiki/Getting-Started/iPhone</ref> , Android<ref>http://trac.pjsip.org/repos/wiki/Getting-Started/Android</ref> , BlabkBerry<ref>http://trac.pjsip.org/repos/wiki/Getting-Started/BB10</ref> , Windows_Mobile<ref>http://trac.pjsip.org/repos/wiki/Getting-Started/Windows-Mobile</ref> , Windows_phone_8<ref>http://trac.pjsip.org/repos/wiki/Getting-Started/Windows-Phone</ref> , Symbian<ref>http://trac.pjsip.org/repos/wiki/Getting-Started/Symbian</ref>

Others

자세한 설치 내용은 이곳<ref>http://trac.pjsip.org/repos/wiki/Python_SIP/Build_Install</ref>을 참조하자.


Simple example

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

  • Code

<syntaxhighlight lang="python"> import sys import pjsua as pj

  1. Logging callback

def log_cb(level, str, len):

   print str,
  1. 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!"


  1. 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)

</syntaxhighlight>

  • 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() 메소드를 통해서 이를 알아볼 수 있다. <syntaxhighlight lang=python> 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.

</syntaxhighlight>

Relationship between objects and handles

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

따라서, 해당 Object들을 삭제하고 싶다면 다음과 같이 해야한다. <syntaxhighlight lang=python> acc.delete() del acc call.hangup() del call buddy.delete del buddy </syntaxhighlight>

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<ref>http://www.pjsip.org/python/pjsua.htm#AccountCallback</ref>, CallCallback<ref>http://www.pjsip.org/python/pjsua.htm#CallCallback</ref>, BuddyCallback<ref>http://www.pjsip.org/python/pjsua.htm#BuddyCallback</ref>)들을 상속받아 적절한 메소드들을 구현한 다음에 해당 클래스에 Callback 객체로 등록하면 된다.

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

Error handling

Error 발생시, Pjsua 에서는 pjsua.Error 오류를 raise 한다. <syntaxhighlight lang=python> import pjsua

try:

   call = acc.make_call('sip:buddy@example.org')

except pjsua.Error, err:

   print 'Exception has occured:', err

except:

   print 'Ouch..'

</syntaxhighlight>

Threading and Concurrency

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

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

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

</syntaxhighlight>

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 사용 전 다음과 같이 객체 생성을 하자. <syntaxhighlight lang=python> import pjsua

lib = Lib() </syntaxhighlight> 한번 객체를 생성하고나면 그 이후부터는 Lib.instance()<ref>http://www.pjsip.org/python/pjsua.htm#Lib-instance</ref> 를 통해 언제든지 참조가 가능하다.

Initialize the Library

Library 초기화는 init()<ref>http://www.pjsip.org/python/pjsua.htm#Lib-init</ref> 메소드로 수행한다. <syntaxhighlight lang=python> try:

   lib.init()

except pjsua.Error, err:

   print 'Initialization error:', err

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

  • UAConfig: SIP user agent settings
  • MediaConfig : Media settings including ICE and TURN
  • LogConfig : Customize logging settings.

<syntaxhighlight lang=python> 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

</syntaxhighlight>

Create one or More Transports

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

   udp = lib.create_transport(pj.TransportType.UDP)

except pj.Error, e:

   print "Error creating transport:", e

</syntaxhighlight> 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() 메소드를 호출하면 된다. <syntaxhighlight lang=python> try:

   lib.start()

except pj.Error, e:

   print "Error starting pjsua:", e

</syntaxhighlight>

Shutting Down the Library

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

References

<references />