diff options
Diffstat (limited to 'pjsip-apps/src')
56 files changed, 5721 insertions, 0 deletions
diff --git a/pjsip-apps/src/pjsua/main.c b/pjsip-apps/src/pjsua/main.c index ef89e6b4..695fba3c 100644 --- a/pjsip-apps/src/pjsua/main.c +++ b/pjsip-apps/src/pjsua/main.c @@ -121,4 +121,5 @@ int main(int argc, char *argv[]) pj_thread_join(sig_thread); } } + return 0; } diff --git a/pjsip-apps/src/pygui/account.py b/pjsip-apps/src/pygui/account.py new file mode 100644 index 00000000..35c4707c --- /dev/null +++ b/pjsip-apps/src/pygui/account.py @@ -0,0 +1,239 @@ +# $Id$ +# +# pjsua Python GUI Demo +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +if sys.version_info[0] >= 3: # Python 3 + import tkinter as tk + from tkinter import ttk + from tkinter import messagebox as msgbox +else: + import Tkinter as tk + import tkMessageBox as msgbox + import ttk + +import random +import pjsua2 as pj +import _pjsua2 +import accountsetting +import application +import call +import chat as ch + +# Account class +class Account(pj.Account): + """ + High level Python Account object, derived from pjsua2's Account object. + """ + def __init__(self, app): + pj.Account.__init__(self) + self.app = app + self.randId = random.randint(1, 9999) + self.cfg = pj.AccountConfig() + self.cfgChanged = False + self.buddyList = [] + self.chatList = [] + self.deleting = False + + def findChat(self, uri_str): + uri = ch.ParseSipUri(uri_str) + if not uri: return None + + for chat in self.chatList: + if chat.isUriParticipant(uri) and chat.isPrivate(): + return chat + return None + + def newChat(self, uri_str): + uri = ch.ParseSipUri(uri_str) + if not uri: return None + + chat = ch.Chat(self.app, self, uri) + self.chatList.append(chat) + self.app.updateWindowMenu() + return chat + + def statusText(self): + status = '?' + if self.isValid(): + ai = self.getInfo() + if ai.regLastErr: + status = self.app.ep.utilStrError(ai.regLastErr) + elif ai.regIsActive: + if ai.onlineStatus: + if len(ai.onlineStatusText): + status = ai.onlineStatusText + else: + status = "Online" + else: + status = "Registered" + else: + if ai.regIsConfigured: + if ai.regStatus/100 == 2: + status = "Unregistered" + else: + status = ai.regStatusText + else: + status = "Doesn't register" + else: + status = '- not created -' + return status + + def onRegState(self, prm): + self.app.updateAccount(self) + + def onIncomingCall(self, prm): + c = call.Call(self, call_id=prm.callId) + call_prm = pj.CallOpParam() + call_prm.statusCode = 180 + c.answer(call_prm) + ci = c.getInfo() + msg = "Incoming call for account '%s'" % self.cfg.idUri + if msgbox.askquestion(msg, "Accept call from '%s'?" % (ci.remoteUri), default=msgbox.YES) == u'yes': + call_prm.statusCode = 200 + c.answer(call_prm) + + # find/create chat instance + chat = self.findChat(ci.remoteUri) + if not chat: chat = self.newChat(ci.remoteUri) + + chat.showWindow() + chat.registerCall(ci.remoteUri, c) + chat.updateCallState(c, ci) + else: + c.hangup(call_prm) + + def onInstantMessage(self, prm): + chat = self.findChat(prm.fromUri) + if not chat: chat = self.newChat(prm.fromUri) + + chat.showWindow() + chat.addMessage(prm.fromUri, prm.msgBody) + + def onInstantMessageStatus(self, prm): + if prm.code/100 == 2: return + + chat = self.findChat(prm.toUri) + if not chat: + print "=== IM status to '%s' cannot find chat" % prm.toUri + return + + chat.addMessage(None, "Failed sending message to '%s': %s" % (prm.toUri, prm.reason)) + + def onTypingIndication(self, prm): + chat = self.findChat(prm.fromUri) + if not chat: + print "=== Incoming typing indication from '%s' cannot find chat" % prm.fromUri + return + + chat.setTypingIndication(prm.fromUri, prm.isTyping) + + +# Account frame, to list accounts +class AccountListFrame(ttk.Frame): + """ + This implements a Frame which contains account list and buttons to operate + on them (Add, Modify, Delete, etc.). + """ + def __init__(self, parent, app, acc_list = []): + ttk.Frame.__init__(self, parent, name='acclist') + self.app = app + self.accList = acc_list + self.accDeletedList = [] + self.pack(expand='yes', fill='both') + self._createWidgets() + for acc in self.accList: + self._showAcc(acc) + + def _createWidgets(self): + self.tv = ttk.Treeview(self, columns=('ID', 'Registrar', 'Default'), selectmode='browse') + self.tv.heading('#0', text='Priority') + self.tv.heading(0, text='ID') + self.tv.heading(1, text='Registrar') + self.tv.heading(2, text='Default?') + self.tv.column('#0', width=60) + self.tv.column(0, width=300) + self.tv.column(1, width=200) + self.tv.column(2, width=60) + self.tv.grid(column=0, row=0, rowspan=4, padx=5, pady=5) + + ttk.Button(self, text='Add..', command=self._onBtnAdd).grid(column=1, row=0, padx=5) + ttk.Button(self, text='Settings..', command=self._onBtnSettings).grid(column=1, row=1) + ttk.Button(self, text='Set Default', command=self._onBtnSetDefault).grid(column=1, row=2) + ttk.Button(self, text='Delete..', command=self._onBtnDelete).grid(column=1, row=3) + + def _showAcc(self, acc): + is_default = 'Yes' if acc.isValid() and acc.isDefault() else '' + values = (acc.cfg.idUri, acc.cfg.regConfig.registrarUri, is_default) + self.tv.insert('', 0, str(acc.randId), open=True, text=str(acc.cfg.priority), values=values) + + def updateAccount(self, acc): + is_default = 'Yes' if acc.isValid() and acc.isDefault() else '' + values = (acc.cfg.idUri, acc.cfg.regConfig.registrarUri, is_default) + self.tv.item(str(acc.randId), text=str(acc.cfg.priority), values=values) + + def _getSelectedAcc(self): + items = self.tv.selection() + if not items: + return None + iid = int(items[0]) + return [acc for acc in self.accList if acc.randId==iid][0] + + def _onBtnAdd(self): + cfg = pj.AccountConfig() + dlg = accountsetting.Dialog(self.master, cfg) + if dlg.doModal(): + acc = Account(self.app) + acc.cfg = cfg + self._showAcc(acc) + self.accList.append(acc) + self.cfgChanged = True + + def _onBtnSettings(self): + acc = self._getSelectedAcc() + if not acc: + return + dlg = accountsetting.Dialog(self.master, acc.cfg) + if dlg.doModal(): + self.updateAccount(acc) + self.cfgChanged = True + + def _onBtnDelete(self): + acc = self._getSelectedAcc() + if not acc: + return + msg = "Do you really want to delete account '%s'" % acc.cfg.idUri + if msgbox.askquestion('Delete account?', msg, default=msgbox.NO) != u'yes': + return + self.accList.remove(acc) + self.accDeletedList.append(acc) + self.tv.delete( (str(acc.randId),) ) + + def _onBtnSetDefault(self): + acc = self._getSelectedAcc() + if not acc: + return + if acc.isValid(): + acc.setDefault() + for acc in self.accList: + self.updateAccount(acc) + + +if __name__ == '__main__': + application.main() diff --git a/pjsip-apps/src/pygui/accountsetting.py b/pjsip-apps/src/pygui/accountsetting.py new file mode 100644 index 00000000..309c7346 --- /dev/null +++ b/pjsip-apps/src/pygui/accountsetting.py @@ -0,0 +1,369 @@ +# $Id$ +# +# pjsua Python GUI Demo +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +if sys.version_info[0] >= 3: # Python 3 + import tkinter as tk + from tkinter import ttk + from tkinter import messagebox as msgbox +else: + import Tkinter as tk + import tkMessageBox as msgbox + import ttk + +import pjsua2 as pj +import endpoint +import application + +class Dialog(tk.Toplevel): + """ + This implements account settings dialog to manipulate account settings. + """ + def __init__(self, parent, cfg): + tk.Toplevel.__init__(self, parent) + self.transient(parent) + self.parent = parent + self.geometry("+100+100") + self.title('Account settings') + + self.frm = ttk.Frame(self) + self.frm.pack(expand='yes', fill='both') + + self.isOk = False + self.cfg = cfg + + self.createWidgets() + + def doModal(self): + if self.parent: + self.parent.wait_window(self) + else: + self.wait_window(self) + return self.isOk + + def createWidgets(self): + # The notebook + self.frm.rowconfigure(0, weight=1) + self.frm.rowconfigure(1, weight=0) + self.frm.columnconfigure(0, weight=1) + self.frm.columnconfigure(1, weight=1) + self.wTab = ttk.Notebook(self.frm) + self.wTab.grid(column=0, row=0, columnspan=2, padx=10, pady=10, ipadx=20, ipady=20, sticky=tk.N+tk.S+tk.W+tk.E) + + # Main buttons + btnOk = ttk.Button(self.frm, text='Ok', command=self.onOk) + btnOk.grid(column=0, row=1, sticky=tk.E, padx=20, pady=10) + btnCancel = ttk.Button(self.frm, text='Cancel', command=self.onCancel) + btnCancel.grid(column=1, row=1, sticky=tk.W, padx=20, pady=10) + + # Tabs + self.createBasicTab() + self.createSipTab() + self.createMediaTab() + self.createMediaNatTab() + + def createBasicTab(self): + # Prepare the variables to set/receive values from GUI + self.cfgPriority = tk.IntVar(value=self.cfg.priority) + self.cfgAccId = tk.StringVar(value=self.cfg.idUri) + self.cfgRegistrar = tk.StringVar(value=self.cfg.regConfig.registrarUri) + self.cfgRegisterOnAdd = tk.IntVar(value=self.cfg.regConfig.registerOnAdd) + self.cfgUsername = tk.StringVar() + self.cfgPassword = tk.StringVar() + if len(self.cfg.sipConfig.authCreds): + self.cfgUsername.set( self.cfg.sipConfig.authCreds[0].username ) + self.cfgPassword.set( self.cfg.sipConfig.authCreds[0].data ) + self.cfgProxy = tk.StringVar() + if len(self.cfg.sipConfig.proxies): + self.cfgProxy.set( self.cfg.sipConfig.proxies[0] ) + + # Build the tab page + frm = ttk.Frame(self.frm) + frm.columnconfigure(0, weight=1) + frm.columnconfigure(1, weight=2) + row = 0 + ttk.Label(frm, text='Priority:').grid(row=row, column=0, sticky=tk.E, pady=2) + tk.Spinbox(frm, from_=0, to=9, textvariable=self.cfgPriority, width=2).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='ID (URI):').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Entry(frm, textvariable=self.cfgAccId, width=32).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='Registrar URI:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Entry(frm, textvariable=self.cfgRegistrar, width=32).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Checkbutton(frm, text='Register on add', variable=self.cfgRegisterOnAdd).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='Optional proxy URI:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Entry(frm, textvariable=self.cfgProxy, width=32).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='Auth username:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Entry(frm, textvariable=self.cfgUsername, width=16).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='Password:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Entry(frm, textvariable=self.cfgPassword, show='*', width=16).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + + self.wTab.add(frm, text='Basic Settings') + + + def createSipTab(self): + # Prepare the variables to set/receive values from GUI + self.cfgPrackUse = tk.IntVar(value=self.cfg.callConfig.prackUse) + self.cfgTimerUse = tk.IntVar(value=self.cfg.callConfig.timerUse) + self.cfgTimerExpires = tk.IntVar(value=self.cfg.callConfig.timerSessExpiresSec) + self.cfgPublish = tk.BooleanVar(value=self.cfg.presConfig.publishEnabled) + self.cfgMwiEnabled = tk.BooleanVar(value=self.cfg.mwiConfig.enabled) + self.cfgEnableContactRewrite = tk.BooleanVar(value=self.cfg.natConfig.contactRewriteUse != 0) + self.cfgEnableViaRewrite = tk.BooleanVar(value=self.cfg.natConfig.viaRewriteUse != 0) + self.cfgEnableSdpRewrite = tk.BooleanVar(value=self.cfg.natConfig.sdpNatRewriteUse != 0) + self.cfgEnableSipOutbound = tk.BooleanVar(value=self.cfg.natConfig.sipOutboundUse != 0) + self.cfgKaInterval = tk.IntVar(value=self.cfg.natConfig.udpKaIntervalSec) + + # Build the tab page + frm = ttk.Frame(self.frm) + frm.columnconfigure(0, weight=1) + frm.columnconfigure(1, weight=2) + row = 0 + ttk.Label(frm, text='100rel/PRACK:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Radiobutton(frm, text='Only offer PRACK', value=pj.PJSUA_100REL_NOT_USED, variable=self.cfgPrackUse).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='Offer and use if remote supports', value=pj.PJSUA_100REL_OPTIONAL, variable=self.cfgPrackUse).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='Required', value=pj.PJSUA_100REL_MANDATORY, variable=self.cfgPrackUse).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='Session Timer:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Radiobutton(frm, text='Not offered', value=pj.PJSUA_SIP_TIMER_INACTIVE, variable=self.cfgTimerUse).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='Optional', value=pj.PJSUA_SIP_TIMER_OPTIONAL, variable=self.cfgTimerUse).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='Required', value=pj.PJSUA_SIP_TIMER_REQUIRED, variable=self.cfgTimerUse).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text="Always use", value=pj.PJSUA_SIP_TIMER_ALWAYS, variable=self.cfgTimerUse).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='Session Timer Expiration:').grid(row=row, column=0, sticky=tk.E, pady=2) + tk.Spinbox(frm, from_=90, to=7200, textvariable=self.cfgTimerExpires, width=5).grid(row=row, column=1, sticky=tk.W, padx=6) + ttk.Label(frm, text='(seconds)').grid(row=row, column=1, sticky=tk.E) + row += 1 + ttk.Label(frm, text='Presence:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Checkbutton(frm, text='Enable PUBLISH', variable=self.cfgPublish).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='Message Waiting Indication:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Checkbutton(frm, text='Enable MWI', variable=self.cfgMwiEnabled).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='NAT Traversal:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Checkbutton(frm, text='Enable Contact Rewrite', variable=self.cfgEnableContactRewrite).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Checkbutton(frm, text='Enable Via Rewrite', variable=self.cfgEnableViaRewrite).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Checkbutton(frm, text='Enable SDP IP Address Rewrite', variable=self.cfgEnableSdpRewrite).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Checkbutton(frm, text='Enable SIP Outbound Extension', variable=self.cfgEnableSipOutbound).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='UDP Keep-Alive Interval:').grid(row=row, column=0, sticky=tk.E, pady=2) + tk.Spinbox(frm, from_=0, to=3600, textvariable=self.cfgKaInterval, width=5).grid(row=row, column=1, sticky=tk.W, padx=6) + ttk.Label(frm, text='(seconds) Zero to disable.').grid(row=row, column=1, sticky=tk.E) + + + self.wTab.add(frm, text='SIP Features') + + def createMediaTab(self): + # Prepare the variables to set/receive values from GUI + self.cfgMedPort = tk.IntVar(value=self.cfg.mediaConfig.transportConfig.port) + self.cfgMedPortRange = tk.IntVar(value=self.cfg.mediaConfig.transportConfig.portRange) + self.cfgMedLockCodec = tk.BooleanVar(value=self.cfg.mediaConfig.lockCodecEnabled) + self.cfgMedSrtp = tk.IntVar(value=self.cfg.mediaConfig.srtpUse) + self.cfgMedSrtpSecure = tk.IntVar(value=self.cfg.mediaConfig.srtpSecureSignaling) + self.cfgMedIpv6 = tk.BooleanVar(value=self.cfg.mediaConfig.ipv6Use==pj.PJSUA_IPV6_ENABLED) + + # Build the tab page + frm = ttk.Frame(self.frm) + frm.columnconfigure(0, weight=1) + frm.columnconfigure(1, weight=21) + row = 0 + ttk.Label(frm, text='Secure RTP (SRTP):').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Radiobutton(frm, text='Disable', value=pj.PJMEDIA_SRTP_DISABLED, variable=self.cfgMedSrtp).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='Mandatory', value=pj.PJMEDIA_SRTP_MANDATORY, variable=self.cfgMedSrtp).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='Optional (non-standard)', value=pj.PJMEDIA_SRTP_OPTIONAL, variable=self.cfgMedSrtp).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='SRTP signaling:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Radiobutton(frm, text='Does not require secure signaling', value=0, variable=self.cfgMedSrtpSecure).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='Require secure next hop (TLS)', value=1, variable=self.cfgMedSrtpSecure).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='Require secure end-to-end (SIPS)', value=2, variable=self.cfgMedSrtpSecure).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='RTP transport start port:').grid(row=row, column=0, sticky=tk.E, pady=2) + tk.Spinbox(frm, from_=0, to=65535, textvariable=self.cfgMedPort, width=5).grid(row=row, column=1, sticky=tk.W, padx=6) + ttk.Label(frm, text='(0: any)').grid(row=row, column=1, sticky=tk.E, pady=2) + row += 1 + ttk.Label(frm, text='Port range:').grid(row=row, column=0, sticky=tk.E, pady=2) + tk.Spinbox(frm, from_=0, to=65535, textvariable=self.cfgMedPortRange, width=5).grid(row=row, column=1, sticky=tk.W, padx=6) + ttk.Label(frm, text='(0: not limited)').grid(row=row, column=1, sticky=tk.E, pady=2) + row += 1 + ttk.Label(frm, text='Lock codec:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Checkbutton(frm, text='Enable', variable=self.cfgMedLockCodec).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='Use IPv6:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Checkbutton(frm, text='Yes', variable=self.cfgMedIpv6).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + + self.wTab.add(frm, text='Media settings') + + def createMediaNatTab(self): + # Prepare the variables to set/receive values from GUI + self.cfgSipUseStun = tk.IntVar(value = self.cfg.natConfig.sipStunUse) + self.cfgMediaUseStun = tk.IntVar(value = self.cfg.natConfig.mediaStunUse) + self.cfgIceEnabled = tk.BooleanVar(value = self.cfg.natConfig.iceEnabled) + self.cfgIceAggressive = tk.BooleanVar(value = self.cfg.natConfig.iceAggressiveNomination) + self.cfgAlwaysUpdate = tk.BooleanVar(value = True if self.cfg.natConfig.iceAlwaysUpdate else False) + self.cfgIceNoHostCands = tk.BooleanVar(value = True if self.cfg.natConfig.iceMaxHostCands == 0 else False) + self.cfgTurnEnabled = tk.BooleanVar(value = self.cfg.natConfig.turnEnabled) + self.cfgTurnServer = tk.StringVar(value = self.cfg.natConfig.turnServer) + self.cfgTurnConnType = tk.IntVar(value = self.cfg.natConfig.turnConnType) + self.cfgTurnUser = tk.StringVar(value = self.cfg.natConfig.turnUserName) + self.cfgTurnPasswd = tk.StringVar(value = self.cfg.natConfig.turnPassword) + + # Build the tab page + frm = ttk.Frame(self.frm) + frm.columnconfigure(0, weight=1) + frm.columnconfigure(1, weight=2) + row = 0 + ttk.Label(frm, text='SIP STUN Usage:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Radiobutton(frm, text='Default', value=pj.PJSUA_STUN_USE_DEFAULT, variable=self.cfgSipUseStun).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='Disable', value=pj.PJSUA_STUN_USE_DISABLED, variable=self.cfgSipUseStun).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='Media STUN Usage:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Radiobutton(frm, text='Default', value=pj.PJSUA_STUN_USE_DEFAULT, variable=self.cfgMediaUseStun).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='Disable', value=pj.PJSUA_STUN_USE_DISABLED, variable=self.cfgMediaUseStun).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='ICE:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Checkbutton(frm, text='Enable', variable=self.cfgIceEnabled).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Checkbutton(frm, text='Use aggresive nomination', variable=self.cfgIceAggressive).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Checkbutton(frm, text='Always re-INVITE after negotiation', variable=self.cfgAlwaysUpdate).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Checkbutton(frm, text='Disable host candidates', variable=self.cfgIceNoHostCands).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='TURN:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Checkbutton(frm, text='Enable', variable=self.cfgTurnEnabled).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='TURN server:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Entry(frm, textvariable=self.cfgTurnServer, width=20).grid(row=row, column=1, sticky=tk.W, padx=6) + ttk.Label(frm, text='host[:port]').grid(row=row, column=1, sticky=tk.E, pady=6) + row += 1 + ttk.Label(frm, text='TURN connection:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Radiobutton(frm, text='UDP', value=pj.PJ_TURN_TP_UDP, variable=self.cfgTurnConnType).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Radiobutton(frm, text='TCP', value=pj.PJ_TURN_TP_TCP, variable=self.cfgTurnConnType).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='TURN username:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Entry(frm, textvariable=self.cfgTurnUser, width=16).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='TURN password:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Entry(frm, textvariable=self.cfgTurnPasswd, show='*', width=16).grid(row=row, column=1, sticky=tk.W, padx=6) + + self.wTab.add(frm, text='NAT settings') + + def onOk(self): + # Check basic settings + errors = ""; + if not self.cfgAccId.get(): + errors += "Account ID is required\n" + if self.cfgAccId.get(): + if not endpoint.validateSipUri(self.cfgAccId.get()): + errors += "Invalid SIP ID URI: '%s'\n" % (self.cfgAccId.get()) + if self.cfgRegistrar.get(): + if not endpoint.validateSipUri(self.cfgRegistrar.get()): + errors += "Invalid SIP registrar URI: '%s'\n" % (self.cfgRegistrar.get()) + if self.cfgProxy.get(): + if not endpoint.validateSipUri(self.cfgProxy.get()): + errors += "Invalid SIP proxy URI: '%s'\n" % (self.cfgProxy.get()) + if self.cfgTurnEnabled.get(): + if not self.cfgTurnServer.get(): + errors += "TURN server is required\n" + if errors: + msgbox.showerror("Error detected:", errors) + return + + # Basic settings + self.cfg.priority = self.cfgPriority.get() + self.cfg.idUri = self.cfgAccId.get() + self.cfg.regConfig.registrarUri = self.cfgRegistrar.get() + self.cfg.regConfig.registerOnAdd = self.cfgRegisterOnAdd.get() + while len(self.cfg.sipConfig.authCreds): + self.cfg.sipConfig.authCreds.pop() + if self.cfgUsername.get(): + cred = pj.AuthCredInfo() + cred.scheme = "digest" + cred.realm = "*" + cred.username = self.cfgUsername.get() + cred.data = self.cfgPassword.get() + self.cfg.sipConfig.authCreds.append(cred) + while len(self.cfg.sipConfig.proxies): + self.cfg.sipConfig.proxies.pop() + if self.cfgProxy.get(): + self.cfg.sipConfig.proxies.append(self.cfgProxy.get()) + + # SIP features + self.cfg.callConfig.prackUse = self.cfgPrackUse.get() + self.cfg.callConfig.timerUse = self.cfgTimerUse.get() + self.cfg.callConfig.timerSessExpiresSec = self.cfgTimerExpires.get() + self.cfg.presConfig.publishEnabled = self.cfgPublish.get() + self.cfg.mwiConfig.enabled = self.cfgMwiEnabled.get() + self.cfg.natConfig.contactRewriteUse = 1 if self.cfgEnableContactRewrite.get() else 0 + self.cfg.natConfig.viaRewriteUse = 1 if self.cfgEnableViaRewrite.get() else 0 + self.cfg.natConfig.sdpNatRewriteUse = 1 if self.cfgEnableSdpRewrite.get() else 0 + self.cfg.natConfig.sipOutboundUse = 1 if self.cfgEnableSipOutbound.get() else 0 + self.cfg.natConfig.udpKaIntervalSec = self.cfgKaInterval.get() + + # Media + self.cfg.mediaConfig.transportConfig.port = self.cfgMedPort.get() + self.cfg.mediaConfig.transportConfig.portRange = self.cfgMedPortRange.get() + self.cfg.mediaConfig.lockCodecEnabled = self.cfgMedLockCodec.get() + self.cfg.mediaConfig.srtpUse = self.cfgMedSrtp.get() + self.cfg.mediaConfig.srtpSecureSignaling = self.cfgMedSrtpSecure.get() + self.cfg.mediaConfig.ipv6Use = pj.PJSUA_IPV6_ENABLED if self.cfgMedIpv6.get() else pj.PJSUA_IPV6_DISABLED + + # NAT + self.cfg.natConfig.sipStunUse = self.cfgSipUseStun.get() + self.cfg.natConfig.mediaStunUse = self.cfgMediaUseStun.get() + self.cfg.natConfig.iceEnabled = self.cfgIceEnabled.get() + self.cfg.natConfig.iceAggressiveNomination = self.cfgIceAggressive .get() + self.cfg.natConfig.iceAlwaysUpdate = self.cfgAlwaysUpdate.get() + self.cfg.natConfig.iceMaxHostCands = 0 if self.cfgIceNoHostCands.get() else -1 + self.cfg.natConfig.turnEnabled = self.cfgTurnEnabled.get() + self.cfg.natConfig.turnServer = self.cfgTurnServer.get() + self.cfg.natConfig.turnConnType = self.cfgTurnConnType.get() + self.cfg.natConfig.turnUserName = self.cfgTurnUser.get() + self.cfg.natConfig.turnPasswordType = 0 + self.cfg.natConfig.turnPassword = self.cfgTurnPasswd.get() + + self.isOk = True + self.destroy() + + def onCancel(self): + self.destroy() + + +if __name__ == '__main__': + application.main() diff --git a/pjsip-apps/src/pygui/application.py b/pjsip-apps/src/pygui/application.py new file mode 100644 index 00000000..0dd5fa50 --- /dev/null +++ b/pjsip-apps/src/pygui/application.py @@ -0,0 +1,510 @@ +# $Id$ +# +# pjsua Python GUI Demo +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +if sys.version_info[0] >= 3: # Python 3 + import tkinter as tk + from tkinter import ttk + from tkinter import messagebox as msgbox +else: + import Tkinter as tk + import tkMessageBox as msgbox + import ttk + +import pjsua2 as pj +import log +import accountsetting +import account +import buddy +import endpoint +import settings + +import os +import traceback + + +class Application(ttk.Frame): + """ + The Application main frame. + """ + def __init__(self): + ttk.Frame.__init__(self, name='application', width=300, height=500) + self.pack(expand='yes', fill='both') + self.master.title('pjsua2 Demo') + self.master.geometry('500x500+100+100') + + # Logger + self.logger = log.Logger() + + # Accounts + self.accList = [] + + # GUI variables + self.showLogWindow = tk.IntVar(value=0) + self.quitting = False + + # Construct GUI + self._createWidgets() + + # Log window + self.logWindow = log.LogWindow(self) + self._onMenuShowHideLogWindow() + + # Instantiate endpoint + self.ep = endpoint.Endpoint() + self.ep.libCreate() + + # Default config + self.appConfig = settings.AppConfig() + self.appConfig.epConfig.uaConfig.threadCnt = 0; + self.appConfig.epConfig.uaConfig.mainThreadOnly = True + self.appConfig.epConfig.logConfig.writer = self.logger + self.appConfig.epConfig.logConfig.filename = "pygui.log" + self.appConfig.epConfig.logConfig.fileFlags = pj.PJ_O_APPEND + self.appConfig.epConfig.logConfig.level = 5 + self.appConfig.epConfig.logConfig.consoleLevel = 5 + + def saveConfig(self, filename='pygui.js'): + # Save disabled accounts since they are not listed in self.accList + disabled_accs = [ac for ac in self.appConfig.accounts if not ac.enabled] + self.appConfig.accounts = [] + + # Get account configs from active accounts + for acc in self.accList: + acfg = settings.AccConfig() + acfg.enabled = True + acfg.config = acc.cfg + for bud in acc.buddyList: + acfg.buddyConfigs.append(bud.cfg) + self.appConfig.accounts.append(acfg) + + # Put back disabled accounts + self.appConfig.accounts.extend(disabled_accs) + # Save + self.appConfig.saveFile(filename) + + def start(self, cfg_file='pygui.js'): + # Load config + if cfg_file and os.path.exists(cfg_file): + self.appConfig.loadFile(cfg_file) + + self.appConfig.epConfig.uaConfig.threadCnt = 0; + self.appConfig.epConfig.uaConfig.mainThreadOnly = True + self.appConfig.epConfig.logConfig.writer = self.logger + self.appConfig.epConfig.logConfig.level = 5 + self.appConfig.epConfig.logConfig.consoleLevel = 5 + + # Initialize library + self.appConfig.epConfig.uaConfig.userAgent = "pygui-" + self.ep.libVersion().full; + self.ep.libInit(self.appConfig.epConfig) + self.master.title('pjsua2 Demo version ' + self.ep.libVersion().full) + + # Create transports + if self.appConfig.udp.enabled: + self.ep.transportCreate(self.appConfig.udp.type, self.appConfig.udp.config) + if self.appConfig.tcp.enabled: + self.ep.transportCreate(self.appConfig.tcp.type, self.appConfig.tcp.config) + if self.appConfig.tls.enabled: + self.ep.transportCreate(self.appConfig.tls.type, self.appConfig.tls.config) + + # Add accounts + for cfg in self.appConfig.accounts: + if cfg.enabled: + self._createAcc(cfg.config) + acc = self.accList[-1] + for buddy_cfg in cfg.buddyConfigs: + self._createBuddy(acc, buddy_cfg) + + # Start library + self.ep.libStart() + + # Start polling + self._onTimer() + + def updateAccount(self, acc): + if acc.deleting: + return # ignore + iid = str(acc.randId) + text = acc.cfg.idUri + status = acc.statusText() + + values = (status,) + if self.tv.exists(iid): + self.tv.item(iid, text=text, values=values) + else: + self.tv.insert('', 'end', iid, open=True, text=text, values=values) + + def updateBuddy(self, bud): + iid = 'buddy' + str(bud.randId) + text = bud.cfg.uri + status = bud.statusText() + + values = (status,) + if self.tv.exists(iid): + self.tv.item(iid, text=text, values=values) + else: + self.tv.insert(str(bud.account.randId), 'end', iid, open=True, text=text, values=values) + + def _createAcc(self, acc_cfg): + acc = account.Account(self) + acc.cfg = acc_cfg + self.accList.append(acc) + self.updateAccount(acc) + acc.create(acc.cfg) + acc.cfgChanged = False + self.updateAccount(acc) + + def _createBuddy(self, acc, buddy_cfg): + bud = buddy.Buddy(self) + bud.cfg = buddy_cfg + bud.account = acc + bud.create(acc, bud.cfg) + self.updateBuddy(bud) + acc.buddyList.append(bud) + + def _createWidgets(self): + self._createAppMenu() + + # Main pane, a Treeview + self.tv = ttk.Treeview(self, columns=('Status'), show='tree') + self.tv.pack(side='top', fill='both', expand='yes', padx=5, pady=5) + + self._createContextMenu() + + # Handle close event + self.master.protocol("WM_DELETE_WINDOW", self._onClose) + + def _createAppMenu(self): + # Main menu bar + top = self.winfo_toplevel() + self.menubar = tk.Menu() + top.configure(menu=self.menubar) + + # File menu + file_menu = tk.Menu(self.menubar, tearoff=False) + self.menubar.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="Add account..", command=self._onMenuAddAccount) + file_menu.add_checkbutton(label="Show/hide log window", command=self._onMenuShowHideLogWindow, variable=self.showLogWindow) + file_menu.add_separator() + file_menu.add_command(label="Settings...", command=self._onMenuSettings) + file_menu.add_command(label="Save Settings", command=self._onMenuSaveSettings) + file_menu.add_separator() + file_menu.add_command(label="Quit", command=self._onMenuQuit) + + # Window menu + self.window_menu = tk.Menu(self.menubar, tearoff=False) + self.menubar.add_cascade(label="Window", menu=self.window_menu) + + # Help menu + help_menu = tk.Menu(self.menubar, tearoff=False) + self.menubar.add_cascade(label="Help", menu=help_menu) + help_menu.add_command(label="About", underline=2, command=self._onMenuAbout) + + def _showChatWindow(self, chat_inst): + chat_inst.showWindow() + + def updateWindowMenu(self): + # Chat windows + self.window_menu.delete(0, tk.END) + for acc in self.accList: + for c in acc.chatList: + cmd = lambda arg=c: self._showChatWindow(arg) + self.window_menu.add_command(label=c.title, command=cmd) + + def _createContextMenu(self): + top = self.winfo_toplevel() + + # Create Account context menu + self.accMenu = tk.Menu(top, tearoff=False) + # Labels, must match with _onAccContextMenu() + labels = ['Unregister', 'Reregister', 'Add buddy...', '-', + 'Online', 'Invisible', 'Away', 'Busy', '-', + 'Settings...', '-', + 'Delete...'] + for label in labels: + if label=='-': + self.accMenu.add_separator() + else: + cmd = lambda arg=label: self._onAccContextMenu(arg) + self.accMenu.add_command(label=label, command=cmd) + + # Create Buddy context menu + # Labels, must match with _onBuddyContextMenu() + self.buddyMenu = tk.Menu(top, tearoff=False) + labels = ['Audio call', 'Send instant message', '-', + 'Subscribe', 'Unsubscribe', '-', + 'Settings...', '-', + 'Delete...'] + + for label in labels: + if label=='-': + self.buddyMenu.add_separator() + else: + cmd = lambda arg=label: self._onBuddyContextMenu(arg) + self.buddyMenu.add_command(label=label, command=cmd) + + if (top.tk.call('tk', 'windowingsystem')=='aqua'): + self.tv.bind('<2>', self._onTvRightClick) + self.tv.bind('<Control-1>', self._onTvRightClick) + else: + self.tv.bind('<3>', self._onTvRightClick) + self.tv.bind('<Double-Button-1>', self._onTvDoubleClick) + + def _getSelectedAccount(self): + items = self.tv.selection() + if not items: + return None + try: + iid = int(items[0]) + except: + return None + accs = [acc for acc in self.accList if acc.randId==iid] + if not accs: + return None + return accs[0] + + def _getSelectedBuddy(self): + items = self.tv.selection() + if not items: + return None + try: + iid = int(items[0][5:]) + iid_parent = int(self.tv.parent(items[0])) + except: + return None + + accs = [acc for acc in self.accList if acc.randId==iid_parent] + if not accs: + return None + + buds = [b for b in accs[0].buddyList if b.randId==iid] + if not buds: + return None + + return buds[0] + + def _onTvRightClick(self, event): + iid = self.tv.identify_row(event.y) + #iid = self.tv.identify('item', event.x, event.y) + if iid: + self.tv.selection_set( (iid,) ) + acc = self._getSelectedAccount() + if acc: + self.accMenu.post(event.x_root, event.y_root) + else: + # A buddy is selected + self.buddyMenu.post(event.x_root, event.y_root) + + def _onTvDoubleClick(self, event): + iid = self.tv.identify_row(event.y) + if iid: + self.tv.selection_set( (iid,) ) + acc = self._getSelectedAccount() + if acc: + self.cfgChanged = False + dlg = accountsetting.Dialog(self.master, acc.cfg) + if dlg.doModal(): + self.updateAccount(acc) + acc.modify(acc.cfg) + else: + bud = self._getSelectedBuddy() + acc = bud.account + chat = acc.findChat(bud.cfg.uri) + if not chat: + chat = acc.newChat(bud.cfg.uri) + chat.showWindow() + + def _onAccContextMenu(self, label): + acc = self._getSelectedAccount() + if not acc: + return + + if label=='Unregister': + acc.setRegistration(False) + elif label=='Reregister': + acc.setRegistration(True) + elif label=='Online': + ps = pj.PresenceStatus() + ps.status = pj.PJSUA_BUDDY_STATUS_ONLINE + acc.setOnlineStatus(ps) + elif label=='Invisible': + ps = pj.PresenceStatus() + ps.status = pj.PJSUA_BUDDY_STATUS_OFFLINE + acc.setOnlineStatus(ps) + elif label=='Away': + ps = pj.PresenceStatus() + ps.status = pj.PJSUA_BUDDY_STATUS_ONLINE + ps.activity = pj.PJRPID_ACTIVITY_AWAY + ps.note = "Away" + acc.setOnlineStatus(ps) + elif label=='Busy': + ps = pj.PresenceStatus() + ps.status = pj.PJSUA_BUDDY_STATUS_ONLINE + ps.activity = pj.PJRPID_ACTIVITY_BUSY + ps.note = "Busy" + acc.setOnlineStatus(ps) + elif label=='Settings...': + self.cfgChanged = False + dlg = accountsetting.Dialog(self.master, acc.cfg) + if dlg.doModal(): + self.updateAccount(acc) + acc.modify(acc.cfg) + elif label=='Delete...': + msg = "Do you really want to delete account '%s'?" % acc.cfg.idUri + if msgbox.askquestion('Delete account?', msg, default=msgbox.NO) != u'yes': + return + iid = str(acc.randId) + self.accList.remove(acc) + acc.setRegistration(False) + acc.deleting = True + del acc + self.tv.delete( (iid,) ) + elif label=='Add buddy...': + cfg = pj.BuddyConfig() + dlg = buddy.SettingDialog(self.master, cfg) + if dlg.doModal(): + self._createBuddy(acc, cfg) + else: + assert not ("Unknown menu " + label) + + def _onBuddyContextMenu(self, label): + bud = self._getSelectedBuddy() + if not bud: + return + acc = bud.account + + if label=='Audio call': + chat = acc.findChat(bud.cfg.uri) + if not chat: chat = acc.newChat(bud.cfg.uri) + chat.showWindow() + chat.startCall() + elif label=='Send instant message': + chat = acc.findChat(bud.cfg.uri) + if not chat: chat = acc.newChat(bud.cfg.uri) + chat.showWindow(True) + elif label=='Subscribe': + bud.subscribePresence(True) + elif label=='Unsubscribe': + bud.subscribePresence(False) + elif label=='Settings...': + subs = bud.cfg.subscribe + uri = bud.cfg.uri + dlg = buddy.SettingDialog(self.master, bud.cfg) + if dlg.doModal(): + self.updateBuddy(bud) + # URI updated? + if uri != bud.cfg.uri: + cfg = bud.cfg + # del old + iid = 'buddy' + str(bud.randId) + acc.buddyList.remove(bud) + del bud + self.tv.delete( (iid,) ) + # add new + self._createBuddy(acc, cfg) + # presence subscribe setting updated + elif subs != bud.cfg.subscribe: + bud.subscribePresence(bud.cfg.subscribe) + elif label=='Delete...': + msg = "Do you really want to delete buddy '%s'?" % bud.cfg.uri + if msgbox.askquestion('Delete buddy?', msg, default=msgbox.NO) != u'yes': + return + iid = 'buddy' + str(bud.randId) + acc.buddyList.remove(bud) + del bud + self.tv.delete( (iid,) ) + else: + assert not ("Unknown menu " + label) + + def _onTimer(self): + if not self.quitting: + self.ep.libHandleEvents(10) + if not self.quitting: + self.master.after(50, self._onTimer) + + def _onClose(self): + self.saveConfig() + self.quitting = True + self.ep.libDestroy() + self.ep = None + self.update() + self.quit() + + def _onMenuAddAccount(self): + cfg = pj.AccountConfig() + dlg = accountsetting.Dialog(self.master, cfg) + if dlg.doModal(): + self._createAcc(cfg) + + def _onMenuShowHideLogWindow(self): + if self.showLogWindow.get(): + self.logWindow.deiconify() + else: + self.logWindow.withdraw() + + def _onMenuSettings(self): + dlg = settings.Dialog(self, self.appConfig) + if dlg.doModal(): + msgbox.showinfo(self.master.title(), 'You need to restart for new settings to take effect') + + def _onMenuSaveSettings(self): + self.saveConfig() + + def _onMenuQuit(self): + self._onClose() + + def _onMenuAbout(self): + msgbox.showinfo(self.master.title(), 'About') + + +class ExceptionCatcher: + """Custom Tk exception catcher, mainly to display more information + from pj.Error exception + """ + def __init__(self, func, subst, widget): + self.func = func + self.subst = subst + self.widget = widget + def __call__(self, *args): + try: + if self.subst: + args = apply(self.subst, args) + return apply(self.func, args) + except pj.Error, error: + print 'Exception:' + print ' ', error.info() + print 'Traceback:' + print traceback.print_stack() + log.writeLog2(1, 'Exception: ' + error.info() + '\n') + except Exception, error: + print 'Exception:' + print ' ', str(error) + print 'Traceback:' + print traceback.print_stack() + log.writeLog2(1, 'Exception: ' + str(error) + '\n') + +def main(): + #tk.CallWrapper = ExceptionCatcher + app = Application() + app.start() + app.mainloop() + +if __name__ == '__main__': + main() diff --git a/pjsip-apps/src/pygui/buddy.py b/pjsip-apps/src/pygui/buddy.py new file mode 100644 index 00000000..33537fc0 --- /dev/null +++ b/pjsip-apps/src/pygui/buddy.py @@ -0,0 +1,152 @@ +# $Id$ +# +# pjsua Python GUI Demo +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +if sys.version_info[0] >= 3: # Python 3 + import tkinter as tk + from tkinter import ttk + from tkinter import messagebox as msgbox +else: + import Tkinter as tk + import tkMessageBox as msgbox + import ttk + +import random +import pjsua2 as pj +import endpoint +import application + +# Buddy class +class Buddy(pj.Buddy): + """ + High level Python Buddy object, derived from pjsua2's Buddy object. + """ + def __init__(self, app): + pj.Buddy.__init__(self) + self.app = app + self.randId = random.randint(1, 9999) + self.cfg = None + self.account = None + + def statusText(self): + bi = self.getInfo() + status = '' + if bi.subState == pj.PJSIP_EVSUB_STATE_ACTIVE: + if bi.presStatus.status == pj.PJSUA_BUDDY_STATUS_ONLINE: + status = bi.presStatus.statusText + if not status: + status = 'Online' + elif bi.presStatus.status == pj.PJSUA_BUDDY_STATUS_OFFLINE: + status = 'Offline' + else: + status = 'Unknown' + return status + + def onBuddyState(self): + self.app.updateBuddy(self) + +class SettingDialog(tk.Toplevel): + """ + This implements buddy settings dialog to manipulate buddy settings. + """ + def __init__(self, parent, cfg): + tk.Toplevel.__init__(self, parent) + self.transient(parent) + self.parent = parent + self.geometry("+100+100") + self.title('Buddy settings') + + self.frm = ttk.Frame(self) + self.frm.pack(expand='yes', fill='both') + + self.isOk = False + self.cfg = cfg + + self.createWidgets() + + def doModal(self): + if self.parent: + self.parent.wait_window(self) + else: + self.wait_window(self) + return self.isOk + + def createWidgets(self): + # The notebook + self.frm.rowconfigure(0, weight=1) + self.frm.rowconfigure(1, weight=0) + self.frm.columnconfigure(0, weight=1) + self.frm.columnconfigure(1, weight=1) + self.wTab = ttk.Notebook(self.frm) + self.wTab.grid(column=0, row=0, columnspan=2, padx=5, pady=5, sticky=tk.N+tk.S+tk.W+tk.E) + + # Main buttons + btnOk = ttk.Button(self.frm, text='Ok', command=self.onOk) + btnOk.grid(column=0, row=1, sticky=tk.E, padx=20, pady=10) + btnCancel = ttk.Button(self.frm, text='Cancel', command=self.onCancel) + btnCancel.grid(column=1, row=1, sticky=tk.W, padx=20, pady=10) + + # Tabs + self.createBasicTab() + + def createBasicTab(self): + # Prepare the variables to set/receive values from GUI + self.cfgUri = tk.StringVar() + self.cfgUri.set( self.cfg.uri ) + self.cfgSubscribe = tk.IntVar() + self.cfgSubscribe.set(self.cfg.subscribe) + + # Build the tab page + frm = ttk.Frame(self.frm) + frm.columnconfigure(0, weight=1) + frm.columnconfigure(1, weight=2) + row = 0 + ttk.Label(frm, text='URI:').grid(row=row, column=0, sticky=tk.E, pady=2) + ttk.Entry(frm, textvariable=self.cfgUri, width=40).grid(row=row, column=1, sticky=tk.W+tk.E, padx=6) + row += 1 + ttk.Checkbutton(frm, text='Subscribe presence', variable=self.cfgSubscribe).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + + self.wTab.add(frm, text='Basic Settings') + + + def onOk(self): + # Check basic settings + errors = ""; + if self.cfgUri.get(): + if not endpoint.validateSipUri(self.cfgUri.get()): + errors += "Invalid Buddy URI: '%s'\n" % (self.cfgUri.get()) + + if errors: + msgbox.showerror("Error detected:", errors) + return + + # Basic settings + self.cfg.uri = self.cfgUri.get() + self.cfg.subscribe = self.cfgSubscribe.get() + + self.isOk = True + self.destroy() + + def onCancel(self): + self.destroy() + + +if __name__ == '__main__': + application.main() diff --git a/pjsip-apps/src/pygui/call.py b/pjsip-apps/src/pygui/call.py new file mode 100644 index 00000000..35bf698c --- /dev/null +++ b/pjsip-apps/src/pygui/call.py @@ -0,0 +1,104 @@ +# $Id$ +# +# pjsua Python GUI Demo +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +if sys.version_info[0] >= 3: # Python 3 + import tkinter as tk + from tkinter import ttk + from tkinter import messagebox as msgbox +else: + import Tkinter as tk + import tkMessageBox as msgbox + import ttk + +import random +import pjsua2 as pj +import application +import endpoint as ep + +# Call class +class Call(pj.Call): + """ + High level Python Call object, derived from pjsua2's Call object. + """ + def __init__(self, acc, peer_uri='', chat=None, call_id = pj.PJSUA_INVALID_ID): + pj.Call.__init__(self, acc, call_id) + self.acc = acc + self.peerUri = peer_uri + self.chat = chat + self.connected = False + self.onhold = False + + def onCallState(self, prm): + ci = self.getInfo() + self.connected = ci.state == pj.PJSIP_INV_STATE_CONFIRMED + if self.chat: + self.chat.updateCallState(self, ci) + + def onCallMediaState(self, prm): + ci = self.getInfo() + for mi in ci.media: + if mi.type == pj.PJMEDIA_TYPE_AUDIO and \ + (mi.status == pj.PJSUA_CALL_MEDIA_ACTIVE or \ + mi.status == pj.PJSUA_CALL_MEDIA_REMOTE_HOLD): + m = self.getMedia(mi.index) + am = pj.AudioMedia.typecastFromMedia(m) + # connect ports + ep.Endpoint.instance.audDevManager().getCaptureDevMedia().startTransmit(am) + am.startTransmit(ep.Endpoint.instance.audDevManager().getPlaybackDevMedia()) + + if mi.status == pj.PJSUA_CALL_MEDIA_REMOTE_HOLD and not self.onhold: + self.chat.addMessage(None, "'%s' sets call onhold" % (self.peerUri)) + self.onhold = True + elif mi.status == pj.PJSUA_CALL_MEDIA_ACTIVE and self.onhold: + self.chat.addMessage(None, "'%s' sets call active" % (self.peerUri)) + self.onhold = False + + def onInstantMessage(self, prm): + # chat instance should have been initalized + if not self.chat: return + + self.chat.addMessage(self.peerUri, prm.msgBody) + self.chat.showWindow() + + def onInstantMessageStatus(self, prm): + if prm.code/100 == 2: return + # chat instance should have been initalized + if not self.chat: return + + self.chat.addMessage(None, "Failed sending message to '%s' (%d): %s" % (self.peerUri, prm.code, prm.reason)) + + def onTypingIndication(self, prm): + # chat instance should have been initalized + if not self.chat: return + + self.chat.setTypingIndication(self.peerUri, prm.isTyping) + + def onDtmfDigit(self, prm): + #msgbox.showinfo("pygui", 'Got DTMF:' + prm.digit) + pass + + def onCallMediaTransportState(self, prm): + #msgbox.showinfo("pygui", "Media transport state") + pass + + +if __name__ == '__main__': + application.main() diff --git a/pjsip-apps/src/pygui/chat.py b/pjsip-apps/src/pygui/chat.py new file mode 100644 index 00000000..760af1c7 --- /dev/null +++ b/pjsip-apps/src/pygui/chat.py @@ -0,0 +1,489 @@ +# $Id$ +# +# pjsua Python GUI Demo +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +if sys.version_info[0] >= 3: # Python 3 + import tkinter as tk + from tkinter import ttk +else: + import Tkinter as tk + import ttk + +import buddy +import call +import chatgui as gui +import endpoint as ep +import pjsua2 as pj +import re + +SipUriRegex = re.compile('(sip|sips):([^:;>\@]*)@?([^:;>]*):?([^:;>]*)') +ConfIdx = 1 + +# Simple SIP uri parser, input URI must have been validated +def ParseSipUri(sip_uri_str): + m = SipUriRegex.search(sip_uri_str) + if not m: + assert(0) + return None + + scheme = m.group(1) + user = m.group(2) + host = m.group(3) + port = m.group(4) + if host == '': + host = user + user = '' + + return SipUri(scheme.lower(), user, host.lower(), port) + +class SipUri: + def __init__(self, scheme, user, host, port): + self.scheme = scheme + self.user = user + self.host = host + self.port = port + + def __cmp__(self, sip_uri): + if self.scheme == sip_uri.scheme and self.user == sip_uri.user and self.host == sip_uri.host: + # don't check port, at least for now + return 0 + return -1 + + def __str__(self): + s = self.scheme + ':' + if self.user: s += self.user + '@' + s += self.host + if self.port: s+= ':' + self.port + return s + +class Chat(gui.ChatObserver): + def __init__(self, app, acc, uri, call_inst=None): + self._app = app + self._acc = acc + self.title = '' + + global ConfIdx + self.confIdx = ConfIdx + ConfIdx += 1 + + # each participant call/buddy instances are stored in call list + # and buddy list with same index as in particpant list + self._participantList = [] # list of SipUri + self._callList = [] # list of Call + self._buddyList = [] # list of Buddy + + self._gui = gui.ChatFrame(self) + self.addParticipant(uri, call_inst) + + def _updateGui(self): + if self.isPrivate(): + self.title = str(self._participantList[0]) + else: + self.title = 'Conference #%d (%d participants)' % (self.confIdx, len(self._participantList)) + self._gui.title(self.title) + self._app.updateWindowMenu() + + def _getCallFromUriStr(self, uri_str, op = ''): + uri = ParseSipUri(uri_str) + if uri not in self._participantList: + print "=== %s cannot find participant with URI '%s'" % (op, uri_str) + return None + idx = self._participantList.index(uri) + if idx < len(self._callList): + return self._callList[idx] + return None + + def _getActiveMediaIdx(self, thecall): + ci = thecall.getInfo() + for mi in ci.media: + if mi.type == pj.PJMEDIA_TYPE_AUDIO and \ + (mi.status != pj.PJSUA_CALL_MEDIA_NONE and \ + mi.status != pj.PJSUA_CALL_MEDIA_ERROR): + return mi.index + return -1 + + def _getAudioMediaFromUriStr(self, uri_str): + c = self._getCallFromUriStr(uri_str) + if not c: return None + + idx = self._getActiveMediaIdx(c) + if idx < 0: return None + + m = c.getMedia(idx) + am = pj.AudioMedia.typecastFromMedia(m) + return am + + def _sendTypingIndication(self, is_typing, sender_uri_str=''): + sender_uri = ParseSipUri(sender_uri_str) if sender_uri_str else None + type_ind_param = pj.SendTypingIndicationParam() + type_ind_param.isTyping = is_typing + for idx, p in enumerate(self._participantList): + # don't echo back to the original sender + if sender_uri and p == sender_uri: + continue + + # send via call, if any, or buddy + sender = None + if self._callList[idx] and self._callList[idx].connected: + sender = self._callList[idx] + else: + sender = self._buddyList[idx] + assert(sender) + + try: + sender.sendTypingIndication(type_ind_param) + except: + pass + + def _sendInstantMessage(self, msg, sender_uri_str=''): + sender_uri = ParseSipUri(sender_uri_str) if sender_uri_str else None + send_im_param = pj.SendInstantMessageParam() + send_im_param.content = str(msg) + for idx, p in enumerate(self._participantList): + # don't echo back to the original sender + if sender_uri and p == sender_uri: + continue + + # send via call, if any, or buddy + sender = None + if self._callList[idx] and self._callList[idx].connected: + sender = self._callList[idx] + else: + sender = self._buddyList[idx] + assert(sender) + + try: + sender.sendInstantMessage(send_im_param) + except: + # error will be handled via Account::onInstantMessageStatus() + pass + + def isPrivate(self): + return len(self._participantList) <= 1 + + def isUriParticipant(self, uri): + return uri in self._participantList + + def registerCall(self, uri_str, call_inst): + uri = ParseSipUri(uri_str) + try: + idx = self._participantList.index(uri) + bud = self._buddyList[idx] + self._callList[idx] = call_inst + call_inst.chat = self + call_inst.peerUri = bud.cfg.uri + except: + assert(0) # idx must be found! + + def showWindow(self, show_text_chat = False): + self._gui.bringToFront() + if show_text_chat: + self._gui.textShowHide(True) + + def addParticipant(self, uri, call_inst=None): + # avoid duplication + if self.isUriParticipant(uri): return + + uri_str = str(uri) + + # find buddy, create one if not found (e.g: for IM/typing ind), + # it is a temporary one and not really registered to acc + bud = None + try: + bud = self._acc.findBuddy(uri_str) + except: + bud = buddy.Buddy(None) + bud_cfg = pj.BuddyConfig() + bud_cfg.uri = uri_str + bud_cfg.subscribe = False + bud.create(self._acc, bud_cfg) + bud.cfg = bud_cfg + bud.account = self._acc + + # update URI from buddy URI + uri = ParseSipUri(bud.cfg.uri) + + # add it + self._participantList.append(uri) + self._callList.append(call_inst) + self._buddyList.append(bud) + self._gui.addParticipant(str(uri)) + self._updateGui() + + def kickParticipant(self, uri): + if (not uri) or (uri not in self._participantList): + assert(0) + return + + idx = self._participantList.index(uri) + del self._participantList[idx] + del self._callList[idx] + del self._buddyList[idx] + self._gui.delParticipant(str(uri)) + + if self._participantList: + self._updateGui() + else: + self.onCloseWindow() + + def addMessage(self, from_uri_str, msg): + if from_uri_str: + # print message on GUI + msg = from_uri_str + ': ' + msg + self._gui.textAddMessage(msg) + # now relay to all participants + self._sendInstantMessage(msg, from_uri_str) + else: + self._gui.textAddMessage(msg, False) + + def setTypingIndication(self, from_uri_str, is_typing): + # notify GUI + self._gui.textSetTypingIndication(from_uri_str, is_typing) + # now relay to all participants + self._sendTypingIndication(is_typing, from_uri_str) + + def startCall(self): + self._gui.enableAudio() + call_param = pj.CallOpParam() + call_param.opt.audioCount = 1 + call_param.opt.videoCount = 0 + fails = [] + for idx, p in enumerate(self._participantList): + # just skip if call is instantiated + if self._callList[idx]: + continue + + uri_str = str(p) + c = call.Call(self._acc, uri_str, self) + self._callList[idx] = c + self._gui.audioUpdateState(uri_str, gui.AudioState.INITIALIZING) + + try: + c.makeCall(uri_str, call_param) + except: + self._callList[idx] = None + self._gui.audioUpdateState(uri_str, gui.AudioState.FAILED) + fails.append(p) + + for p in fails: + # kick participants with call failure, but spare the last (avoid zombie chat) + if not self.isPrivate(): + self.kickParticipant(p) + + def stopCall(self): + for idx, p in enumerate(self._participantList): + self._gui.audioUpdateState(str(p), gui.AudioState.DISCONNECTED) + c = self._callList[idx] + if c: + c.hangup(pj.CallOpParam()) + + def updateCallState(self, thecall, info = None): + # info is optional here, just to avoid calling getInfo() twice (in the caller and here) + if not info: info = thecall.getInfo() + + if info.state < pj.PJSIP_INV_STATE_CONFIRMED: + self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.INITIALIZING) + elif info.state == pj.PJSIP_INV_STATE_CONFIRMED: + self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.CONNECTED) + med_idx = self._getActiveMediaIdx(thecall) + si = thecall.getStreamInfo(med_idx) + stats_str = "Audio codec: %s/%s\n..." % (si.codecName, si.codecClockRate) + self._gui.audioSetStatsText(thecall.peerUri, stats_str) + elif info.state == pj.PJSIP_INV_STATE_DISCONNECTED: + if info.lastStatusCode/100 != 2: + self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.FAILED) + else: + self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.DISCONNECTED) + + # reset entry in the callList + try: + idx = self._callList.index(thecall) + if idx >= 0: self._callList[idx] = None + except: + pass + + self.addMessage(None, "Call to '%s' disconnected: %s" % (thecall.peerUri, info.lastReason)) + + # kick the disconnected participant, but the last (avoid zombie chat) + if not self.isPrivate(): + self.kickParticipant(ParseSipUri(thecall.peerUri)) + + + # ** callbacks from GUI (ChatObserver implementation) ** + + # Text + def onSendMessage(self, msg): + self._sendInstantMessage(msg) + + def onStartTyping(self): + self._sendTypingIndication(True) + + def onStopTyping(self): + self._sendTypingIndication(False) + + # Audio + def onHangup(self, peer_uri_str): + c = self._getCallFromUriStr(peer_uri_str, "onHangup()") + if not c: return + call_param = pj.CallOpParam() + c.hangup(call_param) + + def onHold(self, peer_uri_str): + c = self._getCallFromUriStr(peer_uri_str, "onHold()") + if not c: return + call_param = pj.CallOpParam() + c.setHold(call_param) + + def onUnhold(self, peer_uri_str): + c = self._getCallFromUriStr(peer_uri_str, "onUnhold()") + if not c: return + + call_param = pj.CallOpParam() + call_param.opt.audioCount = 1 + call_param.opt.videoCount = 0 + call_param.opt.flag |= pj.PJSUA_CALL_UNHOLD + c.reinvite(call_param) + + def onRxMute(self, peer_uri_str, mute): + am = self._getAudioMediaFromUriStr(peer_uri_str) + if not am: return + if mute: + am.stopTransmit(ep.Endpoint.instance.audDevManager().getPlaybackDevMedia()) + self.addMessage(None, "Muted audio from '%s'" % (peer_uri_str)) + else: + am.startTransmit(ep.Endpoint.instance.audDevManager().getPlaybackDevMedia()) + self.addMessage(None, "Unmuted audio from '%s'" % (peer_uri_str)) + + def onRxVol(self, peer_uri_str, vol_pct): + am = self._getAudioMediaFromUriStr(peer_uri_str) + if not am: return + # pjsua volume range = 0:mute, 1:no adjustment, 2:100% louder + am.adjustRxLevel(vol_pct/50.0) + self.addMessage(None, "Adjusted volume level audio from '%s'" % (peer_uri_str)) + + def onTxMute(self, peer_uri_str, mute): + am = self._getAudioMediaFromUriStr(peer_uri_str) + if not am: return + if mute: + ep.Endpoint.instance.audDevManager().getCaptureDevMedia().stopTransmit(am) + self.addMessage(None, "Muted audio to '%s'" % (peer_uri_str)) + else: + ep.Endpoint.instance.audDevManager().getCaptureDevMedia().startTransmit(am) + self.addMessage(None, "Unmuted audio to '%s'" % (peer_uri_str)) + + # Chat room + def onAddParticipant(self): + buds = [] + dlg = AddParticipantDlg(None, self._app, buds) + if dlg.doModal(): + for bud in buds: + uri = ParseSipUri(bud.cfg.uri) + self.addParticipant(uri) + if not self.isPrivate(): + self.startCall() + + def onStartAudio(self): + self.startCall() + + def onStopAudio(self): + self.stopCall() + + def onCloseWindow(self): + self.stopCall() + # will remove entry from list eventually destroy this chat? + if self in self._acc.chatList: self._acc.chatList.remove(self) + self._app.updateWindowMenu() + # destroy GUI + self._gui.destroy() + + +class AddParticipantDlg(tk.Toplevel): + """ + List of buddies + """ + def __init__(self, parent, app, bud_list): + tk.Toplevel.__init__(self, parent) + self.title('Add participants..') + self.transient(parent) + self.parent = parent + self._app = app + self.buddyList = bud_list + + self.isOk = False + + self.createWidgets() + + def doModal(self): + if self.parent: + self.parent.wait_window(self) + else: + self.wait_window(self) + return self.isOk + + def createWidgets(self): + # buddy list + list_frame = ttk.Frame(self) + list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=1, padx=20, pady=20) + #scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=list_frame.yview) + #list_frame.config(yscrollcommand=scrl.set) + #scrl.pack(side=tk.RIGHT, fill=tk.Y) + + # draw buddy list + self.buddies = [] + for acc in self._app.accList: + self.buddies.append((0, acc.cfg.idUri)) + for bud in acc.buddyList: + self.buddies.append((1, bud)) + + self.bud_var = [] + for idx,(flag,bud) in enumerate(self.buddies): + self.bud_var.append(tk.IntVar()) + if flag==0: + s = ttk.Separator(list_frame, orient=tk.HORIZONTAL) + s.pack(fill=tk.X) + l = tk.Label(list_frame, anchor=tk.W, text="Account '%s':" % (bud)) + l.pack(fill=tk.X) + else: + c = tk.Checkbutton(list_frame, anchor=tk.W, text=bud.cfg.uri, variable=self.bud_var[idx]) + c.pack(fill=tk.X) + s = ttk.Separator(list_frame, orient=tk.HORIZONTAL) + s.pack(fill=tk.X) + + # Ok/cancel buttons + tail_frame = ttk.Frame(self) + tail_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) + + btnOk = ttk.Button(tail_frame, text='Ok', default=tk.ACTIVE, command=self.onOk) + btnOk.pack(side=tk.LEFT, padx=20, pady=10) + btnCancel = ttk.Button(tail_frame, text='Cancel', command=self.onCancel) + btnCancel.pack(side=tk.RIGHT, padx=20, pady=10) + + def onOk(self): + self.buddyList[:] = [] + for idx,(flag,bud) in enumerate(self.buddies): + if not flag: continue + if self.bud_var[idx].get() and not (bud in self.buddyList): + self.buddyList.append(bud) + + self.isOk = True + self.destroy() + + def onCancel(self): + self.destroy() diff --git a/pjsip-apps/src/pygui/chatgui.py b/pjsip-apps/src/pygui/chatgui.py new file mode 100644 index 00000000..bb58e286 --- /dev/null +++ b/pjsip-apps/src/pygui/chatgui.py @@ -0,0 +1,420 @@ +# $Id$ +# +# pjsua Python GUI Demo +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +if sys.version_info[0] >= 3: # Python 3 + import tkinter as tk + from tkinter import ttk + from tkinter import messagebox as msgbox +else: + import Tkinter as tk + import ttk + import tkMessageBox as msgbox + + +class TextObserver: + def onSendMessage(self, msg): + pass + def onStartTyping(self): + pass + def onStopTyping(self): + pass + +class TextFrame(ttk.Frame): + def __init__(self, master, observer): + ttk.Frame.__init__(self, master) + self._observer = observer + self._isTyping = False + self._createWidgets() + + def _onSendMessage(self, event): + send_text = self._typingBox.get("1.0", tk.END).strip() + if send_text == '': + return + + self.addMessage('me: ' + send_text) + self._typingBox.delete("0.0", tk.END) + self._onTyping(None) + + # notify app for sending message + self._observer.onSendMessage(send_text) + + def _onTyping(self, event): + # notify app for typing indication + is_typing = self._typingBox.get("1.0", tk.END).strip() != '' + if is_typing != self._isTyping: + self._isTyping = is_typing + if is_typing: + self._observer.onStartTyping() + else: + self._observer.onStopTyping() + + def _createWidgets(self): + self.rowconfigure(0, weight=1) + self.rowconfigure(1, weight=0) + self.rowconfigure(2, weight=0) + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=0) + + self._text = tk.Text(self, width=50, height=30, font=("Arial", "10")) + self._text.grid(row=0, column=0, sticky='nswe') + self._text.config(state=tk.DISABLED) + self._text.tag_config("info", foreground="darkgray", font=("Arial", "9", "italic")) + + scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self._text.yview) + self._text.config(yscrollcommand=scrl.set) + scrl.grid(row=0, column=1, sticky='nsw') + + self._typingBox = tk.Text(self, width=50, height=1, font=("Arial", "10")) + self._typingBox.grid(row=1, columnspan=2, sticky='we', pady=0) + + self._statusBar = tk.Label(self, anchor='w', font=("Arial", "8", "italic")) + self._statusBar.grid(row=2, columnspan=2, sticky='we') + + self._typingBox.bind('<Return>', self._onSendMessage) + self._typingBox.bind("<Key>", self._onTyping) + self._typingBox.focus_set() + + def addMessage(self, msg, is_chat = True): + self._text.config(state=tk.NORMAL) + if is_chat: + self._text.insert(tk.END, msg+'\r\n') + else: + self._text.insert(tk.END, msg+'\r\n', 'info') + self._text.config(state=tk.DISABLED) + self._text.yview(tk.END) + + def setTypingIndication(self, who, is_typing): + if is_typing: + self._statusBar['text'] = "'%s' is typing.." % (who) + else: + self._statusBar['text'] = '' + +class AudioState: + NULL, INITIALIZING, CONNECTED, DISCONNECTED, FAILED = range(5) + +class AudioObserver: + def onHangup(self, peer_uri): + pass + def onHold(self, peer_uri): + pass + def onUnhold(self, peer_uri): + pass + def onRxMute(self, peer_uri, is_muted): + pass + def onRxVol(self, peer_uri, vol_pct): + pass + def onTxMute(self, peer_uri, is_muted): + pass + + +class AudioFrame(ttk.Labelframe): + def __init__(self, master, peer_uri, observer): + ttk.Labelframe.__init__(self, master, text=peer_uri) + self.peerUri = peer_uri + self._observer = observer + self._initFrame = None + self._callFrame = None + self._rxMute = False + self._txMute = False + self._state = AudioState.NULL + + self._createInitWidgets() + self._createWidgets() + + def updateState(self, state): + if self._state == state: + return + + if state == AudioState.INITIALIZING: + self._callFrame.pack_forget() + self._initFrame.pack(fill=tk.BOTH) + self._btnCancel.pack(side=tk.TOP) + self._lblInitState['text'] = 'Intializing..' + + elif state == AudioState.CONNECTED: + self._initFrame.pack_forget() + self._callFrame.pack(fill=tk.BOTH) + else: + self._callFrame.pack_forget() + self._initFrame.pack(fill=tk.BOTH) + if state == AudioState.FAILED: + self._lblInitState['text'] = 'Failed' + else: + self._lblInitState['text'] = 'Normal cleared' + self._btnCancel.pack_forget() + + self._btnHold['text'] = 'Hold' + self._btnHold.config(state=tk.NORMAL) + self._rxMute = False + self._txMute = False + self.btnRxMute['text'] = 'Mute' + self.btnTxMute['text'] = 'Mute' + self.rxVol.set(5.0) + + # save last state + self._state = state + + def setStatsText(self, stats_str): + self.stat.config(state=tk.NORMAL) + self.stat.delete("0.0", tk.END) + self.stat.insert(tk.END, stats_str) + self.stat.config(state=tk.DISABLED) + + def _onHold(self): + self._btnHold.config(state=tk.DISABLED) + # notify app + if self._btnHold['text'] == 'Hold': + self._observer.onHold(self.peerUri) + self._btnHold['text'] = 'Unhold' + else: + self._observer.onUnhold(self.peerUri) + self._btnHold['text'] = 'Hold' + self._btnHold.config(state=tk.NORMAL) + + def _onHangup(self): + # notify app + self._observer.onHangup(self.peerUri) + + def _onRxMute(self): + # notify app + self._rxMute = not self._rxMute + self._observer.onRxMute(self.peerUri, self._rxMute) + self.btnRxMute['text'] = 'Unmute' if self._rxMute else 'Mute' + + def _onRxVol(self, event): + # notify app + vol = self.rxVol.get() + self._observer.onRxVol(self.peerUri, vol*10.0) + + def _onTxMute(self): + # notify app + self._txMute = not self._txMute + self._observer.onTxMute(self.peerUri, self._txMute) + self.btnTxMute['text'] = 'Unmute' if self._txMute else 'Mute' + + def _createInitWidgets(self): + self._initFrame = ttk.Frame(self) + #self._initFrame.pack(fill=tk.BOTH) + + + self._lblInitState = tk.Label(self._initFrame, font=("Arial", "12"), text='') + self._lblInitState.pack(side=tk.TOP, fill=tk.X, expand=1) + + # Operation: cancel/kick + self._btnCancel = ttk.Button(self._initFrame, text = 'Cancel', command=self._onHangup) + self._btnCancel.pack(side=tk.TOP) + + def _createWidgets(self): + self._callFrame = ttk.Frame(self) + #self._callFrame.pack(fill=tk.BOTH) + + # toolbar + toolbar = ttk.Frame(self._callFrame) + toolbar.pack(side=tk.TOP, fill=tk.X) + self._btnHold = ttk.Button(toolbar, text='Hold', command=self._onHold) + self._btnHold.pack(side=tk.LEFT, fill=tk.Y) + #self._btnXfer = ttk.Button(toolbar, text='Transfer..') + #self._btnXfer.pack(side=tk.LEFT, fill=tk.Y) + self._btnHangUp = ttk.Button(toolbar, text='Hangup', command=self._onHangup) + self._btnHangUp.pack(side=tk.LEFT, fill=tk.Y) + + # volume tool + vol_frm = ttk.Frame(self._callFrame) + vol_frm.pack(side=tk.TOP, fill=tk.X) + + self.rxVolFrm = ttk.Labelframe(vol_frm, text='RX volume') + self.rxVolFrm.pack(side=tk.LEFT, fill=tk.Y) + + self.btnRxMute = ttk.Button(self.rxVolFrm, width=8, text='Mute', command=self._onRxMute) + self.btnRxMute.pack(side=tk.LEFT) + self.rxVol = tk.Scale(self.rxVolFrm, orient=tk.HORIZONTAL, from_=0.0, to=10.0, showvalue=1) #, tickinterval=10.0, showvalue=1) + self.rxVol.set(5.0) + self.rxVol.bind("<ButtonRelease-1>", self._onRxVol) + self.rxVol.pack(side=tk.LEFT) + + self.txVolFrm = ttk.Labelframe(vol_frm, text='TX volume') + self.txVolFrm.pack(side=tk.RIGHT, fill=tk.Y) + + self.btnTxMute = ttk.Button(self.txVolFrm, width=8, text='Mute', command=self._onTxMute) + self.btnTxMute.pack(side=tk.LEFT) + + # stat + self.stat = tk.Text(self._callFrame, width=10, height=2, bg='lightgray', relief=tk.FLAT, font=("Arial", "9")) + self.stat.insert(tk.END, 'stat here') + self.stat.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) + + +class ChatObserver(TextObserver, AudioObserver): + def onAddParticipant(self): + pass + def onStartAudio(self): + pass + def onStopAudio(self): + pass + def onCloseWindow(self): + pass + +class ChatFrame(tk.Toplevel): + """ + Room + """ + def __init__(self, observer): + tk.Toplevel.__init__(self) + self.protocol("WM_DELETE_WINDOW", self._onClose) + self._observer = observer + + self._text = None + self._text_shown = True + + self._audioEnabled = False + self._audioFrames = [] + self._createWidgets() + + def _createWidgets(self): + # toolbar + self.toolbar = ttk.Frame(self) + self.toolbar.pack(side=tk.TOP, fill=tk.BOTH) + + btnText = ttk.Button(self.toolbar, text='Show/hide text', command=self._onShowHideText) + btnText.pack(side=tk.LEFT, fill=tk.Y) + btnAudio = ttk.Button(self.toolbar, text='Start/stop audio', command=self._onStartStopAudio) + btnAudio.pack(side=tk.LEFT, fill=tk.Y) + + ttk.Separator(self.toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx = 4) + + btnAdd = ttk.Button(self.toolbar, text='Add participant..', command=self._onAddParticipant) + btnAdd.pack(side=tk.LEFT, fill=tk.Y) + + # media frame + self.media = ttk.Frame(self) + self.media.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) + + # create Text Chat frame + self.media_left = ttk.Frame(self.media) + self._text = TextFrame(self.media_left, self._observer) + self._text.pack(fill=tk.BOTH, expand=1) + self.media_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=1) + + # create other media frame + self.media_right = ttk.Frame(self.media) + + def _arrangeMediaFrames(self): + if len(self._audioFrames) == 0: + self.media_right.pack_forget() + return + + self.media_right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=1) + MAX_ROWS = 3 + row_num = 0 + col_num = 1 + for frm in self._audioFrames: + frm.grid(row=row_num, column=col_num, sticky='nsew', padx=5, pady=5) + row_num += 1 + if row_num >= MAX_ROWS: + row_num = 0 + col_num += 1 + + def _onShowHideText(self): + self.textShowHide(not self._text_shown) + + def _onAddParticipant(self): + self._observer.onAddParticipant() + + def _onStartStopAudio(self): + self._audioEnabled = not self._audioEnabled + if self._audioEnabled: + self._observer.onStartAudio() + else: + self._observer.onStopAudio() + self.enableAudio(self._audioEnabled) + + def _onClose(self): + self._observer.onCloseWindow() + + # APIs + + def bringToFront(self): + self.deiconify() + self.lift() + self._text._typingBox.focus_set() + + def textAddMessage(self, msg, is_chat = True): + self._text.addMessage(msg, is_chat) + + def textSetTypingIndication(self, who, is_typing = True): + self._text.setTypingIndication(who, is_typing) + + def addParticipant(self, participant_uri): + aud_frm = AudioFrame(self.media_right, participant_uri, self._observer) + self._audioFrames.append(aud_frm) + + def delParticipant(self, participant_uri): + for aud_frm in self._audioFrames: + if participant_uri == aud_frm.peerUri: + self._audioFrames.remove(aud_frm) + # need to delete aud_frm manually? + aud_frm.destroy() + return + + def textShowHide(self, show = True): + if show: + self.media_left.pack(side=tk.LEFT, fill=tk.BOTH, expand=1) + self._text._typingBox.focus_set() + else: + self.media_left.pack_forget() + self._text_shown = show + + def enableAudio(self, is_enabled = True): + if is_enabled: + self._arrangeMediaFrames() + else: + self.media_right.pack_forget() + self._audioEnabled = is_enabled + + def audioUpdateState(self, participant_uri, state): + for aud_frm in self._audioFrames: + if participant_uri == aud_frm.peerUri: + aud_frm.updateState(state) + break + if state >= AudioState.DISCONNECTED and len(self._audioFrames) == 1: + self.enableAudio(False) + else: + self.enableAudio(True) + + def audioSetStatsText(self, participant_uri, stats_str): + for aud_frm in self._audioFrames: + if participant_uri == aud_frm.peerUri: + aud_frm.setStatsText(stats_str) + break + +if __name__ == '__main__': + root = tk.Tk() + root.title("Chat") + root.columnconfigure(0, weight=1) + root.rowconfigure(0, weight=1) + + obs = ChatObserver() + dlg = ChatFrame(obs) + #dlg = TextFrame(root) + #dlg = AudioFrame(root) + + #dlg.pack(fill=tk.BOTH, expand=1) + root.mainloop() diff --git a/pjsip-apps/src/pygui/endpoint.py b/pjsip-apps/src/pygui/endpoint.py new file mode 100644 index 00000000..d76dbf3f --- /dev/null +++ b/pjsip-apps/src/pygui/endpoint.py @@ -0,0 +1,53 @@ +# $Id$ +# +# pjsua Python GUI Demo +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +if sys.version_info[0] >= 3: # Python 3 + import tkinter as tk + from tkinter import ttk + from tkinter import messagebox as msgbox +else: + import Tkinter as tk + import tkMessageBox as msgbox + import ttk + +import pjsua2 as pj +import application + + +class Endpoint(pj.Endpoint): + """ + This is high level Python object inherited from pj.Endpoint + """ + instance = None + def __init__(self): + pj.Endpoint.__init__(self) + Endpoint.instance = self + + +def validateUri(uri): + return Endpoint.instance.utilVerifyUri(uri) == pj.PJ_SUCCESS + +def validateSipUri(uri): + return Endpoint.instance.utilVerifySipUri(uri) == pj.PJ_SUCCESS + + +if __name__ == '__main__': + application.main() diff --git a/pjsip-apps/src/pygui/log.py b/pjsip-apps/src/pygui/log.py new file mode 100644 index 00000000..d3772419 --- /dev/null +++ b/pjsip-apps/src/pygui/log.py @@ -0,0 +1,127 @@ +# $Id$ +# +# pjsua Python GUI Demo +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +if sys.version_info[0] >= 3: # Python 3 + import tkinter as tk + from tkinter import ttk + from tkinter import messagebox as msgbox +else: + import Tkinter as tk + import tkMessageBox as msgbox + import ttk + +import pjsua2 as pj +import application + + +class LogWindow(tk.Toplevel): + """ + Log window + """ + instance = None + def __init__(self, app): + tk.Toplevel.__init__(self, name='logwnd', width=640, height=480) + LogWindow.instance = self + self.app = app + self.state('withdrawn') + self.title('Log') + self._createWidgets() + self.protocol("WM_DELETE_WINDOW", self._onHide) + + def addLog(self, entry): + """entry fields: + int level; + string msg; + long threadId; + string threadName; + """ + self.addLog2(entry.level, entry.msg) + + def addLog2(self, level, msg): + if level==5: + tags = ('trace',) + elif level==3: + tags = ('info',) + elif level==2: + tags = ('warning',) + elif level<=1: + tags = ('error',) + else: + tags = None + self.text.insert(tk.END, msg, tags) + self.text.see(tk.END) + + def _createWidgets(self): + self.rowconfigure(0, weight=1) + self.rowconfigure(1, weight=0) + self.columnconfigure(0, weight=1) + self.columnconfigure(1, weight=0) + + self.text = tk.Text(self, font=('Courier New', '8'), wrap=tk.NONE, undo=False, padx=4, pady=5) + self.text.grid(row=0, column=0, sticky='nswe', padx=5, pady=5) + + scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview) + self.text.config(yscrollcommand=scrl.set) + scrl.grid(row=0, column=1, sticky='nsw', padx=5, pady=5) + + scrl = ttk.Scrollbar(self, orient=tk.HORIZONTAL, command=self.text.xview) + self.text.config(xscrollcommand=scrl.set) + scrl.grid(row=1, column=0, sticky='we', padx=5, pady=5) + + self.text.bind("<Key>", self._onKey) + + self.text.tag_configure('normal', font=('Courier New', '8'), foreground='black') + self.text.tag_configure('trace', font=('Courier New', '8'), foreground='#777777') + self.text.tag_configure('info', font=('Courier New', '8', 'bold'), foreground='black') + self.text.tag_configure('warning', font=('Courier New', '8', 'bold'), foreground='cyan') + self.text.tag_configure('error', font=('Courier New', '8', 'bold'), foreground='red') + + def _onKey(self, event): + # Ignore key event to make text widget read-only + return "break" + + def _onHide(self): + # Hide when close ('x') button is clicked + self.withdraw() + self.app.showLogWindow.set(0) + + +def writeLog2(level, msg): + if LogWindow.instance: + LogWindow.instance.addLog2(level, msg) + +def writeLog(entry): + if LogWindow.instance: + LogWindow.instance.addLog(entry) + +class Logger(pj.LogWriter): + """ + Logger to receive log messages from pjsua2 + """ + def __init__(self): + pj.LogWriter.__init__(self) + + def write(self, entry): + print entry.msg, + writeLog(entry) + +if __name__ == '__main__': + application.main() diff --git a/pjsip-apps/src/pygui/settings.py b/pjsip-apps/src/pygui/settings.py new file mode 100644 index 00000000..25f053e2 --- /dev/null +++ b/pjsip-apps/src/pygui/settings.py @@ -0,0 +1,362 @@ +# $Id$ +# +# pjsua Python GUI Demo +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +if sys.version_info[0] >= 3: # Python 3 + import tkinter as tk + from tkinter import ttk + from tkinter import messagebox as msgbox +else: + import Tkinter as tk + import tkMessageBox as msgbox + import ttk + +import pjsua2 as pj +#import application + +# Transport setting +class SipTransportConfig: + def __init__(self, type, enabled): + #pj.PersistentObject.__init__(self) + self.type = type + self.enabled = enabled + self.config = pj.TransportConfig() + def readObject(self, node): + child_node = node.readContainer("SipTransport") + self.type = child_node.readInt("type") + self.enabled = child_node.readBool("enabled") + self.config.readObject(child_node) + def writeObject(self, node): + child_node = node.writeNewContainer("SipTransport") + child_node.writeInt("type", self.type) + child_node.writeBool("enabled", self.enabled) + self.config.writeObject(child_node) + +# Account setting with buddy list +class AccConfig: + def __init__(self): + self.enabled = True + self.config = pj.AccountConfig() + self.buddyConfigs = [] + def readObject(self, node): + acc_node = node.readContainer("Account") + self.enabled = acc_node.readBool("enabled") + self.config.readObject(acc_node) + buddy_node = acc_node.readArray("buddies") + while buddy_node.hasUnread(): + buddy_cfg = pj.BuddyConfig() + buddy_cfg.readObject(buddy_node) + self.buddyConfigs.append(buddy_cfg) + def writeObject(self, node): + acc_node = node.writeNewContainer("Account") + acc_node.writeBool("enabled", self.enabled) + self.config.writeObject(acc_node) + buddy_node = acc_node.writeNewArray("buddies") + for buddy in self.buddyConfigs: + buddy_node.writeObject(buddy) + + +# Master settings +class AppConfig: + def __init__(self): + self.epConfig = pj.EpConfig() # pj.EpConfig() + self.udp = SipTransportConfig(pj.PJSIP_TRANSPORT_UDP, True) + self.tcp = SipTransportConfig(pj.PJSIP_TRANSPORT_TCP, True) + self.tls = SipTransportConfig(pj.PJSIP_TRANSPORT_TLS, False) + self.accounts = [] # Array of AccConfig + + def loadFile(self, file): + json = pj.JsonDocument() + json.loadFile(file) + root = json.getRootContainer() + self.epConfig = pj.EpConfig() + self.epConfig.readObject(root) + + tp_node = root.readArray("transports") + self.udp.readObject(tp_node) + self.tcp.readObject(tp_node) + if tp_node.hasUnread(): + self.tls.readObject(tp_node) + + acc_node = root.readArray("accounts") + while acc_node.hasUnread(): + acfg = AccConfig() + acfg.readObject(acc_node) + self.accounts.append(acfg) + + def saveFile(self,file): + json = pj.JsonDocument() + + # Write endpoint config + json.writeObject(self.epConfig) + + # Write transport config + tp_node = json.writeNewArray("transports") + self.udp.writeObject(tp_node) + self.tcp.writeObject(tp_node) + self.tls.writeObject(tp_node) + + # Write account configs + node = json.writeNewArray("accounts") + for acc in self.accounts: + acc.writeObject(node) + + json.saveFile(file) + + +# Settings dialog +class Dialog(tk.Toplevel): + """ + This implements account settings dialog to manipulate account settings. + """ + def __init__(self, parent, cfg): + tk.Toplevel.__init__(self, parent) + self.transient(parent) + self.parent = parent + self.title('Settings') + + self.frm = ttk.Frame(self) + self.frm.pack(expand='yes', fill='both') + + self.isOk = False + self.cfg = cfg + + self.createWidgets() + + def doModal(self): + if self.parent: + self.parent.wait_window(self) + else: + self.wait_window(self) + return self.isOk + + def createWidgets(self): + # The notebook + self.frm.rowconfigure(0, weight=1) + self.frm.rowconfigure(1, weight=0) + self.frm.columnconfigure(0, weight=1) + self.frm.columnconfigure(1, weight=1) + self.wTab = ttk.Notebook(self.frm) + self.wTab.grid(column=0, row=0, columnspan=2, padx=10, pady=10, ipadx=20, ipady=20, sticky=tk.N+tk.S+tk.W+tk.E) + + # Main buttons + btnOk = ttk.Button(self.frm, text='Ok', command=self.onOk) + btnOk.grid(column=0, row=1, sticky=tk.E, padx=20, pady=10) + btnCancel = ttk.Button(self.frm, text='Cancel', command=self.onCancel) + btnCancel.grid(column=1, row=1, sticky=tk.W, padx=20, pady=10) + + # Tabs + self.createBasicTab() + self.createNetworkTab() + self.createMediaTab() + + def createBasicTab(self): + # Prepare the variables to set/receive values from GUI + self.cfgLogFile = tk.StringVar(value=self.cfg.epConfig.logConfig.filename) + self.cfgLogAppend = tk.BooleanVar(value=True if (self.cfg.epConfig.logConfig.fileFlags & pj.PJ_O_APPEND) else False) + + # Build the tab page + frm = ttk.Frame(self.frm) + frm.columnconfigure(0, weight=1) + frm.columnconfigure(1, weight=2) + row = 0 + ttk.Label(frm, text='User Agent:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Label(frm, text=self.cfg.epConfig.uaConfig.userAgent).grid(row=row, column=1, sticky=tk.W, pady=2, padx=6) + row += 1 + ttk.Label(frm, text='Max calls:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Label(frm, text=str(self.cfg.epConfig.uaConfig.maxCalls)).grid(row=row, column=1, sticky=tk.W, pady=2, padx=6) + row += 1 + ttk.Label(frm, text='Log file:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Entry(frm, textvariable=self.cfgLogFile, width=32).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Checkbutton(frm, text='Append log file', variable=self.cfgLogAppend).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + + self.wTab.add(frm, text='Basic') + + def createNetworkTab(self): + self.cfgNameserver = tk.StringVar() + if len(self.cfg.epConfig.uaConfig.nameserver): + self.cfgNameserver.set(self.cfg.epConfig.uaConfig.nameserver[0]) + self.cfgStunServer = tk.StringVar() + if len(self.cfg.epConfig.uaConfig.stunServer): + self.cfgStunServer.set(self.cfg.epConfig.uaConfig.stunServer[0]) + self.cfgStunIgnoreError = tk.BooleanVar(value=self.cfg.epConfig.uaConfig.stunIgnoreFailure) + + self.cfgUdpEnabled = tk.BooleanVar(value=self.cfg.udp.enabled) + self.cfgUdpPort = tk.IntVar(value=self.cfg.udp.config.port) + self.cfgTcpEnabled = tk.BooleanVar(value=self.cfg.tcp.enabled) + self.cfgTcpPort = tk.IntVar(value=self.cfg.tcp.config.port) + self.cfgTlsEnabled = tk.BooleanVar(value=self.cfg.tls.enabled) + self.cfgTlsPort = tk.IntVar(value=self.cfg.tls.config.port) + + self.cfgTlsCaFile = tk.StringVar(value=self.cfg.tls.config.tlsConfig.CaListFile) + self.cfgTlsCertFile = tk.StringVar(value=self.cfg.tls.config.tlsConfig.certFile) + self.cfgTlsVerifyClient = tk.BooleanVar(value=self.cfg.tls.config.tlsConfig.verifyClient) + self.cfgTlsVerifyServer = tk.BooleanVar(value=self.cfg.tls.config.tlsConfig.verifyServer) + + # Build the tab page + frm = ttk.Frame(self.frm) + frm.columnconfigure(0, weight=1) + frm.columnconfigure(1, weight=2) + row = 0 + #ttk.Label(frm, text='UDP transport:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Checkbutton(frm, text='Enable UDP transport', variable=self.cfgUdpEnabled).grid(row=row, column=0, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='UDP port:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + tk.Spinbox(frm, from_=0, to=65535, textvariable=self.cfgUdpPort, width=5).grid(row=row, column=1, sticky=tk.W, padx=6) + ttk.Label(frm, text='(0 for any)').grid(row=row, column=1, sticky=tk.E, pady=6, padx=6) + row += 1 + #ttk.Label(frm, text='TCP transport:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Checkbutton(frm, text='Enable TCP transport', variable=self.cfgTcpEnabled).grid(row=row, column=0, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='TCP port:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + tk.Spinbox(frm, from_=0, to=65535, textvariable=self.cfgTcpPort, width=5).grid(row=row, column=1, sticky=tk.W, padx=6) + ttk.Label(frm, text='(0 for any)').grid(row=row, column=1, sticky=tk.E, pady=6, padx=6) + row += 1 + #ttk.Label(frm, text='TLS transport:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Checkbutton(frm, text='Enable TLS transport', variable=self.cfgTlsEnabled).grid(row=row, column=0, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='TLS port:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + tk.Spinbox(frm, from_=0, to=65535, textvariable=self.cfgTlsPort, width=5).grid(row=row, column=1, sticky=tk.W, padx=6) + ttk.Label(frm, text='(0 for any)').grid(row=row, column=1, sticky=tk.E, pady=6, padx=6) + row += 1 + ttk.Label(frm, text='TLS CA file:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Entry(frm, textvariable=self.cfgTlsCaFile, width=32).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='TLS cert file:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Entry(frm, textvariable=self.cfgTlsCertFile, width=32).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Checkbutton(frm, text='TLS verify server', variable=self.cfgTlsVerifyServer).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Checkbutton(frm, text='TLS verify client', variable=self.cfgTlsVerifyClient).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='DNS and STUN:').grid(row=row, column=0, sticky=tk.W, pady=2, padx=8) + row += 1 + ttk.Label(frm, text='Nameserver:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Entry(frm, textvariable=self.cfgNameserver, width=32).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='STUN Server:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Entry(frm, textvariable=self.cfgStunServer, width=32).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Checkbutton(frm, text='Ignore STUN failure at startup', variable=self.cfgStunIgnoreError).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + + self.wTab.add(frm, text='Network') + + def createMediaTab(self): + self.cfgClockrate = tk.IntVar(value=self.cfg.epConfig.medConfig.clockRate) + self.cfgSndClockrate = tk.IntVar(value=self.cfg.epConfig.medConfig.sndClockRate) + self.cfgAudioPtime = tk.IntVar(value=self.cfg.epConfig.medConfig.audioFramePtime) + self.cfgMediaQuality = tk.IntVar(value=self.cfg.epConfig.medConfig.quality) + self.cfgCodecPtime = tk.IntVar(value=self.cfg.epConfig.medConfig.ptime) + self.cfgVad = tk.BooleanVar(value=not self.cfg.epConfig.medConfig.noVad) + self.cfgEcTailLen = tk.IntVar(value=self.cfg.epConfig.medConfig.ecTailLen) + + # Build the tab page + frm = ttk.Frame(self.frm) + frm.columnconfigure(0, weight=1) + frm.columnconfigure(1, weight=2) + row = 0 + ttk.Label(frm, text='Max media ports:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Label(frm, text=str(self.cfg.epConfig.medConfig.maxMediaPorts)).grid(row=row, column=1, sticky=tk.W, pady=2, padx=6) + row += 1 + ttk.Label(frm, text='Core clock rate:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + tk.Spinbox(frm, from_=8000, to=48000, increment=8000, textvariable=self.cfgClockrate, width=5).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='Snd device clock rate:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + tk.Spinbox(frm, from_=0, to=48000, increment=8000, textvariable=self.cfgSndClockrate, width=5).grid(row=row, column=1, sticky=tk.W, padx=6) + ttk.Label(frm, text='(0: follow core)').grid(row=row, column=1, sticky=tk.E, pady=6, padx=6) + row += 1 + ttk.Label(frm, text='Core ptime:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + tk.Spinbox(frm, from_=10, to=400, increment=10, textvariable=self.cfgAudioPtime, width=3).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='RTP ptime:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + tk.Spinbox(frm, from_=20, to=400, increment=10, textvariable=self.cfgCodecPtime, width=3).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='Media quality (1-10):').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + tk.Spinbox(frm, from_=1, to=10, textvariable=self.cfgMediaQuality, width=5).grid(row=row, column=1, sticky=tk.W, padx=6) + row += 1 + ttk.Label(frm, text='VAD:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + ttk.Checkbutton(frm, text='Enable', variable=self.cfgVad).grid(row=row, column=1, sticky=tk.W, padx=6, pady=2) + row += 1 + ttk.Label(frm, text='Echo canceller tail length:').grid(row=row, column=0, sticky=tk.E, pady=2, padx=8) + tk.Spinbox(frm, from_=0, to=400, increment=10, textvariable=self.cfgEcTailLen, width=3).grid(row=row, column=1, sticky=tk.W, padx=6) + ttk.Label(frm, text='(ms, 0 to disable)').grid(row=row, column=1, sticky=tk.E, pady=6, padx=6) + + self.wTab.add(frm, text='Media') + + def onOk(self): + # Check basic settings + errors = ""; + if errors: + msgbox.showerror("Error detected:", errors) + return + + # Basic settings + self.cfg.epConfig.logConfig.filename = self.cfgLogFile.get() + flags = pj.PJ_O_APPEND if self.cfgLogAppend.get() else 0 + self.cfg.epConfig.logConfig.fileFlags = self.cfg.epConfig.logConfig.fileFlags | flags + + # Network settings + self.cfg.epConfig.uaConfig.nameserver.clear() + if len(self.cfgNameserver.get()): + self.cfg.epConfig.uaConfig.nameserver.append(self.cfgNameserver.get()) + self.cfg.epConfig.uaConfig.stunServer.clear() + if len(self.cfgStunServer.get()): + self.cfg.epConfig.uaConfig.stunServer.append(self.cfgStunServer.get()) + + self.cfg.epConfig.uaConfig.stunIgnoreFailure = self.cfgStunIgnoreError.get() + + self.cfg.udp.enabled = self.cfgUdpEnabled.get() + self.cfg.udp.config.port = self.cfgUdpPort.get() + self.cfg.tcp.enabled = self.cfgTcpEnabled.get() + self.cfg.tcp.config.port = self.cfgTcpPort.get() + self.cfg.tls.enabled = self.cfgTlsEnabled.get() + self.cfg.tls.config.port = self.cfgTlsPort.get() + + self.cfg.tls.config.tlsConfig.CaListFile = self.cfgTlsCaFile.get() + self.cfg.tls.config.tlsConfig.certFile = self.cfgTlsCertFile.get() + self.cfg.tls.config.tlsConfig.verifyClient = self.cfgTlsVerifyClient.get() + self.cfg.tls.config.tlsConfig.verifyServer = self.cfgTlsVerifyServer.get() + + # Media + self.cfg.epConfig.medConfig.clockRate = self.cfgClockrate.get() + self.cfg.epConfig.medConfig.sndClockRate = self.cfgSndClockrate.get() + self.cfg.epConfig.medConfig.audioFramePtime = self.cfgAudioPtime.get() + self.cfg.epConfig.medConfig.quality = self.cfgMediaQuality.get() + self.cfg.epConfig.medConfig.ptime = self.cfgCodecPtime.get() + self.cfg.epConfig.medConfig.noVad = not self.cfgVad.get() + self.cfg.epConfig.medConfig.ecTailLen = self.cfgEcTailLen.get() + + self.isOk = True + self.destroy() + + def onCancel(self): + self.destroy() + + +if __name__ == '__main__': + #application.main() + acfg = AppConfig() + acfg.loadFile('pygui.js') + + dlg = Dialog(None, acfg) + if dlg.doModal(): + acfg.saveFile('pygui.js') +
\ No newline at end of file diff --git a/pjsip-apps/src/samples/debug.c b/pjsip-apps/src/samples/debug.cpp index 6f79d9f8..6f79d9f8 100644 --- a/pjsip-apps/src/samples/debug.c +++ b/pjsip-apps/src/samples/debug.cpp diff --git a/pjsip-apps/src/samples/pjsua2_demo.cpp b/pjsip-apps/src/samples/pjsua2_demo.cpp new file mode 100644 index 00000000..65a6a45f --- /dev/null +++ b/pjsip-apps/src/samples/pjsua2_demo.cpp @@ -0,0 +1,290 @@ +/* $Id$ */ +/* + * Copyright (C) 2008-2013 Teluu Inc. (http://www.teluu.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +#include <pjsua2.hpp> +#include <iostream> +#include <memory> +#include <pj/file_access.h> + +using namespace pj; + +class MyAccount; + +class MyCall : public Call +{ +private: + MyAccount *myAcc; + +public: + MyCall(Account &acc, int call_id = PJSUA_INVALID_ID) + : Call(acc, call_id) + { + myAcc = (MyAccount *)&acc; + } + + virtual void onCallState(OnCallStateParam &prm); +}; + +class MyAccount : public Account +{ +public: + std::vector<Call *> calls; + +public: + MyAccount() + {} + + ~MyAccount() + { + std::cout << "*** Account is being deleted: No of calls=" + << calls.size() << std::endl; + } + + void removeCall(Call *call) + { + for (std::vector<Call *>::iterator it = calls.begin(); + it != calls.end(); ++it) + { + if (*it == call) { + calls.erase(it); + break; + } + } + } + + virtual void onRegState(OnRegStateParam &prm) + { + AccountInfo ai = getInfo(); + std::cout << (ai.regIsActive? "*** Register: code=" : "*** Unregister: code=") + << prm.code << std::endl; + } + + virtual void onIncomingCall(OnIncomingCallParam &iprm) + { + Call *call = new MyCall(*this, iprm.callId); + CallInfo ci = call->getInfo(); + CallOpParam prm; + + std::cout << "*** Incoming Call: " << ci.remoteUri << " [" + << ci.stateText << "]" << std::endl; + + calls.push_back(call); + prm.statusCode = (pjsip_status_code)200; + call->answer(prm); + } +}; + +void MyCall::onCallState(OnCallStateParam &prm) +{ + CallInfo ci = getInfo(); + std::cout << "*** Call: " << ci.remoteUri << " [" << ci.stateText + << "]" << std::endl; + + if (ci.state == PJSIP_INV_STATE_DISCONNECTED) { + myAcc->removeCall(this); + /* Delete the call */ + delete this; + } +} + +static void mainProg1() throw(Error) +{ + Endpoint ep; + + // Create library + ep.libCreate(); + + // Init library + EpConfig ep_cfg; + ep_cfg.logConfig.level = 4; + ep.libInit( ep_cfg ); + + // Transport + TransportConfig tcfg; + tcfg.port = 5060; + ep.transportCreate(PJSIP_TRANSPORT_UDP, tcfg); + + // Start library + ep.libStart(); + std::cout << "*** PJSUA2 STARTED ***" << std::endl; + + // Add account + AccountConfig acc_cfg; + acc_cfg.idUri = "sip:test1@pjsip.org"; + acc_cfg.regConfig.registrarUri = "sip:pjsip.org"; + acc_cfg.sipConfig.authCreds.push_back( AuthCredInfo("digest", "*", + "test1", 0, "test1") ); + std::auto_ptr<MyAccount> acc(new MyAccount); + acc->create(acc_cfg); + + pj_thread_sleep(2000); + + // Make outgoing call + Call *call = new MyCall(*acc); + acc->calls.push_back(call); + CallOpParam prm(true); + prm.opt.audioCount = 1; + prm.opt.videoCount = 0; + call->makeCall("sip:test1@pjsip.org", prm); + + // Hangup all calls + pj_thread_sleep(8000); + ep.hangupAllCalls(); + pj_thread_sleep(4000); + + // Destroy library + std::cout << "*** PJSUA2 SHUTTING DOWN ***" << std::endl; +} + +void mainProg2() throw(Error) +{ + Endpoint ep; + + // Create library + ep.libCreate(); + + string json_str; + + { + EpConfig epCfg; + JsonDocument jDoc; + + epCfg.uaConfig.maxCalls = 61; + epCfg.uaConfig.userAgent = "Just JSON Test"; + epCfg.uaConfig.stunServer.push_back("stun1.pjsip.org"); + epCfg.uaConfig.stunServer.push_back("stun2.pjsip.org"); + epCfg.logConfig.filename = "THE.LOG"; + + jDoc.writeObject(epCfg); + json_str = jDoc.saveString(); + std::cout << json_str << std::endl << std::endl; + } + + { + EpConfig epCfg; + JsonDocument rDoc; + string output; + + rDoc.loadString(json_str); + rDoc.readObject(epCfg); + + JsonDocument wDoc; + + wDoc.writeObject(epCfg); + json_str = wDoc.saveString(); + std::cout << json_str << std::endl << std::endl; + + wDoc.saveFile("jsontest.js"); + } + + { + EpConfig epCfg; + JsonDocument rDoc; + + rDoc.loadFile("jsontest.js"); + rDoc.readObject(epCfg); + pj_file_delete("jsontest.js"); + } + + ep.libDestroy(); +} + +void mainProg() throw(Error) +{ + Endpoint ep; + + // Create library + ep.libCreate(); + + string json_str; + + { + JsonDocument jdoc; + AccountConfig accCfg; + + accCfg.idUri = "\"Just Test\" <sip:test@pjsip.org>"; + accCfg.regConfig.registrarUri = "sip:pjsip.org"; + SipHeader h; + h.hName = "X-Header"; + h.hValue = "User header"; + accCfg.regConfig.headers.push_back(h); + + accCfg.sipConfig.proxies.push_back("<sip:sip.pjsip.org;transport=tcp>"); + accCfg.sipConfig.proxies.push_back("<sip:sip.pjsip.org;transport=tls>"); + + accCfg.mediaConfig.transportConfig.tlsConfig.ciphers.push_back(1); + accCfg.mediaConfig.transportConfig.tlsConfig.ciphers.push_back(2); + accCfg.mediaConfig.transportConfig.tlsConfig.ciphers.push_back(3); + + AuthCredInfo aci; + aci.scheme = "digest"; + aci.username = "test"; + aci.data = "passwd"; + aci.realm = "*"; + accCfg.sipConfig.authCreds.push_back(aci); + + jdoc.writeObject(accCfg); + json_str = jdoc.saveString(); + std::cout << "Original:" << std::endl; + std::cout << json_str << std::endl << std::endl; + } + + { + JsonDocument rdoc; + + rdoc.loadString(json_str); + AccountConfig accCfg; + rdoc.readObject(accCfg); + + JsonDocument wdoc; + wdoc.writeObject(accCfg); + json_str = wdoc.saveString(); + + std::cout << "Parsed:" << std::endl; + std::cout << json_str << std::endl << std::endl; + } + + ep.libDestroy(); +} + +int main() +{ + int ret = 0; + + /* Test endpoint instantiation and destruction without libCreate(), + * libInit() etc. + */ + { + Endpoint ep; + ep.natDetectType(); + { + } + } + + try { + mainProg1(); + std::cout << "Success" << std::endl; + } catch (Error & err) { + std::cout << "Exception: " << err.info() << std::endl; + ret = 1; + } + + return ret; +} + + diff --git a/pjsip-apps/src/swig/Makefile b/pjsip-apps/src/swig/Makefile new file mode 100644 index 00000000..815e0e18 --- /dev/null +++ b/pjsip-apps/src/swig/Makefile @@ -0,0 +1,32 @@ +include ../../../build.mak + +ifneq ($(findstring android,$(TARGET_NAME)),) + # no python for android + DIRS = java +else + DIRS = python java +endif + +export SWIG_FLAGS=-I../../../../pjlib/include \ + -I../../../../pjlib-util/include \ + -I../../../../pjmedia/include \ + -I../../../../pjsip/include \ + -I../../../../pjnath/include -c++ +export SRC_DIR=../../../../pjsip/include +export SRCS=$(SRC_DIR)/pjsua2/endpoint.hpp $(SRC_DIR)/pjsua2/types.hpp + +.PHONY: all clean dep depend distclean print realclean install uninstall + +all: symbols.i + +all clean dep depend distclean print realclean install uninstall: + for dir in $(DIRS); do \ + if $(MAKE) $(MAKE_FLAGS) -C $$dir $@; then \ + true; \ + else \ + exit 1; \ + fi; \ + done + +symbols.i: symbols.lst + python importsym.py diff --git a/pjsip-apps/src/swig/importsym.py b/pjsip-apps/src/swig/importsym.py new file mode 100644 index 00000000..32faf2cc --- /dev/null +++ b/pjsip-apps/src/swig/importsym.py @@ -0,0 +1,193 @@ +# $Id$ +# +# importsym.py: Import C symbol decls (structs, enums, etc) and write them +# to another file +# +# Copyright (C)2013 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import pycparser +from pycparser import c_generator +import sys +import os + +def which(program): + import os + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + if sys.platform == 'win32' and not program.endswith(".exe"): + program += ".exe" + + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + return None + +# +PJ_ROOT_PATH = "../../../" + +# CPP is needed by pycparser. +CPP_PATH = which("cpp") +if not CPP_PATH: + print 'Error: need to have cpp in PATH' + sys.exit(1) + +# Hardcoded! +if sys.platform == 'win32': + PYCPARSER_DIR="C:/devs/tools/pycparser" +elif sys.platform == "linux2": + PYCPARSER_DIR="/home/bennylp/Desktop/opt/src/pycparser-master" +else: + PYCPARSER_DIR="/Library/Python/2.7/site-packages/pycparser" + +if not os.path.exists(PYCPARSER_DIR + '/utils/fake_libc_include'): + print "Error: couldn't find pycparser utils in '%s'" % PYPARSER_DIR + sys.exit(1) + +# Heading, to be placed before the source files +C_HEADING_SECTION = """ +#define PJ_AUTOCONF 1 +#define jmp_buf int +#define __attribute__(x) +""" + +# CPP (C preprocessor) settings +CPP_CFLAGS = [ + '-I' + PYCPARSER_DIR + '/utils/fake_libc_include', + "-I" + PJ_ROOT_PATH + "pjlib/include", + "-I" + PJ_ROOT_PATH + "pjlib-util/include", + "-I" + PJ_ROOT_PATH + "pjnath/include", + "-I" + PJ_ROOT_PATH + "pjmedia/include", + "-I" + PJ_ROOT_PATH + "pjsip/include" + ] + + +class SymbolVisitor(pycparser.c_ast.NodeVisitor): + def __init__(self, names): + self.nodeDict = {} + for name in names: + self.nodeDict[name] = None + + def _add(self, node): + if self.nodeDict.has_key(node.name): + self.nodeDict[node.name] = node + + def visit_Struct(self, node): + self._add(node) + + def visit_Enum(self, node): + self._add(node) + + def visit_Typename(self, node): + self._add(node) + + def visit_Typedef(self, node): + self._add(node) + + +TEMP_FILE="tmpsrc.h" + +class SymbolImporter: + """ + Import C selected declarations from C source file and move it + to another file. + + Parameters: + - listfile Path of file containing list of C source file + and identifier names to be imported. The format + of the listfile is: + + filename name1 name2 name3 + + for example: + + pj/sock_qos.h pj_qos_type pj_qos_flag + pj/types.h pj_status_t PJ_SUCCESS + """ + def __init__(self): + pass + + def process(self, listfile, outfile): + + # Read listfile + f = open(listfile) + lines = f.readlines() + f.close() + + # Process each line in list file, while generating the + # temporary C file to be processed by pycparser + f = open(TEMP_FILE, "w") + f.write(C_HEADING_SECTION) + names = [] + fcnt = 0 + for line in lines: + spec = line.split() + if len(spec) < 2: + continue + fcnt += 1 + f.write("#include <%s>\n" % spec[0]) + names.extend(spec[1:]) + f.close() + print 'Parsing %d symbols from %d files..' % (len(names), fcnt) + + # Parse the temporary C file + ast = pycparser.parse_file(TEMP_FILE, use_cpp=True, cpp_path=CPP_PATH, cpp_args=CPP_CFLAGS) + os.remove(TEMP_FILE) + + # Filter the declarations that we wanted + print 'Filtering..' + visitor = SymbolVisitor(names) + visitor.visit(ast) + + # Print symbol declarations to outfile + print 'Writing declarations..' + f = open(outfile, 'w') + f.write("// This file is autogenerated by importsym script, do not modify!\n\n") + gen = pycparser.c_generator.CGenerator() + for name in names: + node = visitor.nodeDict[name] + if not node: + print " ** Warning: declaration for '%s' is not found **" % k + else: + print " writing '%s'.." % name + output = gen.visit(node) + ";\n\n" + f.write(output) + f.close() + print "Done." + + +if __name__ == "__main__": + print "Importing symbols: 'symbols.lst' --> 'symbols.i'" + si = SymbolImporter() + si.process("symbols.lst", "symbols.i") + try: + os.remove("lextab.py") + except OSError: + pass + try: + os.remove("yacctab.py") + except OSError: + pass + +
\ No newline at end of file diff --git a/pjsip-apps/src/swig/java/Makefile b/pjsip-apps/src/swig/java/Makefile new file mode 100644 index 00000000..1946891f --- /dev/null +++ b/pjsip-apps/src/swig/java/Makefile @@ -0,0 +1,124 @@ +include ../../../../build.mak + +ifneq ($(findstring android,$(TARGET_NAME)),) + OS=android +else + ifneq ($(findstring darwin,$(TARGET_NAME)),) + OS=darwin + endif +endif + +OUT_DIR=output +ifeq ($(OS),Windows_NT) + LIBPJSUA2_SO=$(OUT_DIR)/pjsua2.dll +else + ifeq ($(OS),darwin) + LIBPJSUA2_SO=$(OUT_DIR)/libpjsua2.jnilib + else + ifeq ($(OS),android) + LIBPJSUA2_SO=android/libs/armeabi/libpjsua2.so + else + LIBPJSUA2_SO=$(OUT_DIR)/libpjsua2.so + endif + endif +endif + +# Get JDK location +ifeq ("$(JAVA_HOME)","") + # Get javac location to determine JDK location + JAVAC_PATH = $(shell which javac) + ifeq ("$(JAVAC_PATH)","") + $(error Cannot determine JDK location using 'which' command. Please define JAVA_HOME envvar) + endif + + JAVAC_PATH := $(realpath $(JAVAC_PATH)) + JAVA_BIN := $(dir $(JAVAC_PATH)) + JAVA_HOME := $(patsubst %/bin/,%,$(JAVA_BIN)) +else + ifeq (exists, $(shell test -d $(JAVA_HOME)/bin && echo exists )) + JAVA_BIN := $(JAVA_HOME)/bin + else + JAVA_BIN := $(JAVA_HOME) + endif +endif + +# OS specific +ifeq ($(OS),Windows_NT) + MY_JNI_LDFLAGS = -L$(MY_JDK)/lib -Wl,--kill-at +else + MY_JNI_CFLAGS = -fPIC + MY_JNI_LDFLAGS = -L$(MY_JDK)/lib + ifeq ($(OS),darwin) + MY_JNI_LDFLAGS := $(MY_JNI_LDFLAGS) -Wl,-soname,pjsua2.so + endif + ifeq ($(OS),android) + MY_JNI_CFLAGS := $(MY_JNI_CFLAGS) -D__ANDROID__ + endif +endif + +# Env settings, e.g: path to SWIG, JDK, java(.exe), javac(.exe) +MY_SWIG = swig +MY_JDK = $(JAVA_HOME) +ifneq ($(findstring bin,$(JAVA_BIN)),) + MY_JAVA = $(MY_JDK)/bin/java + MY_JAVAC = $(MY_JDK)/bin/javac +else + MY_JAVA = $(MY_JDK)/java + MY_JAVAC = $(MY_JDK)/javac +endif +MY_JNI_CFLAGS := $(MY_JNI_CFLAGS) -I$(MY_JDK)/include -I$(MY_JDK)/include/win32 \ + -I$(MY_JDK)/include/linux -I. + +# Build settings +MY_CFLAGS = $(PJ_CFLAGS) $(MY_JNI_CFLAGS) +MY_LDFLAGS = $(PJ_LDFLAGS) -lpjsua2-$(TARGET_NAME) $(PJ_LDLIBS) $(MY_JNI_LDFLAGS) +MY_PACKAGE_NAME = org.pjsip.pjsua2 +ifeq ($(OS),android) + MY_PACKAGE_PATH = android/src/$(subst .,/,$(MY_PACKAGE_NAME)) +else + MY_PACKAGE_PATH = $(OUT_DIR)/$(subst .,/,$(MY_PACKAGE_NAME)) +endif + +MY_APP_JAVA = android/src/$(subst .,/,$(MY_PACKAGE_NAME))/app/MyApp.java + +.PHONY: all java install uninstall + +all: $(LIBPJSUA2_SO) java + +$(LIBPJSUA2_SO): $(OUT_DIR)/pjsua2_wrap.o + $(PJ_CXX) -shared -o $(LIBPJSUA2_SO) $(OUT_DIR)/pjsua2_wrap.o $(MY_CFLAGS) $(MY_LDFLAGS) + +$(OUT_DIR)/pjsua2_wrap.o: $(OUT_DIR)/pjsua2_wrap.cpp Makefile + $(PJ_CXX) -c $(OUT_DIR)/pjsua2_wrap.cpp -o $(OUT_DIR)/pjsua2_wrap.o $(MY_CFLAGS) $(MY_LDFLAGS) + +$(OUT_DIR)/pjsua2_wrap.cpp: ../pjsua2.i ../symbols.i $(SRCS) + mkdir -p $(MY_PACKAGE_PATH) + swig $(SWIG_FLAGS) -java -package $(MY_PACKAGE_NAME) -outdir $(MY_PACKAGE_PATH) -o $(OUT_DIR)/pjsua2_wrap.cpp ../pjsua2.i + +clean distclean realclean: + rm -rf $(LIBPJSUA2_SO) $(OUT_DIR)/* $(MY_PACKAGE_PATH)/*.java $(MY_PACKAGE_PATH)/*.class + +java: $(MY_PACKAGE_PATH)/Error.class $(MY_PACKAGE_PATH)/test.class $(MY_PACKAGE_PATH)/sample.class + +$(MY_PACKAGE_PATH)/Error.class: $(MY_PACKAGE_PATH)/Error.java + $(MY_JAVAC) -d $(OUT_DIR) $(MY_PACKAGE_PATH)/*.java $(MY_APP_JAVA) + +$(MY_PACKAGE_PATH)/test.class: test.java + $(MY_JAVAC) -d $(OUT_DIR) -classpath "$(OUT_DIR)" test.java + +$(MY_PACKAGE_PATH)/sample.class: sample.java + $(MY_JAVAC) -d $(OUT_DIR) -classpath "$(OUT_DIR)" sample.java + +test: + @# Need to specify classpath and library path, alternatively, they can be set via + @# CLASSPATH and java.library.path env settings + $(MY_JAVA) -cp "$(OUT_DIR)" -Djava.library.path="$(OUT_DIR)" test + +sample: + @# Need to specify classpath and library path, alternatively, they can be set via + @# CLASSPATH and java.library.path env settings + $(MY_JAVA) -cp "$(OUT_DIR)" -Djava.library.path="$(OUT_DIR)" org.pjsip.pjsua2.app.sample + +install: +uninstall: + diff --git a/pjsip-apps/src/swig/java/android/.classpath b/pjsip-apps/src/swig/java/android/.classpath new file mode 100644 index 00000000..b76ec6cd --- /dev/null +++ b/pjsip-apps/src/swig/java/android/.classpath @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
diff --git a/pjsip-apps/src/swig/java/android/.project b/pjsip-apps/src/swig/java/android/.project new file mode 100644 index 00000000..434a3408 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/.project @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>Pjsua2</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/pjsip-apps/src/swig/java/android/.settings/org.eclipse.jdt.core.prefs b/pjsip-apps/src/swig/java/android/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..48ab4c6b --- /dev/null +++ b/pjsip-apps/src/swig/java/android/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/pjsip-apps/src/swig/java/android/AndroidManifest.xml b/pjsip-apps/src/swig/java/android/AndroidManifest.xml new file mode 100644 index 00000000..5a9b0aef --- /dev/null +++ b/pjsip-apps/src/swig/java/android/AndroidManifest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.pjsip.pjsua2.app" + android:versionCode="1" + android:versionName="1.0" > + + <uses-sdk + android:minSdkVersion="11" + android:targetSdkVersion="15" /> + + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.RECORD_AUDIO" /> + <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> + <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" /> + <uses-permission android:name="android.permission.WRITE_SETTINGS" /> + <uses-permission android:name="android.permission.READ_PHONE_STATE" /> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> + <uses-permission android:name="android.permission.VIBRATE" /> + <uses-permission android:name="android.permission.READ_LOGS" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + + <application + android:allowBackup="true" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + android:theme="@style/AppTheme" > + <activity + android:name="org.pjsip.pjsua2.app.MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + <activity + android:name="org.pjsip.pjsua2.app.CallActivity" + android:label="@string/title_activity_call" > + </activity> + </application> + +</manifest> diff --git a/pjsip-apps/src/swig/java/android/ic_launcher-web.png b/pjsip-apps/src/swig/java/android/ic_launcher-web.png Binary files differnew file mode 100644 index 00000000..a18cbb48 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/ic_launcher-web.png diff --git a/pjsip-apps/src/swig/java/android/jni/Android.mk b/pjsip-apps/src/swig/java/android/jni/Android.mk new file mode 100644 index 00000000..94912baf --- /dev/null +++ b/pjsip-apps/src/swig/java/android/jni/Android.mk @@ -0,0 +1,12 @@ +include ../../../../../build.mak + +LOCAL_PATH := $(PJDIR)/pjsip-apps/src/swig/java/android +include $(CLEAR_VARS) + +LOCAL_MODULE := libpjsua2 +LOCAL_CFLAGS := $(APP_CFLAGS) -frtti -fexceptions +LOCAL_LDFLAGS := $(APP_LDFLAGS) +LOCAL_LDLIBS := $(APP_LDLIBS) +LOCAL_SRC_FILES := ../output/pjsua2_wrap.cpp + +include $(BUILD_SHARED_LIBRARY) diff --git a/pjsip-apps/src/swig/java/android/jni/Application.mk b/pjsip-apps/src/swig/java/android/jni/Application.mk new file mode 100644 index 00000000..87124dd8 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/jni/Application.mk @@ -0,0 +1 @@ +APP_STL := gnustl_static diff --git a/pjsip-apps/src/swig/java/android/proguard-project.txt b/pjsip-apps/src/swig/java/android/proguard-project.txt new file mode 100644 index 00000000..f2fe1559 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/pjsip-apps/src/swig/java/android/project.properties b/pjsip-apps/src/swig/java/android/project.properties new file mode 100644 index 00000000..0840b4a0 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-15 diff --git a/pjsip-apps/src/swig/java/android/res/drawable-hdpi/ic_launcher.png b/pjsip-apps/src/swig/java/android/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..288b6655 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/drawable-hdpi/ic_launcher.png diff --git a/pjsip-apps/src/swig/java/android/res/drawable-mdpi/ic_launcher.png b/pjsip-apps/src/swig/java/android/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..6ae570b4 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/drawable-mdpi/ic_launcher.png diff --git a/pjsip-apps/src/swig/java/android/res/drawable-xhdpi/ic_launcher.png b/pjsip-apps/src/swig/java/android/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..d4fb7cd9 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/drawable-xhdpi/ic_launcher.png diff --git a/pjsip-apps/src/swig/java/android/res/drawable-xxhdpi/ic_launcher.png b/pjsip-apps/src/swig/java/android/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..85a60815 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/drawable-xxhdpi/ic_launcher.png diff --git a/pjsip-apps/src/swig/java/android/res/drawable/bkg.xml b/pjsip-apps/src/swig/java/android/res/drawable/bkg.xml new file mode 100644 index 00000000..f5052332 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/drawable/bkg.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8" ?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true" android:drawable="@color/pressed_color" />
+ <item android:drawable="@color/default_color" />
+</selector>
\ No newline at end of file diff --git a/pjsip-apps/src/swig/java/android/res/layout/activity_call.xml b/pjsip-apps/src/swig/java/android/res/layout/activity_call.xml new file mode 100644 index 00000000..3745eb39 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/layout/activity_call.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/textViewPeer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:text="Peer URI"
+ android:textAppearance="?android:attr/textAppearanceLarge" />
+
+ <TextView
+ android:id="@+id/textViewCallState"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:text="Call state" />
+
+ <Button
+ android:id="@+id/buttonAccept"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="acceptCall"
+ android:text="Accept" />
+
+ <Button
+ android:id="@+id/buttonHangup"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:onClick="hangupCall"
+ android:text="Reject" />
+
+</LinearLayout>
\ No newline at end of file diff --git a/pjsip-apps/src/swig/java/android/res/layout/activity_main.xml b/pjsip-apps/src/swig/java/android/res/layout/activity_main.xml new file mode 100644 index 00000000..c63c0210 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/layout/activity_main.xml @@ -0,0 +1,65 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/activity_vertical_margin"
+ android:paddingLeft="@dimen/activity_horizontal_margin"
+ android:paddingRight="@dimen/activity_horizontal_margin"
+ android:paddingTop="@dimen/activity_vertical_margin"
+ tools:context=".MainActivity" >
+
+ <ListView
+ android:id="@+id/listViewBuddy"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:listSelector="@drawable/bkg" >
+ </ListView>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal" >
+
+ <ImageButton
+ android:id="@+id/buttonCall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="makeCall"
+ android:src="@android:drawable/ic_menu_call" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:text=" "/>
+
+ <ImageButton
+ android:id="@+id/buttonAddBuddy"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="addBuddy"
+ android:src="@android:drawable/ic_menu_add" />
+
+ <ImageButton
+ android:id="@+id/buttonEditBuddy"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="editBuddy"
+ android:src="@android:drawable/ic_menu_edit" />
+
+ <ImageButton
+ android:id="@+id/buttonDelBuddy"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="delBuddy"
+ android:src="@android:drawable/ic_menu_delete" />
+
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file diff --git a/pjsip-apps/src/swig/java/android/res/layout/dlg_account_config.xml b/pjsip-apps/src/swig/java/android/res/layout/dlg_account_config.xml new file mode 100644 index 00000000..71111f14 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/layout/dlg_account_config.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?>
+<TableLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:padding = "20dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/textViewInfo"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingBottom="20dp"
+ android:textColor="#b0b0b0" >
+ </TextView>
+
+ <TableRow>
+ <TextView android:text="ID">
+ </TextView>
+
+ <EditText
+ android:id="@+id/editTextId"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:inputType="textUri" >
+
+ <requestFocus />
+ </EditText>
+ </TableRow>
+ <TableRow>
+ <TextView android:text="Registrar">
+ </TextView>
+ <EditText
+ android:id="@+id/editTextRegistrar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:inputType="textUri" >
+ </EditText>
+ </TableRow>
+ <TableRow>
+ <TextView android:text="Proxy">
+ </TextView>
+ <EditText
+ android:id="@+id/editTextProxy"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:inputType="textUri" >
+ </EditText>
+ </TableRow>
+ <TableRow>
+ <TextView android:text="Username">
+ </TextView>
+
+ <EditText
+ android:id="@+id/editTextUsername"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:inputType="text" >
+
+ </EditText>
+ </TableRow>
+ <TableRow>
+ <TextView android:text="Password">
+ </TextView>
+
+ <EditText
+ android:id="@+id/editTextPassword"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:inputType="textPassword" >
+
+ </EditText>
+ </TableRow>
+</TableLayout>
diff --git a/pjsip-apps/src/swig/java/android/res/layout/dlg_add_buddy.xml b/pjsip-apps/src/swig/java/android/res/layout/dlg_add_buddy.xml new file mode 100644 index 00000000..f13e7005 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/layout/dlg_add_buddy.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?>
+<TableLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:padding = "20dp"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <TableRow>
+ <TextView android:text="Buddy URI">
+ </TextView>
+
+ <EditText
+ android:id="@+id/editTextUri"
+ android:layout_weight="1"
+ android:inputType="textUri" >
+ <requestFocus />
+ </EditText>
+ </TableRow>
+ <TableRow>
+ <CheckBox
+ android:id="@+id/checkBoxSubscribe"
+ android:layout_column="1"
+ android:text="Subscribe presence" />
+ </TableRow>
+</TableLayout>
diff --git a/pjsip-apps/src/swig/java/android/res/menu/call.xml b/pjsip-apps/src/swig/java/android/res/menu/call.xml new file mode 100644 index 00000000..d122a4b7 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/menu/call.xml @@ -0,0 +1,9 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_settings"
+ android:orderInCategory="100"
+ android:showAsAction="never"
+ android:title="@string/action_settings"/>
+
+</menu>
diff --git a/pjsip-apps/src/swig/java/android/res/menu/main.xml b/pjsip-apps/src/swig/java/android/res/menu/main.xml new file mode 100644 index 00000000..be94829a --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/menu/main.xml @@ -0,0 +1,14 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_acc_config"
+ android:icon="@android:drawable/ic_menu_manage"
+ android:showAsAction="ifRoom"
+ android:title="Account Config"/>
+ <item
+ android:id="@+id/action_quit"
+ android:icon="@android:drawable/ic_menu_close_clear_cancel"
+ android:showAsAction="ifRoom"
+ android:title="Quit"/>
+
+</menu>
diff --git a/pjsip-apps/src/swig/java/android/res/values-sw600dp/dimens.xml b/pjsip-apps/src/swig/java/android/res/values-sw600dp/dimens.xml new file mode 100644 index 00000000..c876987e --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/values-sw600dp/dimens.xml @@ -0,0 +1,8 @@ +<resources>
+
+ <!--
+ Customize dimensions originally defined in res/values/dimens.xml (such as + screen margins) for sw600dp devices (e.g. 7" tablets) here.
+ -->
+
+</resources>
diff --git a/pjsip-apps/src/swig/java/android/res/values-sw720dp-land/dimens.xml b/pjsip-apps/src/swig/java/android/res/values-sw720dp-land/dimens.xml new file mode 100644 index 00000000..0df30679 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/values-sw720dp-land/dimens.xml @@ -0,0 +1,9 @@ +<resources>
+
+ <!--
+ Customize dimensions originally defined in res/values/dimens.xml (such as + screen margins) for sw720dp devices (e.g. 10" tablets) in landscape here.
+ -->
+ <dimen name="activity_horizontal_margin">128dp</dimen>
+
+</resources>
diff --git a/pjsip-apps/src/swig/java/android/res/values-v11/styles.xml b/pjsip-apps/src/swig/java/android/res/values-v11/styles.xml new file mode 100644 index 00000000..e3ef53d9 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/values-v11/styles.xml @@ -0,0 +1,11 @@ +<resources>
+
+ <!--
+ Base application theme for API 11+. This theme completely replaces + AppBaseTheme from res/values/styles.xml on API 11+ devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Holo.Light">
+ <!-- API 11 theme customizations can go here. -->
+ </style>
+
+</resources>
diff --git a/pjsip-apps/src/swig/java/android/res/values-v14/styles.xml b/pjsip-apps/src/swig/java/android/res/values-v14/styles.xml new file mode 100644 index 00000000..94dd245c --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/values-v14/styles.xml @@ -0,0 +1,12 @@ +<resources>
+
+ <!--
+ Base application theme for API 14+. This theme completely replaces + AppBaseTheme from BOTH res/values/styles.xml and + res/values-v11/styles.xml on API 14+ devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
+ <!-- API 14 theme customizations can go here. -->
+ </style>
+
+</resources>
diff --git a/pjsip-apps/src/swig/java/android/res/values/colors.xml b/pjsip-apps/src/swig/java/android/res/values/colors.xml new file mode 100644 index 00000000..9ce61cf2 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/values/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="pressed_color">#B8F2F5</color>
+ <color name="default_color">#E8FEFF</color>
+</resources>
\ No newline at end of file diff --git a/pjsip-apps/src/swig/java/android/res/values/dimens.xml b/pjsip-apps/src/swig/java/android/res/values/dimens.xml new file mode 100644 index 00000000..2e0e2ae4 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/values/dimens.xml @@ -0,0 +1,7 @@ +<resources>
+
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="activity_horizontal_margin">16dp</dimen>
+ <dimen name="activity_vertical_margin">16dp</dimen>
+
+</resources>
diff --git a/pjsip-apps/src/swig/java/android/res/values/strings.xml b/pjsip-apps/src/swig/java/android/res/values/strings.xml new file mode 100644 index 00000000..2ee52b69 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/values/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="app_name">Pjsua2</string>
+ <string name="action_settings">Settings</string>
+ <string name="title_activity_call">Call</string>
+ <string name="hello_world">Hello world!</string>
+
+</resources>
diff --git a/pjsip-apps/src/swig/java/android/res/values/styles.xml b/pjsip-apps/src/swig/java/android/res/values/styles.xml new file mode 100644 index 00000000..4ea93266 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/res/values/styles.xml @@ -0,0 +1,20 @@ +<resources>
+
+ <!--
+ Base application theme, dependent on API level. This theme is replaced + by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
+ -->
+ <style name="AppBaseTheme" parent="android:Theme.Light">
+ <!--
+ Theme customizations available in newer API levels can go in + res/values-vXX/styles.xml, while customizations related to + backward-compatibility can go here.
+ -->
+ </style>
+
+ <!-- Application theme. -->
+ <style name="AppTheme" parent="AppBaseTheme">
+ <!-- All customizations that are NOT specific to a particular API-level can go here. -->
+ </style>
+
+</resources>
diff --git a/pjsip-apps/src/swig/java/android/src/org/pjsip/pjsua2/app/CallActivity.java b/pjsip-apps/src/swig/java/android/src/org/pjsip/pjsua2/app/CallActivity.java new file mode 100644 index 00000000..48e1bad3 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/src/org/pjsip/pjsua2/app/CallActivity.java @@ -0,0 +1,146 @@ +/* $Id$ */ +/* + * Copyright (C) 2013 Teluu Inc. (http://www.teluu.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package org.pjsip.pjsua2.app; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.app.Activity; + +import org.pjsip.pjsua2.*; + +public class CallActivity extends Activity implements Handler.Callback { + + public static Handler handler_; + + private final Handler handler = new Handler(this); + private static CallInfo lastCallInfo; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + handler_ = handler; + if (MainActivity.currentCall != null) { + try { + lastCallInfo = MainActivity.currentCall.getInfo(); + updateCallState(lastCallInfo); + } catch (Exception e) { + System.out.println(e); + } + } else { + updateCallState(lastCallInfo); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + handler_ = null; + } + + public void acceptCall(View view) { + CallOpParam prm = new CallOpParam(); + prm.setStatusCode(pjsip_status_code.PJSIP_SC_OK); + try { + MainActivity.currentCall.answer(prm); + } catch (Exception e) { + System.out.println(e); + } + + view.setVisibility(View.GONE); + } + + public void hangupCall(View view) { + handler_ = null; + finish(); + + if (MainActivity.currentCall != null) { + CallOpParam prm = new CallOpParam(); + prm.setStatusCode(pjsip_status_code.PJSIP_SC_DECLINE); + try { + MainActivity.currentCall.hangup(prm); + } catch (Exception e) { + System.out.println(e); + } + + MainActivity.currentCall = null; + } + } + + @Override + public boolean handleMessage(Message m) { + + if (m.what == MainActivity.MSG_TYPE.CALL_STATE) { + + lastCallInfo = (CallInfo) m.obj; + updateCallState(lastCallInfo); + + } else { + + /* Message not handled */ + return false; + + } + + return true; + } + + private void updateCallState(CallInfo ci) { + TextView tvPeer = (TextView) findViewById(R.id.textViewPeer); + TextView tvState = (TextView) findViewById(R.id.textViewCallState); + Button buttonHangup = (Button) findViewById(R.id.buttonHangup); + Button buttonAccept = (Button) findViewById(R.id.buttonAccept); + String call_state = ""; + + if (ci.getRole() == pjsip_role_e.PJSIP_ROLE_UAC) { + buttonAccept.setVisibility(View.GONE); + } + + if (ci.getState().swigValue() < pjsip_inv_state.PJSIP_INV_STATE_CONFIRMED.swigValue()) + { + if (ci.getRole() == pjsip_role_e.PJSIP_ROLE_UAS) { + call_state = "Incoming call.."; + /* Default button texts are already 'Accept' & 'Reject' */ + } else { + buttonHangup.setText("Cancel"); + call_state = ci.getStateText(); + } + } + else if (ci.getState().swigValue() >= pjsip_inv_state.PJSIP_INV_STATE_CONFIRMED.swigValue()) + { + buttonAccept.setVisibility(View.GONE); + call_state = ci.getStateText(); + if (ci.getState() == pjsip_inv_state.PJSIP_INV_STATE_CONFIRMED) { + buttonHangup.setText("Hangup"); + } else if (ci.getState() == pjsip_inv_state.PJSIP_INV_STATE_DISCONNECTED) { + buttonHangup.setText("OK"); + call_state = "Call disconnected: " + ci.getLastReason(); + MainActivity.currentCall = null; + } + } + + tvPeer.setText(ci.getRemoteUri()); + tvState.setText(call_state); + } +} diff --git a/pjsip-apps/src/swig/java/android/src/org/pjsip/pjsua2/app/MainActivity.java b/pjsip-apps/src/swig/java/android/src/org/pjsip/pjsua2/app/MainActivity.java new file mode 100644 index 00000000..34633f97 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/src/org/pjsip/pjsua2/app/MainActivity.java @@ -0,0 +1,497 @@ +/* $Id$ */ +/* + * Copyright (C) 2013 Teluu Inc. (http://www.teluu.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package org.pjsip.pjsua2.app; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.SimpleAdapter; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.pjsip.pjsua2.*; + +public class MainActivity extends Activity implements Handler.Callback, MyAppObserver { + public static MyApp app = null; + public static MyCall currentCall = null; + public static MyAccount account = null; + public static AccountConfig accCfg = null; + + private ListView buddyListView; + private SimpleAdapter buddyListAdapter; + private int buddyListSelectedIdx = -1; + ArrayList<Map<String, String>> buddyList; + private String lastRegStatus = ""; + + private final Handler handler = new Handler(this); + public class MSG_TYPE { + public final static int INCOMING_CALL = 1; + public final static int CALL_STATE = 2; + public final static int REG_STATE = 3; + public final static int BUDDY_STATE = 4; + } + + private HashMap<String, String> putData(String uri, String status) { + HashMap<String, String> item = new HashMap<String, String>(); + item.put("uri", uri); + item.put("status", status); + return item; + } + + private void showCallActivity() { + Intent intent = new Intent(this, CallActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + if (app == null) { + app = new MyApp(); + /* Wait for GDB to init */ + if ((getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { + try { + Thread.sleep(5000); + } catch (InterruptedException e) {} + } + + app.init(this, getFilesDir().getAbsolutePath()); + } + + if (app.accList.size() == 0) { + accCfg = new AccountConfig(); + accCfg.setIdUri("sip:localhost"); + account = app.addAcc(accCfg); + } else { + account = app.accList.get(0); + accCfg = account.cfg; + } + + buddyList = new ArrayList<Map<String, String>>(); + for (int i = 0; i < account.buddyList.size(); i++) { + buddyList.add(putData(account.buddyList.get(i).cfg.getUri(), + account.buddyList.get(i).getStatusText())); + } + + String[] from = { "uri", "status" }; + int[] to = { android.R.id.text1, android.R.id.text2 }; + buddyListAdapter = new SimpleAdapter(this, buddyList, android.R.layout.simple_list_item_2, from, to); + + buddyListView = (ListView) findViewById(R.id.listViewBuddy);; + buddyListView.setAdapter(buddyListAdapter); + buddyListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, final View view, + int position, long id) + { + view.setSelected(true); + buddyListSelectedIdx = position; + } + }); + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_acc_config: + dlgAccountSetting(); + break; + + case R.id.action_quit: + Message m = Message.obtain(handler, 0); + m.sendToTarget(); + break; + + default: + break; + } + + return true; + } + + @Override + public boolean handleMessage(Message m) { + + if (m.what == 0) { + + app.deinit(); + finish(); + Runtime.getRuntime().gc(); + android.os.Process.killProcess(android.os.Process.myPid()); + + } else if (m.what == MSG_TYPE.CALL_STATE) { + + CallInfo ci = (CallInfo) m.obj; + + /* Forward the message to CallActivity */ + if (CallActivity.handler_ != null) { + Message m2 = Message.obtain(CallActivity.handler_, MSG_TYPE.CALL_STATE, ci); + m2.sendToTarget(); + } + + if (ci.getState() == pjsip_inv_state.PJSIP_INV_STATE_DISCONNECTED) + currentCall = null; + + } else if (m.what == MSG_TYPE.BUDDY_STATE) { + + MyBuddy buddy = (MyBuddy) m.obj; + int idx = account.buddyList.indexOf(buddy); + if (idx >= 0) { + buddyList.get(idx).put("status", buddy.getStatusText()); + buddyListAdapter.notifyDataSetChanged(); + // TODO: selection color/mark is gone after this, + // dont know how to return it back. + //buddyListView.setSelection(buddyListSelectedIdx); + //buddyListView.performItemClick(buddyListView, buddyListSelectedIdx, + // buddyListView.getItemIdAtPosition(buddyListSelectedIdx)); + + /* Return back Call activity */ + notifyCallState(currentCall); + } + + } else if (m.what == MSG_TYPE.REG_STATE) { + + String msg_str = (String) m.obj; + lastRegStatus = msg_str; + + } else if (m.what == MSG_TYPE.INCOMING_CALL) { + + /* Incoming call */ + final MyCall call = (MyCall) m.obj; + CallOpParam prm = new CallOpParam(); + + /* Only one call at anytime */ + if (currentCall != null) { + prm.setStatusCode(pjsip_status_code.PJSIP_SC_BUSY_HERE); + try { + call.hangup(prm); + } catch (Exception e) {} + return true; + } + + /* Answer with ringing */ + prm.setStatusCode(pjsip_status_code.PJSIP_SC_RINGING); + try { + call.answer(prm); + } catch (Exception e) {} + + currentCall = call; + showCallActivity(); + + } else { + + /* Message not handled */ + return false; + + } + + return true; + } + + + private void dlgAccountSetting() { + + LayoutInflater li = LayoutInflater.from(this); + View view = li.inflate(R.layout.dlg_account_config, null); + + if (!lastRegStatus.isEmpty()) { + TextView tvInfo = (TextView)view.findViewById(R.id.textViewInfo); + tvInfo.setText("Last status: " + lastRegStatus); + } + + AlertDialog.Builder adb = new AlertDialog.Builder(this); + adb.setView(view); + adb.setTitle("Account Settings"); + + final EditText etId = (EditText)view.findViewById(R.id.editTextId); + final EditText etReg = (EditText)view.findViewById(R.id.editTextRegistrar); + final EditText etProxy = (EditText)view.findViewById(R.id.editTextProxy); + final EditText etUser = (EditText)view.findViewById(R.id.editTextUsername); + final EditText etPass = (EditText)view.findViewById(R.id.editTextPassword); + + etId. setText(accCfg.getIdUri()); + etReg. setText(accCfg.getRegConfig().getRegistrarUri()); + StringVector proxies = accCfg.getSipConfig().getProxies(); + if (proxies.size() > 0) + etProxy.setText(proxies.get(0)); + else + etProxy.setText(""); + AuthCredInfoVector creds = accCfg.getSipConfig().getAuthCreds(); + if (creds.size() > 0) { + etUser. setText(creds.get(0).getUsername()); + etPass. setText(creds.get(0).getData()); + } else { + etUser. setText(""); + etPass. setText(""); + } + + adb.setCancelable(false); + adb.setPositiveButton("OK", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog,int id) { + String acc_id = etId.getText().toString(); + String registrar = etReg.getText().toString(); + String proxy = etProxy.getText().toString(); + String username = etUser.getText().toString(); + String password = etPass.getText().toString(); + + accCfg.setIdUri(acc_id); + accCfg.getRegConfig().setRegistrarUri(registrar); + AuthCredInfoVector creds = accCfg.getSipConfig().getAuthCreds(); + creds.clear(); + if (!username.isEmpty()) { + creds.add(new AuthCredInfo("Digest", "*", username, 0, password)); + } + StringVector proxies = accCfg.getSipConfig().getProxies(); + proxies.clear(); + if (!proxy.isEmpty()) { + proxies.add(proxy); + } + + /* Finally */ + lastRegStatus = ""; + try { + account.modify(accCfg); + } catch (Exception e) {} + } + }); + adb.setNegativeButton("Cancel", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog,int id) { + dialog.cancel(); + } + }); + + AlertDialog ad = adb.create(); + ad.show(); + } + + + public void makeCall(View view) { + if (buddyListSelectedIdx == -1) + return; + + /* Only one call at anytime */ + if (currentCall != null) { + return; + } + + HashMap<String, String> item = (HashMap<String, String>) buddyListView.getItemAtPosition(buddyListSelectedIdx); + String buddy_uri = item.get("uri"); + + MyCall call = new MyCall(account, -1); + CallOpParam prm = new CallOpParam(); + CallSetting opt = prm.getOpt(); + opt.setAudioCount(1); + opt.setVideoCount(0); + + try { + call.makeCall(buddy_uri, prm); + } catch (Exception e) { + currentCall = null; + return; + } + + currentCall = call; + showCallActivity(); + } + + private void dlgAddEditBuddy(BuddyConfig initial) { + final BuddyConfig cfg = new BuddyConfig(); + final BuddyConfig old_cfg = initial; + final boolean is_add = initial == null; + + LayoutInflater li = LayoutInflater.from(this); + View view = li.inflate(R.layout.dlg_add_buddy, null); + + AlertDialog.Builder adb = new AlertDialog.Builder(this); + adb.setView(view); + + final EditText etUri = (EditText)view.findViewById(R.id.editTextUri); + final CheckBox cbSubs = (CheckBox)view.findViewById(R.id.checkBoxSubscribe); + + if (is_add) { + adb.setTitle("Add Buddy"); + } else { + adb.setTitle("Edit Buddy"); + etUri. setText(initial.getUri()); + cbSubs.setChecked(initial.getSubscribe()); + } + + adb.setCancelable(false); + adb.setPositiveButton("OK", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog,int id) { + cfg.setUri(etUri.getText().toString()); + cfg.setSubscribe(cbSubs.isChecked()); + + if (is_add) { + account.addBuddy(cfg); + buddyList.add(putData(cfg.getUri(), "")); + buddyListAdapter.notifyDataSetChanged(); + buddyListSelectedIdx = -1; + } else { + if (!old_cfg.getUri().equals(cfg.getUri())) { + account.delBuddy(buddyListSelectedIdx); + account.addBuddy(cfg); + buddyList.remove(buddyListSelectedIdx); + buddyList.add(putData(cfg.getUri(), "")); + buddyListAdapter.notifyDataSetChanged(); + buddyListSelectedIdx = -1; + } else if (old_cfg.getSubscribe() != cfg.getSubscribe()) { + MyBuddy bud = account.buddyList.get(buddyListSelectedIdx); + try { + bud.subscribePresence(cfg.getSubscribe()); + } catch (Exception e) {} + } + } + } + }); + adb.setNegativeButton("Cancel", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog,int id) { + dialog.cancel(); + } + }); + + AlertDialog ad = adb.create(); + ad.show(); + } + + public void addBuddy(View view) { + dlgAddEditBuddy(null); + } + + public void editBuddy(View view) { + if (buddyListSelectedIdx == -1) + return; + + BuddyConfig old_cfg = account.buddyList.get(buddyListSelectedIdx).cfg; + dlgAddEditBuddy(old_cfg); + } + + public void delBuddy(View view) { + if (buddyListSelectedIdx == -1) + return; + + final HashMap<String, String> item = (HashMap<String, String>) buddyListView.getItemAtPosition(buddyListSelectedIdx); + String buddy_uri = item.get("uri"); + + DialogInterface.OnClickListener ocl = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + account.delBuddy(buddyListSelectedIdx); + buddyList.remove(item); + buddyListAdapter.notifyDataSetChanged(); + buddyListSelectedIdx = -1; + break; + case DialogInterface.BUTTON_NEGATIVE: + break; + } + } + }; + + AlertDialog.Builder adb = new AlertDialog.Builder(this); + adb.setTitle(buddy_uri); + adb.setMessage("\nDelete this buddy?\n"); + adb.setPositiveButton("Yes", ocl); + adb.setNegativeButton("No", ocl); + adb.show(); + } + + + /* + * === MyAppObserver === + * + * As we cannot do UI from worker thread, the callbacks mostly just send + * a message to UI/main thread. + */ + + public void notifyIncomingCall(MyCall call) { + Message m = Message.obtain(handler, MSG_TYPE.INCOMING_CALL, call); + m.sendToTarget(); + } + + public void notifyRegState(pjsip_status_code code, String reason, int expiration) { + String msg_str = ""; + if (expiration == 0) + msg_str += "Unregistration"; + else + msg_str += "Registration"; + + if (code.swigValue()/100 == 2) + msg_str += " successful"; + else + msg_str += " failed: " + reason; + + Message m = Message.obtain(handler, MSG_TYPE.REG_STATE, msg_str); + m.sendToTarget(); + } + + public void notifyCallState(MyCall call) { + if (currentCall == null || call.getId() != currentCall.getId()) + return; + + CallInfo ci; + try { + ci = call.getInfo(); + } catch (Exception e) { + ci = null; + } + Message m = Message.obtain(handler, MSG_TYPE.CALL_STATE, ci); + m.sendToTarget(); + } + + public void notifyBuddyState(MyBuddy buddy) { + Message m = Message.obtain(handler, MSG_TYPE.BUDDY_STATE, buddy); + m.sendToTarget(); + } + + /* === end of MyAppObserver ==== */ + +} diff --git a/pjsip-apps/src/swig/java/android/src/org/pjsip/pjsua2/app/MyApp.java b/pjsip-apps/src/swig/java/android/src/org/pjsip/pjsua2/app/MyApp.java new file mode 100644 index 00000000..7d7ab5d4 --- /dev/null +++ b/pjsip-apps/src/swig/java/android/src/org/pjsip/pjsua2/app/MyApp.java @@ -0,0 +1,449 @@ +/* $Id$ */ +/* + * Copyright (C) 2013 Teluu Inc. (http://www.teluu.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package org.pjsip.pjsua2.app; + +import java.io.File; +import java.util.ArrayList; +import org.pjsip.pjsua2.*; + + +/* Interface to separate UI & engine a bit better */ +interface MyAppObserver { + abstract void notifyRegState(pjsip_status_code code, String reason, int expiration); + abstract void notifyIncomingCall(MyCall call); + abstract void notifyCallState(MyCall call); + abstract void notifyBuddyState(MyBuddy buddy); +} + + +class MyLogWriter extends LogWriter { + @Override + public void write(LogEntry entry) { + System.out.println(entry.getMsg()); + } +} + + +class MyCall extends Call { + MyCall(MyAccount acc, int call_id) { + super(acc, call_id); + } + + @Override + public void onCallState(OnCallStateParam prm) { + MyApp.observer.notifyCallState(this); + } + + @Override + public void onCallMediaState(OnCallMediaStateParam prm) { + CallInfo ci; + try { + ci = getInfo(); + } catch (Exception e) { + return; + } + + CallMediaInfoVector cmiv = ci.getMedia(); + + for (int i = 0; i < cmiv.size(); i++) { + CallMediaInfo cmi = cmiv.get(i); + if (cmi.getType() == pjmedia_type.PJMEDIA_TYPE_AUDIO && + (cmi.getStatus() == pjsua_call_media_status.PJSUA_CALL_MEDIA_ACTIVE || + cmi.getStatus() == pjsua_call_media_status.PJSUA_CALL_MEDIA_REMOTE_HOLD)) + { + // unfortunately, on Java too, the returned Media cannot be downcasted to AudioMedia + Media m = getMedia(i); + AudioMedia am = AudioMedia.typecastFromMedia(m); + + // connect ports + try { + MyApp.ep.audDevManager().getCaptureDevMedia().startTransmit(am); + am.startTransmit(MyApp.ep.audDevManager().getPlaybackDevMedia()); + } catch (Exception e) { + continue; + } + } + } + } +} + + +class MyAccount extends Account { + public ArrayList<MyBuddy> buddyList = new ArrayList<MyBuddy>(); + public AccountConfig cfg; + + MyAccount(AccountConfig config) { + super(); + cfg = config; + } + + public MyBuddy addBuddy(BuddyConfig bud_cfg) + { + /* Create Buddy */ + MyBuddy bud = new MyBuddy(bud_cfg); + try { + bud.create(this, bud_cfg); + } catch (Exception e) { + bud = null; + } + + if (bud != null) { + buddyList.add(bud); + if (bud_cfg.getSubscribe()) + try { + bud.subscribePresence(true); + } catch (Exception e) {} + } + + return bud; + } + + public void delBuddy(MyBuddy buddy) { + buddyList.remove(buddy); + } + + public void delBuddy(int index) { + buddyList.remove(index); + } + + @Override + public void onRegState(OnRegStateParam prm) { + MyApp.observer.notifyRegState(prm.getCode(), prm.getReason(), prm.getExpiration()); + } + + @Override + public void onIncomingCall(OnIncomingCallParam prm) { + System.out.println("======== Incoming call ======== "); + MyCall call = new MyCall(this, prm.getCallId()); + MyApp.observer.notifyIncomingCall(call); + } + + @Override + public void onInstantMessage(OnInstantMessageParam prm) { + System.out.println("======== Incoming pager ======== "); + System.out.println("From : " + prm.getFromUri()); + System.out.println("To : " + prm.getToUri()); + System.out.println("Contact : " + prm.getContactUri()); + System.out.println("Mimetype : " + prm.getContentType()); + System.out.println("Body : " + prm.getMsgBody()); + } +} + + +class MyBuddy extends Buddy { + public BuddyConfig cfg; + + MyBuddy(BuddyConfig config) { + super(); + cfg = config; + } + + String getStatusText() { + BuddyInfo bi; + + try { + bi = getInfo(); + } catch (Exception e) { + return "?"; + } + + String status = ""; + if (bi.getSubState() == pjsip_evsub_state.PJSIP_EVSUB_STATE_ACTIVE) { + if (bi.getPresStatus().getStatus() == pjsua_buddy_status.PJSUA_BUDDY_STATUS_ONLINE) { + status = bi.getPresStatus().getStatusText(); + if (status == null || status.isEmpty()) { + status = "Online"; + } + } else if (bi.getPresStatus().getStatus() == pjsua_buddy_status.PJSUA_BUDDY_STATUS_OFFLINE) { + status = "Offline"; + } else { + status = "Unknown"; + } + } + return status; + } + + @Override + public void onBuddyState() { + MyApp.observer.notifyBuddyState(this); + } + +} + + +class MyAccountConfig { + public AccountConfig accCfg = new AccountConfig(); + public ArrayList<BuddyConfig> buddyCfgs = new ArrayList<BuddyConfig>(); + + public void readObject(ContainerNode node) { + try { + ContainerNode acc_node = node.readContainer("Account"); + accCfg.readObject(acc_node); + ContainerNode buddies_node = acc_node.readArray("buddies"); + buddyCfgs.clear(); + while (buddies_node.hasUnread()) { + BuddyConfig bud_cfg = new BuddyConfig(); + bud_cfg.readObject(buddies_node); + buddyCfgs.add(bud_cfg); + } + } catch (Exception e) {} + } + + public void writeObject(ContainerNode node) { + try { + ContainerNode acc_node = node.writeNewContainer("Account"); + accCfg.writeObject(acc_node); + ContainerNode buddies_node = acc_node.writeNewArray("buddies"); + for (int j = 0; j < buddyCfgs.size(); j++) { + buddyCfgs.get(j).writeObject(buddies_node); + } + } catch (Exception e) {} + } +} + + +class MyApp { + static { + System.loadLibrary("pjsua2"); + System.out.println("Library loaded"); + } + + public static Endpoint ep = new Endpoint(); + public static MyAppObserver observer; + public ArrayList<MyAccount> accList = new ArrayList<MyAccount>(); + + private ArrayList<MyAccountConfig> accCfgs = new ArrayList<MyAccountConfig>(); + private EpConfig epConfig = new EpConfig(); + private TransportConfig sipTpConfig = new TransportConfig(); + private String appDir; + + /* Maintain reference to log writer to avoid premature cleanup by GC */ + private MyLogWriter logWriter; + + private final String configName = "pjsua2.json"; + private final int SIP_PORT = 6000; + private final int LOG_LEVEL = 4; + + public void init(MyAppObserver obs, String app_dir) { + init(obs, app_dir, false); + } + + public void init(MyAppObserver obs, String app_dir, boolean own_worker_thread) { + observer = obs; + appDir = app_dir; + + /* Create endpoint */ + try { + ep.libCreate(); + } catch (Exception e) { + return; + } + + + /* Load config */ + String configPath = appDir + "/" + configName; + File f = new File(configPath); + if (f.exists()) { + loadConfig(configPath); + } else { + /* Set 'default' values */ + sipTpConfig.setPort(SIP_PORT); + } + + /* Override log level setting */ + epConfig.getLogConfig().setLevel(LOG_LEVEL); + epConfig.getLogConfig().setConsoleLevel(LOG_LEVEL); + + /* Set log config. */ + LogConfig log_cfg = epConfig.getLogConfig(); + logWriter = new MyLogWriter(); + log_cfg.setWriter(logWriter); + log_cfg.setDecor(log_cfg.getDecor() & + ~(pj_log_decoration.PJ_LOG_HAS_CR.swigValue() | + pj_log_decoration.PJ_LOG_HAS_NEWLINE.swigValue())); + + /* Set ua config. */ + UaConfig ua_cfg = epConfig.getUaConfig(); + ua_cfg.setUserAgent("Pjsua2And" + ep.libVersion().getFull()); + if (own_worker_thread) { + ua_cfg.setThreadCnt(0); + ua_cfg.setMainThreadOnly(true); + } + + /* Init endpoint */ + try { + ep.libInit(epConfig); + } catch (Exception e) { + return; + } + + /* Create transports. */ + try { + ep.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_UDP, sipTpConfig); + } catch (Exception e) { + System.out.println(e); + } + + try { + ep.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_TCP, sipTpConfig); + } catch (Exception e) { + System.out.println(e); + } + + /* Create accounts. */ + for (int i = 0; i < accCfgs.size(); i++) { + MyAccountConfig my_cfg = accCfgs.get(i); + MyAccount acc = addAcc(my_cfg.accCfg); + if (acc == null) + continue; + + /* Add Buddies */ + for (int j = 0; j < my_cfg.buddyCfgs.size(); j++) { + BuddyConfig bud_cfg = my_cfg.buddyCfgs.get(j); + acc.addBuddy(bud_cfg); + } + } + + /* Start. */ + try { + ep.libStart(); + } catch (Exception e) { + return; + } + } + + public MyAccount addAcc(AccountConfig cfg) { + MyAccount acc = new MyAccount(cfg); + try { + acc.create(cfg); + } catch (Exception e) { + acc = null; + return null; + } + + accList.add(acc); + return acc; + } + + public void delAcc(MyAccount acc) { + accList.remove(acc); + } + + private void loadConfig(String filename) { + JsonDocument json = new JsonDocument(); + + try { + /* Load file */ + json.loadFile(filename); + ContainerNode root = json.getRootContainer(); + + /* Read endpoint config */ + epConfig.readObject(root); + + /* Read transport config */ + ContainerNode tp_node = root.readContainer("SipTransport"); + sipTpConfig.readObject(tp_node); + + /* Read account configs */ + accCfgs.clear(); + ContainerNode accs_node = root.readArray("accounts"); + while (accs_node.hasUnread()) { + MyAccountConfig acc_cfg = new MyAccountConfig(); + acc_cfg.readObject(accs_node); + accCfgs.add(acc_cfg); + } + } catch (Exception e) { + System.out.println(e); + } + + /* Force delete json now, as I found that Java somehow destroys it + * after lib has been destroyed and from non-registered thread. + */ + json.delete(); + } + + private void buildAccConfigs() { + /* Sync accCfgs from accList */ + accCfgs.clear(); + for (int i = 0; i < accList.size(); i++) { + MyAccount acc = accList.get(i); + MyAccountConfig my_acc_cfg = new MyAccountConfig(); + my_acc_cfg.accCfg = acc.cfg; + + my_acc_cfg.buddyCfgs.clear(); + for (int j = 0; j < acc.buddyList.size(); j++) { + MyBuddy bud = acc.buddyList.get(j); + my_acc_cfg.buddyCfgs.add(bud.cfg); + } + + accCfgs.add(my_acc_cfg); + } + } + + private void saveConfig(String filename) { + JsonDocument json = new JsonDocument(); + + try { + /* Write endpoint config */ + json.writeObject(epConfig); + + /* Write transport config */ + ContainerNode tp_node = json.writeNewContainer("SipTransport"); + sipTpConfig.writeObject(tp_node); + + /* Write account configs */ + buildAccConfigs(); + ContainerNode accs_node = json.writeNewArray("accounts"); + for (int i = 0; i < accCfgs.size(); i++) { + accCfgs.get(i).writeObject(accs_node); + } + + /* Save file */ + json.saveFile(filename); + } catch (Exception e) {} + + /* Force delete json now, as I found that Java somehow destroys it + * after lib has been destroyed and from non-registered thread. + */ + json.delete(); + } + + public void deinit() { + String configPath = appDir + "/" + configName; + saveConfig(configPath); + + /* Try force GC to avoid late destroy of PJ objects as they should be + * deleted before lib is destroyed. + */ + Runtime.getRuntime().gc(); + + /* Shutdown pjsua. Note that Endpoint destructor will also invoke + * libDestroy(), so this will be a test of double libDestroy(). + */ + try { + ep.libDestroy(); + } catch (Exception e) {} + + /* Force delete Endpoint here, to avoid deletion from a non- + * registered thread (by GC?). + */ + ep.delete(); + ep = null; + } +} diff --git a/pjsip-apps/src/swig/java/sample.java b/pjsip-apps/src/swig/java/sample.java new file mode 100644 index 00000000..34d06b82 --- /dev/null +++ b/pjsip-apps/src/swig/java/sample.java @@ -0,0 +1,140 @@ +/* $Id$ */ +/* + * Copyright (C) 2013 Teluu Inc. (http://www.teluu.com) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +package org.pjsip.pjsua2.app; + +import java.io.IOException; +import org.pjsip.pjsua2.*; +import org.pjsip.pjsua2.app.*; + +class MyObserver implements MyAppObserver { + private static MyCall currentCall = null; + + @Override + public void notifyRegState(pjsip_status_code code, String reason, int expiration) {} + + @Override + public void notifyIncomingCall(MyCall call) { + /* Auto answer. */ + CallOpParam call_param = new CallOpParam(); + call_param.setStatusCode(pjsip_status_code.PJSIP_SC_OK); + try { + currentCall = call; + currentCall.answer(call_param); + } catch (Exception e) { + System.out.println(e); + return; + } + } + + @Override + public void notifyCallState(MyCall call) { + if (currentCall == null || call.getId() != currentCall.getId()) + return; + + CallInfo ci; + try { + ci = call.getInfo(); + } catch (Exception e) { + ci = null; + } + if (ci.getState() == pjsip_inv_state.PJSIP_INV_STATE_DISCONNECTED) + currentCall = null; + } + + @Override + public void notifyBuddyState(MyBuddy buddy) {} +} + +class MyShutdownHook extends Thread { + Thread thread; + MyShutdownHook(Thread thr) { + thread = thr; + } + public void run() { + thread.interrupt(); + try { + thread.join(); + } catch (Exception e) { + ; + } + } +} + +public class sample { + private static MyApp app = new MyApp(); + private static MyAppObserver observer = new MyObserver(); + private static MyAccount account = null; + private static AccountConfig accCfg = null; + + private static void runWorker() { + try { + app.init(observer, ".", true); + } catch (Exception e) { + System.out.println(e); + app.deinit(); + System.exit(-1); + } + + if (app.accList.size() == 0) { + accCfg = new AccountConfig(); + accCfg.setIdUri("sip:localhost"); + account = app.addAcc(accCfg); + + accCfg.setIdUri("sip:301@pjsip.org"); + AccountSipConfig sipCfg = accCfg.getSipConfig(); + AuthCredInfoVector ciVec = sipCfg.getAuthCreds(); + ciVec.add(new AuthCredInfo("Digest", + "*", + "301", + 0, + "pw301")); + + StringVector proxy = sipCfg.getProxies(); + proxy.add("sip:pjsip.org;transport=tcp"); + + AccountRegConfig regCfg = accCfg.getRegConfig(); + regCfg.setRegistrarUri("sip:pjsip.org"); + account = app.addAcc(accCfg); + } else { + account = app.accList.get(0); + accCfg = account.cfg; + } + + try { + account.modify(accCfg); + } catch (Exception e) {} + + while (!Thread.currentThread().isInterrupted()) { + MyApp.ep.libHandleEvents(10); + try { + Thread.currentThread().sleep(50); + } catch (InterruptedException ie) { + break; + } + } + app.deinit(); + } + + public static void main(String argv[]) { + Runtime.getRuntime().addShutdownHook(new MyShutdownHook(Thread.currentThread())); + + runWorker(); + } +} diff --git a/pjsip-apps/src/swig/java/test.java b/pjsip-apps/src/swig/java/test.java new file mode 100644 index 00000000..f616460d --- /dev/null +++ b/pjsip-apps/src/swig/java/test.java @@ -0,0 +1,17 @@ +import org.pjsip.pjsua2.*; + +public class test { + static { + System.loadLibrary("pjsua2"); + System.out.println("Library loaded"); + } + + public static void main(String argv[]) { + + AuthCredInfo cred = new AuthCredInfo(); + + cred.setRealm("Hello world"); + + System.out.println(cred.getRealm()); + } +} diff --git a/pjsip-apps/src/swig/pjsua2.i b/pjsip-apps/src/swig/pjsua2.i new file mode 100644 index 00000000..2cc41d4a --- /dev/null +++ b/pjsip-apps/src/swig/pjsua2.i @@ -0,0 +1,111 @@ +%module(directors="1") pjsua2 + +// +// Suppress few warnings +// +#pragma SWIG nowarn=312 // 312: nested struct (in types.h, sip_auth.h) + +// +// Header section +// +%{ +#include "pjsua2.hpp" +using namespace std; +using namespace pj; +%} + +#ifdef SWIGPYTHON + %feature("director:except") { + if( $error != NULL ) { + PyObject *ptype, *pvalue, *ptraceback; + PyErr_Fetch( &ptype, &pvalue, &ptraceback ); + PyErr_Restore( ptype, pvalue, ptraceback ); + PyErr_Print(); + //Py_Exit(1); + } + } +#endif + +// Allow C++ exceptions to be handled in Java +#ifdef SWIGJAVA + %typemap(throws, throws="java.lang.Exception") pj::Error { + jclass excep = jenv->FindClass("java/lang/Exception"); + if (excep) + jenv->ThrowNew(excep, $1.info(true).c_str()); + return $null; +} + + // Force the Error Java class to extend java.lang.Exception + %typemap(javabase) pj::Error "java.lang.Exception"; + + // Override getMessage() + %typemap(javacode) pj::Error %{ + public String getMessage() { + return getTitle(); + } +%} +#endif + + +// Constants from PJSIP libraries +%include "symbols.i" + + +// +// Classes that can be extended in the target language +// +%feature("director") LogWriter; +%feature("director") Endpoint; +%feature("director") Account; +%feature("director") Call; +%feature("director") Buddy; +%feature("director") FindBuddyMatch; + +// +// STL stuff. +// +%include "std_string.i" +%include "std_vector.i" + +%template(StringVector) std::vector<std::string>; +%template(IntVector) std::vector<int>; + +// +// Ignore stuffs in pjsua2 +// +%ignore fromPj; +%ignore toPj; + +// +// Now include the API itself. +// +%include "pjsua2/types.hpp" + +%ignore pj::ContainerNode::op; +%ignore pj::ContainerNode::data; +%ignore container_node_op; +%ignore container_node_internal_data; +%include "pjsua2/persistent.hpp" + +%include "pjsua2/siptypes.hpp" + +%template(SipHeaderVector) std::vector<pj::SipHeader>; +%template(AuthCredInfoVector) std::vector<pj::AuthCredInfo>; +%template(SipMultipartPartVector) std::vector<pj::SipMultipartPart>; +%template(BuddyVector) std::vector<pj::Buddy*>; +%template(AudioMediaVector) std::vector<pj::AudioMedia*>; +%template(MediaFormatVector) std::vector<pj::MediaFormat*>; +%template(AudioDevInfoVector) std::vector<pj::AudioDevInfo*>; +%template(CodecInfoVector) std::vector<pj::CodecInfo*>; + +%include "pjsua2/media.hpp" +%include "pjsua2/endpoint.hpp" +%include "pjsua2/presence.hpp" +%include "pjsua2/account.hpp" +%include "pjsua2/call.hpp" + +%template(CallMediaInfoVector) std::vector<pj::CallMediaInfo>; + +%ignore pj::JsonDocument::allocElement; +%ignore pj::JsonDocument::getPool; +%include "pjsua2/json.hpp" diff --git a/pjsip-apps/src/swig/python/Makefile b/pjsip-apps/src/swig/python/Makefile new file mode 100644 index 00000000..80af9bf9 --- /dev/null +++ b/pjsip-apps/src/swig/python/Makefile @@ -0,0 +1,29 @@ +PYTHON_SO=_pjsua2.so + +#PYTHON_SETUP_FLAGS = --inplace +ifeq ($(OS),Windows_NT) + PYTHON_SETUP_FLAGS += --compiler=mingw32 +endif + +SWIG_FLAGS += -w312 + +.PHONY: all install uninstall + +all: $(PYTHON_SO) + +$(PYTHON_SO): pjsua2_wrap.cpp setup.py + python setup.py build $(PYTHON_SETUP_FLAGS) + +pjsua2_wrap.cpp: ../pjsua2.i ../symbols.i Makefile $(SRCS) + swig $(SWIG_FLAGS) -python -o pjsua2_wrap.cpp ../pjsua2.i + +clean distclean realclean: + rm -rf $(PYTHON_SO) pjsua2_wrap.cpp pjsua2_wrap.h pjsua2.py build *.pyc + +install: + python setup.py install --user + +uninstall: + rm -f $(HOME)/.local/lib/python2.7/site-packages/pjsua2* + rm -f $(HOME)/.local/lib/python2.7/site-packages/_pjsua2* + diff --git a/pjsip-apps/src/swig/python/helper.mak b/pjsip-apps/src/swig/python/helper.mak new file mode 100644 index 00000000..41baf2b2 --- /dev/null +++ b/pjsip-apps/src/swig/python/helper.mak @@ -0,0 +1,20 @@ +include ../../../../build.mak + +lib_dir: + @for token in `echo $(APP_LDFLAGS)`; do \ + echo $$token | grep L | sed 's/-L//'; \ + done + +inc_dir: + @for token in `echo $(APP_CFLAGS)`; do \ + echo $$token | grep I | sed 's/-I//'; \ + done + +libs: + @for token in `echo $(APP_LDLIBS)`; do \ + echo $$token | grep \\-l | sed 's/-l//'; \ + done + +target_name: + @echo $(TARGET_NAME) + diff --git a/pjsip-apps/src/swig/python/setup.py b/pjsip-apps/src/swig/python/setup.py new file mode 100644 index 00000000..16842e51 --- /dev/null +++ b/pjsip-apps/src/swig/python/setup.py @@ -0,0 +1,118 @@ +# $Id$ +# +# pjsua2 Setup script. +# +# Copyright (C)2012 Teluu Inc. (http://www.teluu.com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +from distutils.core import setup, Extension +import os +import sys +import platform + +# find pjsip version +pj_version="" +pj_version_major="" +pj_version_minor="" +pj_version_rev="" +pj_version_suffix="" +f = open('../../../../version.mak', 'r') +for line in f: + if line.find("export PJ_VERSION_MAJOR") != -1: + tokens=line.split("=") + if len(tokens)>1: + pj_version_major= tokens[1].strip() + elif line.find("export PJ_VERSION_MINOR") != -1: + tokens=line.split("=") + if len(tokens)>1: + pj_version_minor= line.split("=")[1].strip() + elif line.find("export PJ_VERSION_REV") != -1: + tokens=line.split("=") + if len(tokens)>1: + pj_version_rev= line.split("=")[1].strip() + elif line.find("export PJ_VERSION_SUFFIX") != -1: + tokens=line.split("=") + if len(tokens)>1: + pj_version_suffix= line.split("=")[1].strip() + +f.close() +if not pj_version_major: + print 'Unable to get PJ_VERSION_MAJOR' + sys.exit(1) + +pj_version = pj_version_major + "." + pj_version_minor +if pj_version_rev: + pj_version += "." + pj_version_rev +if pj_version_suffix: + pj_version += "-" + pj_version_suffix + +#print 'PJ_VERSION = "'+ pj_version + '"' + +# Get targetname +f = os.popen("make --no-print-directory -f helper.mak target_name") +pj_target_name = f.read().rstrip("\r\n") +f.close() + +# Fill in pj_inc_dirs +pj_inc_dirs = [] +f = os.popen("make --no-print-directory -f helper.mak inc_dir") +for line in f: + pj_inc_dirs.append(line.rstrip("\r\n")) +f.close() + +# Fill in pj_lib_dirs +pj_lib_dirs = [] +f = os.popen("make --no-print-directory -f helper.mak lib_dir") +for line in f: + pj_lib_dirs.append(line.rstrip("\r\n")) +f.close() + +# Fill in pj_libs +pj_libs = ['pjsua2-' + pj_target_name] +f = os.popen("make --no-print-directory -f helper.mak libs") +for line in f: + pj_libs.append(line.rstrip("\r\n")) +f.close() + +# Fill in extra link args +extra_link_args = ['-static-libstdc++'] +if platform.system() == 'Darwin': + # Mac OS X depedencies + extra_link_args += ["-framework", "CoreFoundation", + "-framework", "AudioToolbox", + "-framework", "QTKit"] + # OS X Lion support + if platform.mac_ver()[0].startswith("10.7"): + extra_link_args += ["-framework", "AudioUnit"] + + +setup(name="pjsua2", + version=pj_version, + description='SIP User Agent Library based on PJSIP', + url='http://www.pjsip.org', + ext_modules = [Extension("_pjsua2", + ["pjsua2_wrap.cpp"], + define_macros=[('PJ_AUTOCONF', '1'),], + include_dirs=pj_inc_dirs, + library_dirs=pj_lib_dirs, + libraries=pj_libs, + extra_link_args=extra_link_args + ) + ], + py_modules=["pjsua2"] + ) + + diff --git a/pjsip-apps/src/swig/python/test.py b/pjsip-apps/src/swig/python/test.py new file mode 100644 index 00000000..dc805c77 --- /dev/null +++ b/pjsip-apps/src/swig/python/test.py @@ -0,0 +1,112 @@ +import pjsua2 as pj +import sys + +# +# Basic data structure test, to make sure basic struct +# and array operations work +# +def ua_data_test(): + # + # AuthCredInfo + # + print "UA data types test.." + the_realm = "pjsip.org" + ci = pj.AuthCredInfo() + ci.realm = the_realm + ci.dataType = 20 + + ci2 = ci + assert ci.dataType == 20 + assert ci2.realm == the_realm + + # + # UaConfig + # See here how we manipulate std::vector + # + uc = pj.UaConfig() + uc.maxCalls = 10 + uc.userAgent = "Python" + uc.nameserver = pj.StringVector(["10.0.0.1", "10.0.0.2"]) + uc.nameserver.append("NS1") + + uc2 = uc + assert uc2.maxCalls == 10 + assert uc2.userAgent == "Python" + assert len(uc2.nameserver) == 3 + assert uc2.nameserver[0] == "10.0.0.1" + assert uc2.nameserver[1] == "10.0.0.2" + assert uc2.nameserver[2] == "NS1" + + print " Dumping nameservers: ", + for s in uc2.nameserver: + print s, + print "" + +# +# Exception test +# +def ua_run_test_exception(): + print "Exception test.." + ep = pj.Endpoint() + ep.libCreate() + got_exception = False + try: + ep.natDetectType() + except pj.Error, e: + got_exception = True + print " Got exception: status=%u, reason=%s,\n title=%s,\n srcFile=%s, srcLine=%d" % \ + (e.status, e.reason, e.title, e.srcFile, e.srcLine) + assert e.status == 370050 + assert e.reason.find("PJNATH_ESTUNINSERVER") >= 0 + assert e.title == "pjsua_detect_nat_type()" + assert got_exception + +# +# Custom log writer +# +class MyLogWriter(pj.LogWriter): + def write(self, entry): + print "This is Python:", entry.msg + +# +# Testing log writer callback +# +def ua_run_log_test(): + print "Logging test.." + ep_cfg = pj.EpConfig() + + lw = MyLogWriter() + ep_cfg.logConfig.writer = lw + ep_cfg.logConfig.decor = ep_cfg.logConfig.decor & ~(pj.PJ_LOG_HAS_CR | pj.PJ_LOG_HAS_NEWLINE) + + ep = pj.Endpoint() + ep.libCreate() + ep.libInit(ep_cfg) + ep.libDestroy() + +# +# Simple create, init, start, and destroy sequence +# +def ua_run_ua_test(): + print "UA test run.." + ep_cfg = pj.EpConfig() + + ep = pj.Endpoint() + ep.libCreate() + ep.libInit(ep_cfg) + ep.libStart() + + print "************* Endpoint started ok, now shutting down... *************" + ep.libDestroy() + +# +# main() +# +if __name__ == "__main__": + ua_data_test() + ua_run_test_exception() + ua_run_log_test() + ua_run_ua_test() + sys.exit(0) + +
\ No newline at end of file diff --git a/pjsip-apps/src/swig/symbols.i b/pjsip-apps/src/swig/symbols.i new file mode 100644 index 00000000..e101e774 --- /dev/null +++ b/pjsip-apps/src/swig/symbols.i @@ -0,0 +1,130 @@ +// This file is autogenerated by importsym script, do not modify! + +typedef int pj_status_t; + +enum pj_constants_ {PJ_SUCCESS = 0, PJ_TRUE = 1, PJ_FALSE = 0}; + +typedef unsigned char pj_uint8_t; + +typedef int pj_int32_t; + +typedef unsigned int pj_uint32_t; + +typedef unsigned short pj_uint16_t; + +enum pj_file_access {PJ_O_RDONLY = 0x1101, PJ_O_WRONLY = 0x1102, PJ_O_RDWR = 0x1103, PJ_O_APPEND = 0x1108}; + +enum pj_log_decoration {PJ_LOG_HAS_DAY_NAME = 1, PJ_LOG_HAS_YEAR = 2, PJ_LOG_HAS_MONTH = 4, PJ_LOG_HAS_DAY_OF_MON = 8, PJ_LOG_HAS_TIME = 16, PJ_LOG_HAS_MICRO_SEC = 32, PJ_LOG_HAS_SENDER = 64, PJ_LOG_HAS_NEWLINE = 128, PJ_LOG_HAS_CR = 256, PJ_LOG_HAS_SPACE = 512, PJ_LOG_HAS_COLOR = 1024, PJ_LOG_HAS_LEVEL_TEXT = 2048, PJ_LOG_HAS_THREAD_ID = 4096, PJ_LOG_HAS_THREAD_SWC = 8192, PJ_LOG_HAS_INDENT = 16384}; + +typedef enum pj_qos_type {PJ_QOS_TYPE_BEST_EFFORT, PJ_QOS_TYPE_BACKGROUND, PJ_QOS_TYPE_VIDEO, PJ_QOS_TYPE_VOICE, PJ_QOS_TYPE_CONTROL} pj_qos_type; + +typedef enum pj_qos_flag {PJ_QOS_PARAM_HAS_DSCP = 1, PJ_QOS_PARAM_HAS_SO_PRIO = 2, PJ_QOS_PARAM_HAS_WMM = 4} pj_qos_flag; + +typedef enum pj_qos_wmm_prio {PJ_QOS_WMM_PRIO_BULK_EFFORT, PJ_QOS_WMM_PRIO_BULK, PJ_QOS_WMM_PRIO_VIDEO, PJ_QOS_WMM_PRIO_VOICE} pj_qos_wmm_prio; + +typedef struct pj_qos_params +{ + pj_uint8_t flags; + pj_uint8_t dscp_val; + pj_uint8_t so_prio; + pj_qos_wmm_prio wmm_prio; +} pj_qos_params; + +typedef enum pj_ssl_cipher {PJ_TLS_NULL_WITH_NULL_NULL = 0x00000000, PJ_TLS_RSA_WITH_NULL_MD5 = 0x00000001, PJ_TLS_RSA_WITH_NULL_SHA = 0x00000002, PJ_TLS_RSA_WITH_NULL_SHA256 = 0x0000003B, PJ_TLS_RSA_WITH_RC4_128_MD5 = 0x00000004, PJ_TLS_RSA_WITH_RC4_128_SHA = 0x00000005, PJ_TLS_RSA_WITH_3DES_EDE_CBC_SHA = 0x0000000A, PJ_TLS_RSA_WITH_AES_128_CBC_SHA = 0x0000002F, PJ_TLS_RSA_WITH_AES_256_CBC_SHA = 0x00000035, PJ_TLS_RSA_WITH_AES_128_CBC_SHA256 = 0x0000003C, PJ_TLS_RSA_WITH_AES_256_CBC_SHA256 = 0x0000003D, PJ_TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA = 0x0000000D, PJ_TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA = 0x00000010, PJ_TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA = 0x00000013, PJ_TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA = 0x00000016, PJ_TLS_DH_DSS_WITH_AES_128_CBC_SHA = 0x00000030, PJ_TLS_DH_RSA_WITH_AES_128_CBC_SHA = 0x00000031, PJ_TLS_DHE_DSS_WITH_AES_128_CBC_SHA = 0x00000032, PJ_TLS_DHE_RSA_WITH_AES_128_CBC_SHA = 0x00000033, PJ_TLS_DH_DSS_WITH_AES_256_CBC_SHA = 0x00000036, PJ_TLS_DH_RSA_WITH_AES_256_CBC_SHA = 0x00000037, PJ_TLS_DHE_DSS_WITH_AES_256_CBC_SHA = 0x00000038, PJ_TLS_DHE_RSA_WITH_AES_256_CBC_SHA = 0x00000039, PJ_TLS_DH_DSS_WITH_AES_128_CBC_SHA256 = 0x0000003E, PJ_TLS_DH_RSA_WITH_AES_128_CBC_SHA256 = 0x0000003F, PJ_TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 = 0x00000040, PJ_TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 = 0x00000067, PJ_TLS_DH_DSS_WITH_AES_256_CBC_SHA256 = 0x00000068, PJ_TLS_DH_RSA_WITH_AES_256_CBC_SHA256 = 0x00000069, PJ_TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 = 0x0000006A, PJ_TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 = 0x0000006B, PJ_TLS_DH_anon_WITH_RC4_128_MD5 = 0x00000018, PJ_TLS_DH_anon_WITH_3DES_EDE_CBC_SHA = 0x0000001B, PJ_TLS_DH_anon_WITH_AES_128_CBC_SHA = 0x00000034, PJ_TLS_DH_anon_WITH_AES_256_CBC_SHA = 0x0000003A, PJ_TLS_DH_anon_WITH_AES_128_CBC_SHA256 = 0x0000006C, PJ_TLS_DH_anon_WITH_AES_256_CBC_SHA256 = 0x0000006D, PJ_TLS_RSA_EXPORT_WITH_RC4_40_MD5 = 0x00000003, PJ_TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5 = 0x00000006, PJ_TLS_RSA_WITH_IDEA_CBC_SHA = 0x00000007, PJ_TLS_RSA_EXPORT_WITH_DES40_CBC_SHA = 0x00000008, PJ_TLS_RSA_WITH_DES_CBC_SHA = 0x00000009, PJ_TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA = 0x0000000B, PJ_TLS_DH_DSS_WITH_DES_CBC_SHA = 0x0000000C, PJ_TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA = 0x0000000E, PJ_TLS_DH_RSA_WITH_DES_CBC_SHA = 0x0000000F, PJ_TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA = 0x00000011, PJ_TLS_DHE_DSS_WITH_DES_CBC_SHA = 0x00000012, PJ_TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA = 0x00000014, PJ_TLS_DHE_RSA_WITH_DES_CBC_SHA = 0x00000015, PJ_TLS_DH_anon_EXPORT_WITH_RC4_40_MD5 = 0x00000017, PJ_TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA = 0x00000019, PJ_TLS_DH_anon_WITH_DES_CBC_SHA = 0x0000001A, PJ_SSL_FORTEZZA_KEA_WITH_NULL_SHA = 0x0000001C, PJ_SSL_FORTEZZA_KEA_WITH_FORTEZZA_CBC_SHA = 0x0000001D, PJ_SSL_FORTEZZA_KEA_WITH_RC4_128_SHA = 0x0000001E, PJ_SSL_CK_RC4_128_WITH_MD5 = 0x00010080, PJ_SSL_CK_RC4_128_EXPORT40_WITH_MD5 = 0x00020080, PJ_SSL_CK_RC2_128_CBC_WITH_MD5 = 0x00030080, PJ_SSL_CK_RC2_128_CBC_EXPORT40_WITH_MD5 = 0x00040080, PJ_SSL_CK_IDEA_128_CBC_WITH_MD5 = 0x00050080, PJ_SSL_CK_DES_64_CBC_WITH_MD5 = 0x00060040, PJ_SSL_CK_DES_192_EDE3_CBC_WITH_MD5 = 0x000700C0} pj_ssl_cipher; + +typedef enum pj_stun_nat_type {PJ_STUN_NAT_TYPE_UNKNOWN, PJ_STUN_NAT_TYPE_ERR_UNKNOWN, PJ_STUN_NAT_TYPE_OPEN, PJ_STUN_NAT_TYPE_BLOCKED, PJ_STUN_NAT_TYPE_SYMMETRIC_UDP, PJ_STUN_NAT_TYPE_FULL_CONE, PJ_STUN_NAT_TYPE_SYMMETRIC, PJ_STUN_NAT_TYPE_RESTRICTED, PJ_STUN_NAT_TYPE_PORT_RESTRICTED} pj_stun_nat_type; + +typedef enum pj_turn_tp_type {PJ_TURN_TP_UDP = 17, PJ_TURN_TP_TCP = 6, PJ_TURN_TP_TLS = 255} pj_turn_tp_type; + +typedef enum pjmedia_event_type {PJMEDIA_EVENT_NONE, PJMEDIA_EVENT_FMT_CHANGED = ((('H' << 24) | ('C' << 16)) | ('M' << 8)) | 'F', PJMEDIA_EVENT_WND_CLOSING = ((('L' << 24) | ('C' << 16)) | ('N' << 8)) | 'W', PJMEDIA_EVENT_WND_CLOSED = ((('O' << 24) | ('C' << 16)) | ('N' << 8)) | 'W', PJMEDIA_EVENT_WND_RESIZED = ((('Z' << 24) | ('R' << 16)) | ('N' << 8)) | 'W', PJMEDIA_EVENT_MOUSE_BTN_DOWN = ((('N' << 24) | ('D' << 16)) | ('S' << 8)) | 'M', PJMEDIA_EVENT_KEYFRAME_FOUND = ((('F' << 24) | ('R' << 16)) | ('F' << 8)) | 'I', PJMEDIA_EVENT_KEYFRAME_MISSING = ((('M' << 24) | ('R' << 16)) | ('F' << 8)) | 'I', PJMEDIA_EVENT_ORIENT_CHANGED = ((('T' << 24) | ('N' << 16)) | ('R' << 8)) | 'O'} pjmedia_event_type; + +typedef enum pjmedia_srtp_use {PJMEDIA_SRTP_DISABLED, PJMEDIA_SRTP_OPTIONAL, PJMEDIA_SRTP_MANDATORY} pjmedia_srtp_use; + +typedef enum pjmedia_vid_stream_rc_method {PJMEDIA_VID_STREAM_RC_NONE = 0, PJMEDIA_VID_STREAM_RC_SIMPLE_BLOCKING = 1} pjmedia_vid_stream_rc_method; + +typedef pj_int32_t pjmedia_vid_dev_index; + +enum pjmedia_vid_dev_std_index {PJMEDIA_VID_DEFAULT_CAPTURE_DEV = -1, PJMEDIA_VID_DEFAULT_RENDER_DEV = -2, PJMEDIA_VID_INVALID_DEV = -3}; + +typedef enum pjmedia_aud_dev_route {PJMEDIA_AUD_DEV_ROUTE_DEFAULT = 0, PJMEDIA_AUD_DEV_ROUTE_LOUDSPEAKER = 1, PJMEDIA_AUD_DEV_ROUTE_EARPIECE = 2, PJMEDIA_AUD_DEV_ROUTE_BLUETOOTH = 4} pjmedia_aud_dev_route; + +typedef enum pjmedia_aud_dev_cap {PJMEDIA_AUD_DEV_CAP_EXT_FORMAT = 1, PJMEDIA_AUD_DEV_CAP_INPUT_LATENCY = 2, PJMEDIA_AUD_DEV_CAP_OUTPUT_LATENCY = 4, PJMEDIA_AUD_DEV_CAP_INPUT_VOLUME_SETTING = 8, PJMEDIA_AUD_DEV_CAP_OUTPUT_VOLUME_SETTING = 16, PJMEDIA_AUD_DEV_CAP_INPUT_SIGNAL_METER = 32, PJMEDIA_AUD_DEV_CAP_OUTPUT_SIGNAL_METER = 64, PJMEDIA_AUD_DEV_CAP_INPUT_ROUTE = 128, PJMEDIA_AUD_DEV_CAP_OUTPUT_ROUTE = 256, PJMEDIA_AUD_DEV_CAP_EC = 512, PJMEDIA_AUD_DEV_CAP_EC_TAIL = 1024, PJMEDIA_AUD_DEV_CAP_VAD = 2048, PJMEDIA_AUD_DEV_CAP_CNG = 4096, PJMEDIA_AUD_DEV_CAP_PLC = 8192, PJMEDIA_AUD_DEV_CAP_MAX = 16384} pjmedia_aud_dev_cap; + +enum pjmedia_file_writer_option {PJMEDIA_FILE_WRITE_PCM = 0, PJMEDIA_FILE_WRITE_ALAW = 1, PJMEDIA_FILE_WRITE_ULAW = 2}; + +enum pjmedia_file_player_option {PJMEDIA_FILE_NO_LOOP = 1}; + +typedef enum pjmedia_type {PJMEDIA_TYPE_NONE, PJMEDIA_TYPE_AUDIO, PJMEDIA_TYPE_VIDEO, PJMEDIA_TYPE_APPLICATION, PJMEDIA_TYPE_UNKNOWN} pjmedia_type; + +typedef enum pjmedia_dir {PJMEDIA_DIR_NONE = 0, PJMEDIA_DIR_ENCODING = 1, PJMEDIA_DIR_CAPTURE = PJMEDIA_DIR_ENCODING, PJMEDIA_DIR_DECODING = 2, PJMEDIA_DIR_PLAYBACK = PJMEDIA_DIR_DECODING, PJMEDIA_DIR_RENDER = PJMEDIA_DIR_DECODING, PJMEDIA_DIR_ENCODING_DECODING = 3, PJMEDIA_DIR_CAPTURE_PLAYBACK = PJMEDIA_DIR_ENCODING_DECODING, PJMEDIA_DIR_CAPTURE_RENDER = PJMEDIA_DIR_ENCODING_DECODING} pjmedia_dir; + +typedef enum pjmedia_tp_proto {PJMEDIA_TP_PROTO_NONE = 0, PJMEDIA_TP_PROTO_RTP_AVP, PJMEDIA_TP_PROTO_RTP_SAVP, PJMEDIA_TP_PROTO_UNKNOWN} pjmedia_tp_proto; + +typedef enum pjmedia_format_id {PJMEDIA_FORMAT_L16 = 0, PJMEDIA_FORMAT_PCM = PJMEDIA_FORMAT_L16, PJMEDIA_FORMAT_PCMA = ((('W' << 24) | ('A' << 16)) | ('L' << 8)) | 'A', PJMEDIA_FORMAT_ALAW = PJMEDIA_FORMAT_PCMA, PJMEDIA_FORMAT_PCMU = ((('W' << 24) | ('A' << 16)) | ('L' << 8)) | 'u', PJMEDIA_FORMAT_ULAW = PJMEDIA_FORMAT_PCMU, PJMEDIA_FORMAT_AMR = ((('R' << 24) | ('M' << 16)) | ('A' << 8)) | ' ', PJMEDIA_FORMAT_G729 = ((('9' << 24) | ('2' << 16)) | ('7' << 8)) | 'G', PJMEDIA_FORMAT_ILBC = ((('C' << 24) | ('B' << 16)) | ('L' << 8)) | 'I', PJMEDIA_FORMAT_RGB24 = ((('3' << 24) | ('B' << 16)) | ('G' << 8)) | 'R', PJMEDIA_FORMAT_RGBA = ((('A' << 24) | ('B' << 16)) | ('G' << 8)) | 'R', PJMEDIA_FORMAT_BGRA = ((('A' << 24) | ('R' << 16)) | ('G' << 8)) | 'B', PJMEDIA_FORMAT_RGB32 = PJMEDIA_FORMAT_RGBA, PJMEDIA_FORMAT_DIB = (((' ' << 24) | ('B' << 16)) | ('I' << 8)) | 'D', PJMEDIA_FORMAT_GBRP = ((('P' << 24) | ('R' << 16)) | ('B' << 8)) | 'G', PJMEDIA_FORMAT_AYUV = ((('V' << 24) | ('U' << 16)) | ('Y' << 8)) | 'A', PJMEDIA_FORMAT_YUY2 = ((('2' << 24) | ('Y' << 16)) | ('U' << 8)) | 'Y', PJMEDIA_FORMAT_UYVY = ((('Y' << 24) | ('V' << 16)) | ('Y' << 8)) | 'U', PJMEDIA_FORMAT_YVYU = ((('U' << 24) | ('Y' << 16)) | ('V' << 8)) | 'Y', PJMEDIA_FORMAT_I420 = ((('0' << 24) | ('2' << 16)) | ('4' << 8)) | 'I', PJMEDIA_FORMAT_IYUV = PJMEDIA_FORMAT_I420, PJMEDIA_FORMAT_YV12 = ((('2' << 24) | ('1' << 16)) | ('V' << 8)) | 'Y', PJMEDIA_FORMAT_I422 = ((('2' << 24) | ('2' << 16)) | ('4' << 8)) | 'I', PJMEDIA_FORMAT_I420JPEG = ((('0' << 24) | ('2' << 16)) | ('4' << 8)) | 'J', PJMEDIA_FORMAT_I422JPEG = ((('2' << 24) | ('2' << 16)) | ('4' << 8)) | 'J', PJMEDIA_FORMAT_H261 = ((('1' << 24) | ('6' << 16)) | ('2' << 8)) | 'H', PJMEDIA_FORMAT_H263 = ((('3' << 24) | ('6' << 16)) | ('2' << 8)) | 'H', PJMEDIA_FORMAT_H263P = ((('3' << 24) | ('6' << 16)) | ('2' << 8)) | 'P', PJMEDIA_FORMAT_H264 = ((('4' << 24) | ('6' << 16)) | ('2' << 8)) | 'H', PJMEDIA_FORMAT_MJPEG = ((('G' << 24) | ('P' << 16)) | ('J' << 8)) | 'M', PJMEDIA_FORMAT_MPEG1VIDEO = ((('V' << 24) | ('1' << 16)) | ('P' << 8)) | 'M', PJMEDIA_FORMAT_MPEG2VIDEO = ((('V' << 24) | ('2' << 16)) | ('P' << 8)) | 'M', PJMEDIA_FORMAT_MPEG4 = ((('4' << 24) | ('G' << 16)) | ('P' << 8)) | 'M'} pjmedia_format_id; + +typedef enum pjsip_cred_data_type {PJSIP_CRED_DATA_PLAIN_PASSWD = 0, PJSIP_CRED_DATA_DIGEST = 1, PJSIP_CRED_DATA_EXT_AKA = 16} pjsip_cred_data_type; + +typedef enum pjsip_dialog_cap_status {PJSIP_DIALOG_CAP_UNSUPPORTED = 0, PJSIP_DIALOG_CAP_SUPPORTED = 1, PJSIP_DIALOG_CAP_UNKNOWN = 2} pjsip_dialog_cap_status; + +typedef enum pjsip_event_id_e {PJSIP_EVENT_UNKNOWN, PJSIP_EVENT_TIMER, PJSIP_EVENT_TX_MSG, PJSIP_EVENT_RX_MSG, PJSIP_EVENT_TRANSPORT_ERROR, PJSIP_EVENT_TSX_STATE, PJSIP_EVENT_USER} pjsip_event_id_e; + +typedef enum pjsip_status_code {PJSIP_SC_TRYING = 100, PJSIP_SC_RINGING = 180, PJSIP_SC_CALL_BEING_FORWARDED = 181, PJSIP_SC_QUEUED = 182, PJSIP_SC_PROGRESS = 183, PJSIP_SC_OK = 200, PJSIP_SC_ACCEPTED = 202, PJSIP_SC_MULTIPLE_CHOICES = 300, PJSIP_SC_MOVED_PERMANENTLY = 301, PJSIP_SC_MOVED_TEMPORARILY = 302, PJSIP_SC_USE_PROXY = 305, PJSIP_SC_ALTERNATIVE_SERVICE = 380, PJSIP_SC_BAD_REQUEST = 400, PJSIP_SC_UNAUTHORIZED = 401, PJSIP_SC_PAYMENT_REQUIRED = 402, PJSIP_SC_FORBIDDEN = 403, PJSIP_SC_NOT_FOUND = 404, PJSIP_SC_METHOD_NOT_ALLOWED = 405, PJSIP_SC_NOT_ACCEPTABLE = 406, PJSIP_SC_PROXY_AUTHENTICATION_REQUIRED = 407, PJSIP_SC_REQUEST_TIMEOUT = 408, PJSIP_SC_GONE = 410, PJSIP_SC_REQUEST_ENTITY_TOO_LARGE = 413, PJSIP_SC_REQUEST_URI_TOO_LONG = 414, PJSIP_SC_UNSUPPORTED_MEDIA_TYPE = 415, PJSIP_SC_UNSUPPORTED_URI_SCHEME = 416, PJSIP_SC_BAD_EXTENSION = 420, PJSIP_SC_EXTENSION_REQUIRED = 421, PJSIP_SC_SESSION_TIMER_TOO_SMALL = 422, PJSIP_SC_INTERVAL_TOO_BRIEF = 423, PJSIP_SC_TEMPORARILY_UNAVAILABLE = 480, PJSIP_SC_CALL_TSX_DOES_NOT_EXIST = 481, PJSIP_SC_LOOP_DETECTED = 482, PJSIP_SC_TOO_MANY_HOPS = 483, PJSIP_SC_ADDRESS_INCOMPLETE = 484, PJSIP_AC_AMBIGUOUS = 485, PJSIP_SC_BUSY_HERE = 486, PJSIP_SC_REQUEST_TERMINATED = 487, PJSIP_SC_NOT_ACCEPTABLE_HERE = 488, PJSIP_SC_BAD_EVENT = 489, PJSIP_SC_REQUEST_UPDATED = 490, PJSIP_SC_REQUEST_PENDING = 491, PJSIP_SC_UNDECIPHERABLE = 493, PJSIP_SC_INTERNAL_SERVER_ERROR = 500, PJSIP_SC_NOT_IMPLEMENTED = 501, PJSIP_SC_BAD_GATEWAY = 502, PJSIP_SC_SERVICE_UNAVAILABLE = 503, PJSIP_SC_SERVER_TIMEOUT = 504, PJSIP_SC_VERSION_NOT_SUPPORTED = 505, PJSIP_SC_MESSAGE_TOO_LARGE = 513, PJSIP_SC_PRECONDITION_FAILURE = 580, PJSIP_SC_BUSY_EVERYWHERE = 600, PJSIP_SC_DECLINE = 603, PJSIP_SC_DOES_NOT_EXIST_ANYWHERE = 604, PJSIP_SC_NOT_ACCEPTABLE_ANYWHERE = 606, PJSIP_SC_TSX_TIMEOUT = PJSIP_SC_REQUEST_TIMEOUT, PJSIP_SC_TSX_TRANSPORT_ERROR = PJSIP_SC_SERVICE_UNAVAILABLE, PJSIP_SC__force_32bit = 0x7FFFFFFF} pjsip_status_code; + +typedef enum pjsip_hdr_e {PJSIP_H_ACCEPT, PJSIP_H_ACCEPT_ENCODING_UNIMP, PJSIP_H_ACCEPT_LANGUAGE_UNIMP, PJSIP_H_ALERT_INFO_UNIMP, PJSIP_H_ALLOW, PJSIP_H_AUTHENTICATION_INFO_UNIMP, PJSIP_H_AUTHORIZATION, PJSIP_H_CALL_ID, PJSIP_H_CALL_INFO_UNIMP, PJSIP_H_CONTACT, PJSIP_H_CONTENT_DISPOSITION_UNIMP, PJSIP_H_CONTENT_ENCODING_UNIMP, PJSIP_H_CONTENT_LANGUAGE_UNIMP, PJSIP_H_CONTENT_LENGTH, PJSIP_H_CONTENT_TYPE, PJSIP_H_CSEQ, PJSIP_H_DATE_UNIMP, PJSIP_H_ERROR_INFO_UNIMP, PJSIP_H_EXPIRES, PJSIP_H_FROM, PJSIP_H_IN_REPLY_TO_UNIMP, PJSIP_H_MAX_FORWARDS, PJSIP_H_MIME_VERSION_UNIMP, PJSIP_H_MIN_EXPIRES, PJSIP_H_ORGANIZATION_UNIMP, PJSIP_H_PRIORITY_UNIMP, PJSIP_H_PROXY_AUTHENTICATE, PJSIP_H_PROXY_AUTHORIZATION, PJSIP_H_PROXY_REQUIRE_UNIMP, PJSIP_H_RECORD_ROUTE, PJSIP_H_REPLY_TO_UNIMP, PJSIP_H_REQUIRE, PJSIP_H_RETRY_AFTER, PJSIP_H_ROUTE, PJSIP_H_SERVER_UNIMP, PJSIP_H_SUBJECT_UNIMP, PJSIP_H_SUPPORTED, PJSIP_H_TIMESTAMP_UNIMP, PJSIP_H_TO, PJSIP_H_UNSUPPORTED, PJSIP_H_USER_AGENT_UNIMP, PJSIP_H_VIA, PJSIP_H_WARNING_UNIMP, PJSIP_H_WWW_AUTHENTICATE, PJSIP_H_OTHER} pjsip_hdr_e; + +typedef enum pjsip_transport_type_e {PJSIP_TRANSPORT_UNSPECIFIED, PJSIP_TRANSPORT_UDP, PJSIP_TRANSPORT_TCP, PJSIP_TRANSPORT_TLS, PJSIP_TRANSPORT_SCTP, PJSIP_TRANSPORT_LOOP, PJSIP_TRANSPORT_LOOP_DGRAM, PJSIP_TRANSPORT_START_OTHER, PJSIP_TRANSPORT_IPV6 = 128, PJSIP_TRANSPORT_UDP6 = PJSIP_TRANSPORT_UDP + PJSIP_TRANSPORT_IPV6, PJSIP_TRANSPORT_TCP6 = PJSIP_TRANSPORT_TCP + PJSIP_TRANSPORT_IPV6, PJSIP_TRANSPORT_TLS6 = PJSIP_TRANSPORT_TLS + PJSIP_TRANSPORT_IPV6} pjsip_transport_type_e; + +enum pjsip_transport_flags_e {PJSIP_TRANSPORT_RELIABLE = 1, PJSIP_TRANSPORT_SECURE = 2, PJSIP_TRANSPORT_DATAGRAM = 4}; + +typedef enum pjsip_transport_state {PJSIP_TP_STATE_CONNECTED, PJSIP_TP_STATE_DISCONNECTED} pjsip_transport_state; + +typedef enum pjsip_ssl_method {PJSIP_SSL_UNSPECIFIED_METHOD = 0, PJSIP_TLSV1_METHOD = 31, PJSIP_SSLV2_METHOD = 20, PJSIP_SSLV3_METHOD = 30, PJSIP_SSLV23_METHOD = 23} pjsip_ssl_method; + +typedef enum pjsip_tsx_state_e {PJSIP_TSX_STATE_NULL, PJSIP_TSX_STATE_CALLING, PJSIP_TSX_STATE_TRYING, PJSIP_TSX_STATE_PROCEEDING, PJSIP_TSX_STATE_COMPLETED, PJSIP_TSX_STATE_CONFIRMED, PJSIP_TSX_STATE_TERMINATED, PJSIP_TSX_STATE_DESTROYED, PJSIP_TSX_STATE_MAX} pjsip_tsx_state_e; + +typedef enum pjsip_role_e {PJSIP_ROLE_UAC, PJSIP_ROLE_UAS, PJSIP_UAC_ROLE = PJSIP_ROLE_UAC, PJSIP_UAS_ROLE = PJSIP_ROLE_UAS} pjsip_role_e; + +typedef enum pjsip_redirect_op {PJSIP_REDIRECT_REJECT, PJSIP_REDIRECT_ACCEPT, PJSIP_REDIRECT_ACCEPT_REPLACE, PJSIP_REDIRECT_PENDING, PJSIP_REDIRECT_STOP} pjsip_redirect_op; + +typedef enum pjrpid_activity {PJRPID_ACTIVITY_UNKNOWN, PJRPID_ACTIVITY_AWAY, PJRPID_ACTIVITY_BUSY} pjrpid_activity; + +typedef enum pjsip_evsub_state {PJSIP_EVSUB_STATE_NULL, PJSIP_EVSUB_STATE_SENT, PJSIP_EVSUB_STATE_ACCEPTED, PJSIP_EVSUB_STATE_PENDING, PJSIP_EVSUB_STATE_ACTIVE, PJSIP_EVSUB_STATE_TERMINATED, PJSIP_EVSUB_STATE_UNKNOWN} pjsip_evsub_state; + +typedef enum pjsip_inv_state {PJSIP_INV_STATE_NULL, PJSIP_INV_STATE_CALLING, PJSIP_INV_STATE_INCOMING, PJSIP_INV_STATE_EARLY, PJSIP_INV_STATE_CONNECTING, PJSIP_INV_STATE_CONFIRMED, PJSIP_INV_STATE_DISCONNECTED} pjsip_inv_state; + +enum pjsua_invalid_id_const_ {PJSUA_INVALID_ID = -1}; + +typedef enum pjsua_state {PJSUA_STATE_NULL, PJSUA_STATE_CREATED, PJSUA_STATE_INIT, PJSUA_STATE_STARTING, PJSUA_STATE_RUNNING, PJSUA_STATE_CLOSING} pjsua_state; + +typedef enum pjsua_stun_use {PJSUA_STUN_USE_DEFAULT, PJSUA_STUN_USE_DISABLED} pjsua_stun_use; + +typedef enum pjsua_call_hold_type {PJSUA_CALL_HOLD_TYPE_RFC3264, PJSUA_CALL_HOLD_TYPE_RFC2543} pjsua_call_hold_type; + +typedef int pjsua_acc_id; + +typedef enum pjsua_destroy_flag {PJSUA_DESTROY_NO_RX_MSG = 1, PJSUA_DESTROY_NO_TX_MSG = 2, PJSUA_DESTROY_NO_NETWORK = PJSUA_DESTROY_NO_RX_MSG | PJSUA_DESTROY_NO_TX_MSG} pjsua_destroy_flag; + +typedef enum pjsua_100rel_use {PJSUA_100REL_NOT_USED, PJSUA_100REL_MANDATORY, PJSUA_100REL_OPTIONAL} pjsua_100rel_use; + +typedef enum pjsua_sip_timer_use {PJSUA_SIP_TIMER_INACTIVE, PJSUA_SIP_TIMER_OPTIONAL, PJSUA_SIP_TIMER_REQUIRED, PJSUA_SIP_TIMER_ALWAYS} pjsua_sip_timer_use; + +typedef enum pjsua_ipv6_use {PJSUA_IPV6_DISABLED, PJSUA_IPV6_ENABLED} pjsua_ipv6_use; + +typedef enum pjsua_buddy_status {PJSUA_BUDDY_STATUS_UNKNOWN, PJSUA_BUDDY_STATUS_ONLINE, PJSUA_BUDDY_STATUS_OFFLINE} pjsua_buddy_status; + +typedef enum pjsua_call_media_status {PJSUA_CALL_MEDIA_NONE, PJSUA_CALL_MEDIA_ACTIVE, PJSUA_CALL_MEDIA_LOCAL_HOLD, PJSUA_CALL_MEDIA_REMOTE_HOLD, PJSUA_CALL_MEDIA_ERROR} pjsua_call_media_status; + +typedef int pjsua_vid_win_id; + +typedef int pjsua_call_id; + +typedef enum pjsua_med_tp_st {PJSUA_MED_TP_NULL, PJSUA_MED_TP_CREATING, PJSUA_MED_TP_IDLE, PJSUA_MED_TP_INIT, PJSUA_MED_TP_RUNNING, PJSUA_MED_TP_DISABLED} pjsua_med_tp_st; + +typedef enum pjsua_call_vid_strm_op {PJSUA_CALL_VID_STRM_NO_OP, PJSUA_CALL_VID_STRM_ADD, PJSUA_CALL_VID_STRM_REMOVE, PJSUA_CALL_VID_STRM_CHANGE_DIR, PJSUA_CALL_VID_STRM_CHANGE_CAP_DEV, PJSUA_CALL_VID_STRM_START_TRANSMIT, PJSUA_CALL_VID_STRM_STOP_TRANSMIT, PJSUA_CALL_VID_STRM_SEND_KEYFRAME} pjsua_call_vid_strm_op; + +typedef enum pjsua_vid_req_keyframe_method {PJSUA_VID_REQ_KEYFRAME_SIP_INFO = 1, PJSUA_VID_REQ_KEYFRAME_RTCP_PLI = 2} pjsua_vid_req_keyframe_method; + +typedef enum pjsua_call_flag {PJSUA_CALL_UNHOLD = 1, PJSUA_CALL_UPDATE_CONTACT = 2, PJSUA_CALL_INCLUDE_DISABLED_MEDIA = 4} pjsua_call_flag; + +typedef enum pjsua_create_media_transport_flag {PJSUA_MED_TP_CLOSE_MEMBER = 1} pjsua_create_media_transport_flag; + diff --git a/pjsip-apps/src/swig/symbols.lst b/pjsip-apps/src/swig/symbols.lst new file mode 100644 index 00000000..685c3713 --- /dev/null +++ b/pjsip-apps/src/swig/symbols.lst @@ -0,0 +1,34 @@ +pj/types.h pj_status_t pj_constants_ pj_uint8_t pj_int32_t pj_uint32_t pj_uint16_t +pj/file_io.h pj_file_access +pj/log.h pj_log_decoration +pj/sock_qos.h pj_qos_type pj_qos_flag pj_qos_wmm_prio pj_qos_params +pj/ssl_sock.h pj_ssl_cipher + +pjnath/nat_detect.h pj_stun_nat_type +pjnath/turn_session.h pj_turn_tp_type + +pjmedia/event.h pjmedia_event_type +pjmedia/transport_srtp.h pjmedia_srtp_use +pjmedia/vid_stream.h pjmedia_vid_stream_rc_method +pjmedia-videodev/videodev.h pjmedia_vid_dev_index pjmedia_vid_dev_std_index +pjmedia-audiodev/audiodev.h pjmedia_aud_dev_route pjmedia_aud_dev_cap +pjmedia/wav_port.h pjmedia_file_writer_option pjmedia_file_player_option +pjmedia/types.h pjmedia_type pjmedia_dir pjmedia_tp_proto +pjmedia/format.h pjmedia_format_id + +pjsip/sip_auth.h pjsip_cred_data_type +pjsip/sip_dialog.h pjsip_dialog_cap_status +pjsip/sip_event.h pjsip_event_id_e +pjsip/sip_msg.h pjsip_status_code pjsip_hdr_e +pjsip/sip_transport.h pjsip_transport_type_e pjsip_transport_flags_e pjsip_transport_state +pjsip/sip_transport_tls.h pjsip_ssl_method +pjsip/sip_transaction.h pjsip_tsx_state_e +pjsip/sip_types.h pjsip_role_e +pjsip/sip_util.h pjsip_redirect_op + +pjsip-simple/rpid.h pjrpid_activity +pjsip-simple/evsub.h pjsip_evsub_state + +pjsip-ua/sip_inv.h pjsip_inv_state + +pjsua-lib/pjsua.h pjsua_invalid_id_const_ pjsua_state pjsua_stun_use pjsua_call_hold_type pjsua_acc_id pjsua_destroy_flag pjsua_100rel_use pjsua_sip_timer_use pjsua_ipv6_use pjsua_buddy_status pjsua_call_media_status pjsua_vid_win_id pjsua_call_id pjsua_med_tp_st pjsua_call_vid_strm_op pjsua_vid_req_keyframe_method pjsua_call_flag pjsua_create_media_transport_flag |