summaryrefslogtreecommitdiff
path: root/pjsip-apps/src/pygui
diff options
context:
space:
mode:
authorLiong Sauw Ming <ming@teluu.com>2014-01-16 05:30:46 +0000
committerLiong Sauw Ming <ming@teluu.com>2014-01-16 05:30:46 +0000
commite56ea14ab8531ee3cec375460577d1b89bf62e26 (patch)
treedf77c3acb961514b2022ee9e030071b691145920 /pjsip-apps/src/pygui
parentbd1c47e995a3a844868f1d4dcc8f77f163ae721b (diff)
Closed #1723: Merging pjsua2 branch into trunk
git-svn-id: http://svn.pjsip.org/repos/pjproject/trunk@4704 74dad513-b988-da41-8d7b-12977e46ad98
Diffstat (limited to 'pjsip-apps/src/pygui')
-rw-r--r--pjsip-apps/src/pygui/account.py239
-rw-r--r--pjsip-apps/src/pygui/accountsetting.py369
-rw-r--r--pjsip-apps/src/pygui/application.py510
-rw-r--r--pjsip-apps/src/pygui/buddy.py152
-rw-r--r--pjsip-apps/src/pygui/call.py104
-rw-r--r--pjsip-apps/src/pygui/chat.py489
-rw-r--r--pjsip-apps/src/pygui/chatgui.py420
-rw-r--r--pjsip-apps/src/pygui/endpoint.py53
-rw-r--r--pjsip-apps/src/pygui/log.py127
-rw-r--r--pjsip-apps/src/pygui/settings.py362
10 files changed, 2825 insertions, 0 deletions
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