123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544 |
- #
- # 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
- write=sys.stdout.write
- # 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:
- write("=== "+ op +" cannot find participant with URI '" + uri_str + "'\r\n")
- 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
- target = None
- if self._callList[idx] and self._callList[idx].connected:
- target = self._callList[idx]
- else:
- target = self._buddyList[idx]
- assert(target)
- try:
- target.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
- target = None
- if self._callList[idx] and self._callList[idx].connected:
- target = self._callList[idx]
- else:
- target = self._buddyList[idx]
- assert(target)
- try:
- target.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.findBuddy2(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)
- if not self.isPrivate():
- # inform peer about conference participants
- conf_welcome_str = '\n---\n'
- conf_welcome_str += 'Welcome to the conference, participants:\n'
- conf_welcome_str += '%s (host)\n' % (self._acc.cfg.idUri)
- for p in self._participantList:
- conf_welcome_str += '%s\n' % (str(p))
- conf_welcome_str += '---\n'
- send_im_param = pj.SendInstantMessageParam()
- send_im_param.content = conf_welcome_str
- try:
- thecall.sendInstantMessage(send_im_param)
- except:
- pass
- # inform others, including self
- msg = "[Conf manager] %s has joined" % (thecall.peerUri)
- self.addMessage(None, msg)
- self._sendInstantMessage(msg, thecall.peerUri)
- 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))
- # inform others, including self
- msg = "[Conf manager] %s has left" % (thecall.peerUri)
- self.addMessage(None, msg)
- self._sendInstantMessage(msg, thecall.peerUri)
- def updateCallMediaState(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()
- med_idx = self._getActiveMediaIdx(thecall)
- if (med_idx < 0):
- self._gui.audioSetStatsText(thecall.peerUri, 'No active media')
- return
- si = thecall.getStreamInfo(med_idx)
- dir_str = ''
- if si.dir == 0:
- dir_str = 'inactive'
- else:
- if si.dir & pj.PJMEDIA_DIR_ENCODING:
- dir_str += 'send '
- if si.dir & pj.PJMEDIA_DIR_DECODING:
- dir_str += 'receive '
- stats_str = "Direction : %s\n" % (dir_str)
- stats_str += "Audio codec : %s (%sHz)" % (si.codecName, si.codecClockRate)
- self._gui.audioSetStatsText(thecall.peerUri, stats_str)
- m = pj.AudioMedia.typecastFromMedia(thecall.getMedia(med_idx))
- # make conference
- for c in self._callList:
- if c == thecall:
- continue
- med_idx = self._getActiveMediaIdx(c)
- if med_idx < 0:
- continue
- mm = pj.AudioMedia.typecastFromMedia(c.getMedia(med_idx))
- m.startTransmit(mm)
- mm.startTransmit(m)
- # ** 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()
|