Ignore:
Timestamp:
Aug 2, 2017 9:45:09 AM (5 years ago)
Author:
riza
Message:

Close #2034: Add support to Python3 using PJSUA2 API.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • pjproject/trunk/pjsip-apps/src/pygui/chat.py

    r4757 r5638  
    2121import sys 
    2222if sys.version_info[0] >= 3: # Python 3 
    23         import tkinter as tk 
    24         from tkinter import ttk 
     23    import tkinter as tk 
     24    from tkinter import ttk 
    2525else: 
    26         import Tkinter as tk 
    27         import ttk 
     26    import Tkinter as tk 
     27    import ttk 
    2828 
    2929import buddy 
     
    3636SipUriRegex = re.compile('(sip|sips):([^:;>\@]*)@?([^:;>]*):?([^:;>]*)') 
    3737ConfIdx = 1 
     38write=sys.stdout.write 
    3839 
    3940# Simple SIP uri parser, input URI must have been validated 
    4041def ParseSipUri(sip_uri_str): 
    41         m = SipUriRegex.search(sip_uri_str) 
    42         if not m: 
    43                 assert(0) 
    44                 return None 
    45          
    46         scheme = m.group(1) 
    47         user = m.group(2) 
    48         host = m.group(3) 
    49         port = m.group(4) 
    50         if host == '': 
    51                 host = user 
    52                 user = '' 
    53                  
    54         return SipUri(scheme.lower(), user, host.lower(), port) 
    55          
     42    m = SipUriRegex.search(sip_uri_str) 
     43    if not m: 
     44        assert(0) 
     45        return None 
     46 
     47    scheme = m.group(1) 
     48    user = m.group(2) 
     49    host = m.group(3) 
     50    port = m.group(4) 
     51    if host == '': 
     52        host = user 
     53        user = '' 
     54 
     55    return SipUri(scheme.lower(), user, host.lower(), port) 
     56 
    5657class SipUri: 
    57         def __init__(self, scheme, user, host, port): 
    58                 self.scheme = scheme 
    59                 self.user = user 
    60                 self.host = host 
    61                 self.port = port 
    62                  
    63         def __cmp__(self, sip_uri): 
    64                 if self.scheme == sip_uri.scheme and self.user == sip_uri.user and self.host == sip_uri.host: 
    65                         # don't check port, at least for now 
    66                         return 0 
    67                 return -1 
    68          
    69         def __str__(self): 
    70                 s = self.scheme + ':' 
    71                 if self.user: s += self.user + '@' 
    72                 s += self.host 
    73                 if self.port: s+= ':' + self.port 
    74                 return s 
    75          
     58    def __init__(self, scheme, user, host, port): 
     59        self.scheme = scheme 
     60        self.user = user 
     61        self.host = host 
     62        self.port = port 
     63 
     64    def __cmp__(self, sip_uri): 
     65        if self.scheme == sip_uri.scheme and self.user == sip_uri.user and self.host == sip_uri.host: 
     66            # don't check port, at least for now 
     67            return 0 
     68        return -1 
     69 
     70    def __str__(self): 
     71        s = self.scheme + ':' 
     72        if self.user: s += self.user + '@' 
     73        s += self.host 
     74        if self.port: s+= ':' + self.port 
     75        return s 
     76 
    7677class Chat(gui.ChatObserver): 
    77         def __init__(self, app, acc, uri, call_inst=None): 
    78                 self._app = app 
    79                 self._acc = acc 
    80                 self.title = '' 
    81                  
    82                 global ConfIdx 
    83                 self.confIdx = ConfIdx 
    84                 ConfIdx += 1 
    85                  
    86                 # each participant call/buddy instances are stored in call list 
    87                 # and buddy list with same index as in particpant list 
    88                 self._participantList = []      # list of SipUri 
    89                 self._callList = []             # list of Call 
    90                 self._buddyList = []            # list of Buddy 
    91                  
    92                 self._gui = gui.ChatFrame(self) 
    93                 self.addParticipant(uri, call_inst) 
    94          
    95         def _updateGui(self): 
    96                 if self.isPrivate(): 
    97                         self.title = str(self._participantList[0]) 
    98                 else: 
    99                         self.title = 'Conference #%d (%d participants)' % (self.confIdx, len(self._participantList)) 
    100                 self._gui.title(self.title) 
    101                 self._app.updateWindowMenu() 
    102                  
    103         def _getCallFromUriStr(self, uri_str, op = ''): 
    104                 uri = ParseSipUri(uri_str) 
    105                 if uri not in self._participantList: 
    106                         print "=== %s cannot find participant with URI '%s'" % (op, uri_str) 
    107                         return None 
    108                 idx = self._participantList.index(uri) 
    109                 if idx < len(self._callList): 
    110                         return self._callList[idx] 
    111                 return None 
    112          
    113         def _getActiveMediaIdx(self, thecall): 
    114                 ci = thecall.getInfo() 
    115                 for mi in ci.media: 
    116                         if mi.type == pj.PJMEDIA_TYPE_AUDIO and \ 
    117                           (mi.status != pj.PJSUA_CALL_MEDIA_NONE and \ 
    118                            mi.status != pj.PJSUA_CALL_MEDIA_ERROR): 
    119                                 return mi.index 
    120                 return -1 
    121                  
    122         def _getAudioMediaFromUriStr(self, uri_str): 
    123                 c = self._getCallFromUriStr(uri_str) 
    124                 if not c: return None 
    125  
    126                 idx = self._getActiveMediaIdx(c) 
    127                 if idx < 0: return None 
    128  
    129                 m = c.getMedia(idx) 
    130                 am = pj.AudioMedia.typecastFromMedia(m) 
    131                 return am 
    132                  
    133         def _sendTypingIndication(self, is_typing, sender_uri_str=''): 
    134                 sender_uri = ParseSipUri(sender_uri_str) if sender_uri_str else None 
    135                 type_ind_param = pj.SendTypingIndicationParam() 
    136                 type_ind_param.isTyping = is_typing 
    137                 for idx, p in enumerate(self._participantList): 
    138                         # don't echo back to the original sender 
    139                         if sender_uri and p == sender_uri: 
    140                                 continue 
    141                                  
    142                         # send via call, if any, or buddy 
    143                         target = None 
    144                         if self._callList[idx] and self._callList[idx].connected: 
    145                                 target = self._callList[idx] 
    146                         else: 
    147                                 target = self._buddyList[idx] 
    148                         assert(target) 
    149                                  
    150                         try: 
    151                                 target.sendTypingIndication(type_ind_param) 
    152                         except: 
    153                                 pass 
    154  
    155         def _sendInstantMessage(self, msg, sender_uri_str=''): 
    156                 sender_uri = ParseSipUri(sender_uri_str) if sender_uri_str else None 
    157                 send_im_param = pj.SendInstantMessageParam() 
    158                 send_im_param.content = str(msg) 
    159                 for idx, p in enumerate(self._participantList): 
    160                         # don't echo back to the original sender 
    161                         if sender_uri and p == sender_uri: 
    162                                 continue 
    163                                  
    164                         # send via call, if any, or buddy 
    165                         target = None 
    166                         if self._callList[idx] and self._callList[idx].connected: 
    167                                 target = self._callList[idx] 
    168                         else: 
    169                                 target = self._buddyList[idx] 
    170                         assert(target) 
    171                          
    172                         try: 
    173                                 target.sendInstantMessage(send_im_param) 
    174                         except: 
    175                                 # error will be handled via Account::onInstantMessageStatus() 
    176                                 pass 
    177  
    178         def isPrivate(self): 
    179                 return len(self._participantList) <= 1 
    180                  
    181         def isUriParticipant(self, uri): 
    182                 return uri in self._participantList 
    183                  
    184         def registerCall(self, uri_str, call_inst): 
    185                 uri = ParseSipUri(uri_str) 
    186                 try: 
    187                         idx = self._participantList.index(uri) 
    188                         bud = self._buddyList[idx] 
    189                         self._callList[idx] = call_inst 
    190                         call_inst.chat = self 
    191                         call_inst.peerUri = bud.cfg.uri 
    192                 except: 
    193                         assert(0) # idx must be found! 
    194                  
    195         def showWindow(self, show_text_chat = False): 
    196                 self._gui.bringToFront() 
    197                 if show_text_chat: 
    198                         self._gui.textShowHide(True) 
    199                  
    200         def addParticipant(self, uri, call_inst=None): 
    201                 # avoid duplication 
    202                 if self.isUriParticipant(uri): return 
    203                  
    204                 uri_str = str(uri) 
    205                  
    206                 # find buddy, create one if not found (e.g: for IM/typing ind), 
    207                 # it is a temporary one and not really registered to acc 
    208                 bud = None 
    209                 try: 
    210                         bud = self._acc.findBuddy(uri_str) 
    211                 except: 
    212                         bud = buddy.Buddy(None) 
    213                         bud_cfg = pj.BuddyConfig() 
    214                         bud_cfg.uri = uri_str 
    215                         bud_cfg.subscribe = False 
    216                         bud.create(self._acc, bud_cfg) 
    217                         bud.cfg = bud_cfg 
    218                         bud.account = self._acc 
    219                          
    220                 # update URI from buddy URI 
    221                 uri = ParseSipUri(bud.cfg.uri) 
    222                  
    223                 # add it 
    224                 self._participantList.append(uri) 
    225                 self._callList.append(call_inst) 
    226                 self._buddyList.append(bud) 
    227                 self._gui.addParticipant(str(uri)) 
    228                 self._updateGui() 
    229          
    230         def kickParticipant(self, uri): 
    231                 if (not uri) or (uri not in self._participantList): 
    232                         assert(0) 
    233                         return 
    234                  
    235                 idx = self._participantList.index(uri) 
    236                 del self._participantList[idx] 
    237                 del self._callList[idx] 
    238                 del self._buddyList[idx] 
    239                 self._gui.delParticipant(str(uri)) 
    240                  
    241                 if self._participantList: 
    242                         self._updateGui() 
    243                 else: 
    244                         self.onCloseWindow() 
    245                          
    246         def addMessage(self, from_uri_str, msg): 
    247                 if from_uri_str: 
    248                         # print message on GUI 
    249                         msg = from_uri_str + ': ' + msg 
    250                         self._gui.textAddMessage(msg) 
    251                         # now relay to all participants 
    252                         self._sendInstantMessage(msg, from_uri_str) 
    253                 else: 
    254                         self._gui.textAddMessage(msg, False) 
    255                          
    256         def setTypingIndication(self, from_uri_str, is_typing): 
    257                 # notify GUI 
    258                 self._gui.textSetTypingIndication(from_uri_str, is_typing) 
    259                 # now relay to all participants 
    260                 self._sendTypingIndication(is_typing, from_uri_str) 
    261                  
    262         def startCall(self): 
    263                 self._gui.enableAudio() 
    264                 call_param = pj.CallOpParam() 
    265                 call_param.opt.audioCount = 1 
    266                 call_param.opt.videoCount = 0 
    267                 fails = [] 
    268                 for idx, p in enumerate(self._participantList): 
    269                         # just skip if call is instantiated 
    270                         if self._callList[idx]: 
    271                                 continue 
    272                          
    273                         uri_str = str(p) 
    274                         c = call.Call(self._acc, uri_str, self) 
    275                         self._callList[idx] = c 
    276                         self._gui.audioUpdateState(uri_str, gui.AudioState.INITIALIZING) 
    277                          
    278                         try: 
    279                                 c.makeCall(uri_str, call_param) 
    280                         except: 
    281                                 self._callList[idx] = None 
    282                                 self._gui.audioUpdateState(uri_str, gui.AudioState.FAILED) 
    283                                 fails.append(p) 
    284                                  
    285                 for p in fails: 
    286                         # kick participants with call failure, but spare the last (avoid zombie chat) 
    287                         if not self.isPrivate(): 
    288                                 self.kickParticipant(p) 
    289                          
    290         def stopCall(self): 
    291                 for idx, p in enumerate(self._participantList): 
    292                         self._gui.audioUpdateState(str(p), gui.AudioState.DISCONNECTED) 
    293                         c = self._callList[idx] 
    294                         if c: 
    295                                 c.hangup(pj.CallOpParam()) 
    296  
    297         def updateCallState(self, thecall, info = None): 
    298                 # info is optional here, just to avoid calling getInfo() twice (in the caller and here) 
    299                 if not info: info = thecall.getInfo() 
    300                  
    301                 if info.state < pj.PJSIP_INV_STATE_CONFIRMED: 
    302                         self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.INITIALIZING) 
    303                 elif info.state == pj.PJSIP_INV_STATE_CONFIRMED: 
    304                         self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.CONNECTED) 
    305                         if not self.isPrivate(): 
    306                                 # inform peer about conference participants 
    307                                 conf_welcome_str  = '\n---\n' 
    308                                 conf_welcome_str += 'Welcome to the conference, participants:\n' 
    309                                 conf_welcome_str += '%s (host)\n' % (self._acc.cfg.idUri) 
    310                                 for p in self._participantList: 
    311                                         conf_welcome_str += '%s\n' % (str(p)) 
    312                                 conf_welcome_str += '---\n' 
    313                                 send_im_param = pj.SendInstantMessageParam() 
    314                                 send_im_param.content = conf_welcome_str 
    315                                 try: 
    316                                         thecall.sendInstantMessage(send_im_param) 
    317                                 except: 
    318                                         pass 
    319                                          
    320                                 # inform others, including self 
    321                                 msg = "[Conf manager] %s has joined" % (thecall.peerUri) 
    322                                 self.addMessage(None, msg) 
    323                                 self._sendInstantMessage(msg, thecall.peerUri) 
    324                                  
    325                 elif info.state == pj.PJSIP_INV_STATE_DISCONNECTED: 
    326                         if info.lastStatusCode/100 != 2: 
    327                                 self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.FAILED) 
    328                         else: 
    329                                 self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.DISCONNECTED) 
    330                          
    331                         # reset entry in the callList 
    332                         try: 
    333                                 idx = self._callList.index(thecall) 
    334                                 if idx >= 0: self._callList[idx] = None 
    335                         except: 
    336                                 pass 
    337                          
    338                         self.addMessage(None, "Call to '%s' disconnected: %s" % (thecall.peerUri, info.lastReason)) 
    339                          
    340                         # kick the disconnected participant, but the last (avoid zombie chat) 
    341                         if not self.isPrivate(): 
    342                                 self.kickParticipant(ParseSipUri(thecall.peerUri)) 
    343                                  
    344                                 # inform others, including self 
    345                                 msg = "[Conf manager] %s has left" % (thecall.peerUri) 
    346                                 self.addMessage(None, msg) 
    347                                 self._sendInstantMessage(msg, thecall.peerUri) 
    348  
    349         def updateCallMediaState(self, thecall, info = None): 
    350                 # info is optional here, just to avoid calling getInfo() twice (in the caller and here) 
    351                 if not info: info = thecall.getInfo() 
    352                  
    353                 med_idx = self._getActiveMediaIdx(thecall) 
    354                 if (med_idx < 0): 
    355                         self._gui.audioSetStatsText(thecall.peerUri, 'No active media') 
    356                         return 
    357  
    358                 si = thecall.getStreamInfo(med_idx) 
    359                 dir_str = '' 
    360                 if si.dir == 0: 
    361                         dir_str = 'inactive' 
    362                 else: 
    363                         if si.dir & pj.PJMEDIA_DIR_ENCODING: 
    364                                 dir_str += 'send ' 
    365                         if si.dir & pj.PJMEDIA_DIR_DECODING: 
    366                                 dir_str += 'receive ' 
    367                 stats_str  = "Direction   : %s\n" % (dir_str) 
    368                 stats_str += "Audio codec : %s (%sHz)" % (si.codecName, si.codecClockRate) 
    369                 self._gui.audioSetStatsText(thecall.peerUri, stats_str) 
    370                 m = pj.AudioMedia.typecastFromMedia(thecall.getMedia(med_idx)) 
    371                  
    372                 # make conference 
    373                 for c in self._callList: 
    374                         if c == thecall: 
    375                                 continue 
    376                         med_idx = self._getActiveMediaIdx(c) 
    377                         if med_idx < 0: 
    378                                 continue 
    379                         mm = pj.AudioMedia.typecastFromMedia(c.getMedia(med_idx)) 
    380                         m.startTransmit(mm) 
    381                         mm.startTransmit(m) 
    382  
    383                          
    384         # ** callbacks from GUI (ChatObserver implementation) ** 
    385          
    386         # Text 
    387         def onSendMessage(self, msg): 
    388                 self._sendInstantMessage(msg) 
    389  
    390         def onStartTyping(self): 
    391                 self._sendTypingIndication(True) 
    392                  
    393         def onStopTyping(self): 
    394                 self._sendTypingIndication(False) 
    395                  
    396         # Audio 
    397         def onHangup(self, peer_uri_str): 
    398                 c = self._getCallFromUriStr(peer_uri_str, "onHangup()") 
    399                 if not c: return 
    400                 call_param = pj.CallOpParam() 
    401                 c.hangup(call_param) 
    402  
    403         def onHold(self, peer_uri_str): 
    404                 c = self._getCallFromUriStr(peer_uri_str, "onHold()") 
    405                 if not c: return 
    406                 call_param = pj.CallOpParam() 
    407                 c.setHold(call_param) 
    408  
    409         def onUnhold(self, peer_uri_str): 
    410                 c = self._getCallFromUriStr(peer_uri_str, "onUnhold()") 
    411                 if not c: return 
    412                  
    413                 call_param = pj.CallOpParam() 
    414                 call_param.opt.audioCount = 1 
    415                 call_param.opt.videoCount = 0 
    416                 call_param.opt.flag |= pj.PJSUA_CALL_UNHOLD 
    417                 c.reinvite(call_param) 
    418                  
    419         def onRxMute(self, peer_uri_str, mute): 
    420                 am = self._getAudioMediaFromUriStr(peer_uri_str) 
    421                 if not am: return 
    422                 if mute: 
    423                         am.stopTransmit(ep.Endpoint.instance.audDevManager().getPlaybackDevMedia()) 
    424                         self.addMessage(None, "Muted audio from '%s'" % (peer_uri_str)) 
    425                 else: 
    426                         am.startTransmit(ep.Endpoint.instance.audDevManager().getPlaybackDevMedia()) 
    427                         self.addMessage(None, "Unmuted audio from '%s'" % (peer_uri_str)) 
    428                  
    429         def onRxVol(self, peer_uri_str, vol_pct): 
    430                 am = self._getAudioMediaFromUriStr(peer_uri_str) 
    431                 if not am: return 
    432                 # pjsua volume range = 0:mute, 1:no adjustment, 2:100% louder 
    433                 am.adjustRxLevel(vol_pct/50.0) 
    434                 self.addMessage(None, "Adjusted volume level audio from '%s'" % (peer_uri_str)) 
    435                          
    436         def onTxMute(self, peer_uri_str, mute): 
    437                 am = self._getAudioMediaFromUriStr(peer_uri_str) 
    438                 if not am: return 
    439                 if mute: 
    440                         ep.Endpoint.instance.audDevManager().getCaptureDevMedia().stopTransmit(am) 
    441                         self.addMessage(None, "Muted audio to '%s'" % (peer_uri_str)) 
    442                 else: 
    443                         ep.Endpoint.instance.audDevManager().getCaptureDevMedia().startTransmit(am) 
    444                         self.addMessage(None, "Unmuted audio to '%s'" % (peer_uri_str)) 
    445  
    446         # Chat room 
    447         def onAddParticipant(self): 
    448                 buds = [] 
    449                 dlg = AddParticipantDlg(None, self._app, buds) 
    450                 if dlg.doModal(): 
    451                         for bud in buds: 
    452                                 uri = ParseSipUri(bud.cfg.uri) 
    453                                 self.addParticipant(uri) 
    454                         if not self.isPrivate(): 
    455                                 self.startCall() 
    456                                  
    457         def onStartAudio(self): 
    458                 self.startCall() 
    459  
    460         def onStopAudio(self): 
    461                 self.stopCall() 
    462                  
    463         def onCloseWindow(self): 
    464                 self.stopCall() 
    465                 # will remove entry from list eventually destroy this chat? 
    466                 if self in self._acc.chatList: self._acc.chatList.remove(self) 
    467                 self._app.updateWindowMenu() 
    468                 # destroy GUI 
    469                 self._gui.destroy() 
     78    def __init__(self, app, acc, uri, call_inst=None): 
     79        self._app = app 
     80        self._acc = acc 
     81        self.title = '' 
     82 
     83        global ConfIdx 
     84        self.confIdx = ConfIdx 
     85        ConfIdx += 1 
     86 
     87        # each participant call/buddy instances are stored in call list 
     88        # and buddy list with same index as in particpant list 
     89        self._participantList = []      # list of SipUri 
     90        self._callList = []             # list of Call 
     91        self._buddyList = []            # list of Buddy 
     92 
     93        self._gui = gui.ChatFrame(self) 
     94        self.addParticipant(uri, call_inst) 
     95 
     96    def _updateGui(self): 
     97        if self.isPrivate(): 
     98            self.title = str(self._participantList[0]) 
     99        else: 
     100            self.title = 'Conference #%d (%d participants)' % (self.confIdx, len(self._participantList)) 
     101        self._gui.title(self.title) 
     102        self._app.updateWindowMenu() 
     103 
     104    def _getCallFromUriStr(self, uri_str, op = ''): 
     105        uri = ParseSipUri(uri_str) 
     106        if uri not in self._participantList: 
     107            write("=== "+ op +" cannot find participant with URI '" + uri_str + "'\r\n") 
     108            return None 
     109        idx = self._participantList.index(uri) 
     110        if idx < len(self._callList): 
     111            return self._callList[idx] 
     112        return None 
     113 
     114    def _getActiveMediaIdx(self, thecall): 
     115        ci = thecall.getInfo() 
     116        for mi in ci.media: 
     117            if mi.type == pj.PJMEDIA_TYPE_AUDIO and \ 
     118              (mi.status != pj.PJSUA_CALL_MEDIA_NONE and \ 
     119               mi.status != pj.PJSUA_CALL_MEDIA_ERROR): 
     120                return mi.index 
     121        return -1 
     122 
     123    def _getAudioMediaFromUriStr(self, uri_str): 
     124        c = self._getCallFromUriStr(uri_str) 
     125        if not c: return None 
     126 
     127        idx = self._getActiveMediaIdx(c) 
     128        if idx < 0: return None 
     129 
     130        m = c.getMedia(idx) 
     131        am = pj.AudioMedia.typecastFromMedia(m) 
     132        return am 
     133 
     134    def _sendTypingIndication(self, is_typing, sender_uri_str=''): 
     135        sender_uri = ParseSipUri(sender_uri_str) if sender_uri_str else None 
     136        type_ind_param = pj.SendTypingIndicationParam() 
     137        type_ind_param.isTyping = is_typing 
     138        for idx, p in enumerate(self._participantList): 
     139            # don't echo back to the original sender 
     140            if sender_uri and p == sender_uri: 
     141                continue 
     142 
     143            # send via call, if any, or buddy 
     144            target = None 
     145            if self._callList[idx] and self._callList[idx].connected: 
     146                target = self._callList[idx] 
     147            else: 
     148                target = self._buddyList[idx] 
     149            assert(target) 
     150 
     151            try: 
     152                target.sendTypingIndication(type_ind_param) 
     153            except: 
     154                pass 
     155 
     156    def _sendInstantMessage(self, msg, sender_uri_str=''): 
     157        sender_uri = ParseSipUri(sender_uri_str) if sender_uri_str else None 
     158        send_im_param = pj.SendInstantMessageParam() 
     159        send_im_param.content = str(msg) 
     160        for idx, p in enumerate(self._participantList): 
     161            # don't echo back to the original sender 
     162            if sender_uri and p == sender_uri: 
     163                continue 
     164 
     165            # send via call, if any, or buddy 
     166            target = None 
     167            if self._callList[idx] and self._callList[idx].connected: 
     168                target = self._callList[idx] 
     169            else: 
     170                target = self._buddyList[idx] 
     171            assert(target) 
     172 
     173            try: 
     174                target.sendInstantMessage(send_im_param) 
     175            except: 
     176                # error will be handled via Account::onInstantMessageStatus() 
     177                pass 
     178 
     179    def isPrivate(self): 
     180        return len(self._participantList) <= 1 
     181 
     182    def isUriParticipant(self, uri): 
     183        return uri in self._participantList 
     184 
     185    def registerCall(self, uri_str, call_inst): 
     186        uri = ParseSipUri(uri_str) 
     187        try: 
     188            idx = self._participantList.index(uri) 
     189            bud = self._buddyList[idx] 
     190            self._callList[idx] = call_inst 
     191            call_inst.chat = self 
     192            call_inst.peerUri = bud.cfg.uri 
     193        except: 
     194            assert(0) # idx must be found! 
     195 
     196    def showWindow(self, show_text_chat = False): 
     197        self._gui.bringToFront() 
     198        if show_text_chat: 
     199            self._gui.textShowHide(True) 
     200 
     201    def addParticipant(self, uri, call_inst=None): 
     202        # avoid duplication 
     203        if self.isUriParticipant(uri): return 
     204 
     205        uri_str = str(uri) 
     206 
     207        # find buddy, create one if not found (e.g: for IM/typing ind), 
     208        # it is a temporary one and not really registered to acc 
     209        bud = None 
     210        try: 
     211            bud = self._acc.findBuddy(uri_str) 
     212        except: 
     213            bud = buddy.Buddy(None) 
     214            bud_cfg = pj.BuddyConfig() 
     215            bud_cfg.uri = uri_str 
     216            bud_cfg.subscribe = False 
     217            bud.create(self._acc, bud_cfg) 
     218            bud.cfg = bud_cfg 
     219            bud.account = self._acc 
     220 
     221        # update URI from buddy URI 
     222        uri = ParseSipUri(bud.cfg.uri) 
     223 
     224        # add it 
     225        self._participantList.append(uri) 
     226        self._callList.append(call_inst) 
     227        self._buddyList.append(bud) 
     228        self._gui.addParticipant(str(uri)) 
     229        self._updateGui() 
     230 
     231    def kickParticipant(self, uri): 
     232        if (not uri) or (uri not in self._participantList): 
     233            assert(0) 
     234            return 
     235 
     236        idx = self._participantList.index(uri) 
     237        del self._participantList[idx] 
     238        del self._callList[idx] 
     239        del self._buddyList[idx] 
     240        self._gui.delParticipant(str(uri)) 
     241 
     242        if self._participantList: 
     243            self._updateGui() 
     244        else: 
     245            self.onCloseWindow() 
     246 
     247    def addMessage(self, from_uri_str, msg): 
     248        if from_uri_str: 
     249            # print message on GUI 
     250            msg = from_uri_str + ': ' + msg 
     251            self._gui.textAddMessage(msg) 
     252            # now relay to all participants 
     253            self._sendInstantMessage(msg, from_uri_str) 
     254        else: 
     255            self._gui.textAddMessage(msg, False) 
     256 
     257    def setTypingIndication(self, from_uri_str, is_typing): 
     258        # notify GUI 
     259        self._gui.textSetTypingIndication(from_uri_str, is_typing) 
     260        # now relay to all participants 
     261        self._sendTypingIndication(is_typing, from_uri_str) 
     262 
     263    def startCall(self): 
     264        self._gui.enableAudio() 
     265        call_param = pj.CallOpParam() 
     266        call_param.opt.audioCount = 1 
     267        call_param.opt.videoCount = 0 
     268        fails = [] 
     269        for idx, p in enumerate(self._participantList): 
     270            # just skip if call is instantiated 
     271            if self._callList[idx]: 
     272                continue 
     273 
     274            uri_str = str(p) 
     275            c = call.Call(self._acc, uri_str, self) 
     276            self._callList[idx] = c 
     277            self._gui.audioUpdateState(uri_str, gui.AudioState.INITIALIZING) 
     278 
     279            try: 
     280                c.makeCall(uri_str, call_param) 
     281            except: 
     282                self._callList[idx] = None 
     283                self._gui.audioUpdateState(uri_str, gui.AudioState.FAILED) 
     284                fails.append(p) 
     285 
     286        for p in fails: 
     287            # kick participants with call failure, but spare the last (avoid zombie chat) 
     288            if not self.isPrivate(): 
     289                self.kickParticipant(p) 
     290 
     291    def stopCall(self): 
     292        for idx, p in enumerate(self._participantList): 
     293            self._gui.audioUpdateState(str(p), gui.AudioState.DISCONNECTED) 
     294            c = self._callList[idx] 
     295            if c: 
     296                c.hangup(pj.CallOpParam()) 
     297 
     298    def updateCallState(self, thecall, info = None): 
     299        # info is optional here, just to avoid calling getInfo() twice (in the caller and here) 
     300        if not info: info = thecall.getInfo() 
     301 
     302        if info.state < pj.PJSIP_INV_STATE_CONFIRMED: 
     303            self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.INITIALIZING) 
     304        elif info.state == pj.PJSIP_INV_STATE_CONFIRMED: 
     305            self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.CONNECTED) 
     306            if not self.isPrivate(): 
     307                # inform peer about conference participants 
     308                conf_welcome_str  = '\n---\n' 
     309                conf_welcome_str += 'Welcome to the conference, participants:\n' 
     310                conf_welcome_str += '%s (host)\n' % (self._acc.cfg.idUri) 
     311                for p in self._participantList: 
     312                    conf_welcome_str += '%s\n' % (str(p)) 
     313                conf_welcome_str += '---\n' 
     314                send_im_param = pj.SendInstantMessageParam() 
     315                send_im_param.content = conf_welcome_str 
     316                try: 
     317                    thecall.sendInstantMessage(send_im_param) 
     318                except: 
     319                    pass 
     320 
     321                # inform others, including self 
     322                msg = "[Conf manager] %s has joined" % (thecall.peerUri) 
     323                self.addMessage(None, msg) 
     324                self._sendInstantMessage(msg, thecall.peerUri) 
     325 
     326        elif info.state == pj.PJSIP_INV_STATE_DISCONNECTED: 
     327            if info.lastStatusCode/100 != 2: 
     328                self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.FAILED) 
     329            else: 
     330                self._gui.audioUpdateState(thecall.peerUri, gui.AudioState.DISCONNECTED) 
     331 
     332            # reset entry in the callList 
     333            try: 
     334                idx = self._callList.index(thecall) 
     335                if idx >= 0: self._callList[idx] = None 
     336            except: 
     337                pass 
     338 
     339            self.addMessage(None, "Call to '%s' disconnected: %s" % (thecall.peerUri, info.lastReason)) 
     340 
     341            # kick the disconnected participant, but the last (avoid zombie chat) 
     342            if not self.isPrivate(): 
     343                self.kickParticipant(ParseSipUri(thecall.peerUri)) 
     344 
     345                # inform others, including self 
     346                msg = "[Conf manager] %s has left" % (thecall.peerUri) 
     347                self.addMessage(None, msg) 
     348                self._sendInstantMessage(msg, thecall.peerUri) 
     349 
     350    def updateCallMediaState(self, thecall, info = None): 
     351        # info is optional here, just to avoid calling getInfo() twice (in the caller and here) 
     352        if not info: info = thecall.getInfo() 
     353 
     354        med_idx = self._getActiveMediaIdx(thecall) 
     355        if (med_idx < 0): 
     356            self._gui.audioSetStatsText(thecall.peerUri, 'No active media') 
     357            return 
     358 
     359        si = thecall.getStreamInfo(med_idx) 
     360        dir_str = '' 
     361        if si.dir == 0: 
     362            dir_str = 'inactive' 
     363        else: 
     364            if si.dir & pj.PJMEDIA_DIR_ENCODING: 
     365                dir_str += 'send ' 
     366            if si.dir & pj.PJMEDIA_DIR_DECODING: 
     367                dir_str += 'receive ' 
     368        stats_str  = "Direction   : %s\n" % (dir_str) 
     369        stats_str += "Audio codec : %s (%sHz)" % (si.codecName, si.codecClockRate) 
     370        self._gui.audioSetStatsText(thecall.peerUri, stats_str) 
     371        m = pj.AudioMedia.typecastFromMedia(thecall.getMedia(med_idx)) 
     372 
     373        # make conference 
     374        for c in self._callList: 
     375            if c == thecall: 
     376                continue 
     377            med_idx = self._getActiveMediaIdx(c) 
     378            if med_idx < 0: 
     379                continue 
     380            mm = pj.AudioMedia.typecastFromMedia(c.getMedia(med_idx)) 
     381            m.startTransmit(mm) 
     382            mm.startTransmit(m) 
     383 
     384 
     385    # ** callbacks from GUI (ChatObserver implementation) ** 
     386 
     387    # Text 
     388    def onSendMessage(self, msg): 
     389        self._sendInstantMessage(msg) 
     390 
     391    def onStartTyping(self): 
     392        self._sendTypingIndication(True) 
     393 
     394    def onStopTyping(self): 
     395        self._sendTypingIndication(False) 
     396 
     397    # Audio 
     398    def onHangup(self, peer_uri_str): 
     399        c = self._getCallFromUriStr(peer_uri_str, "onHangup()") 
     400        if not c: return 
     401        call_param = pj.CallOpParam() 
     402        c.hangup(call_param) 
     403 
     404    def onHold(self, peer_uri_str): 
     405        c = self._getCallFromUriStr(peer_uri_str, "onHold()") 
     406        if not c: return 
     407        call_param = pj.CallOpParam() 
     408        c.setHold(call_param) 
     409 
     410    def onUnhold(self, peer_uri_str): 
     411        c = self._getCallFromUriStr(peer_uri_str, "onUnhold()") 
     412        if not c: return 
     413 
     414        call_param = pj.CallOpParam() 
     415        call_param.opt.audioCount = 1 
     416        call_param.opt.videoCount = 0 
     417        call_param.opt.flag |= pj.PJSUA_CALL_UNHOLD 
     418        c.reinvite(call_param) 
     419 
     420    def onRxMute(self, peer_uri_str, mute): 
     421        am = self._getAudioMediaFromUriStr(peer_uri_str) 
     422        if not am: return 
     423        if mute: 
     424            am.stopTransmit(ep.Endpoint.instance.audDevManager().getPlaybackDevMedia()) 
     425            self.addMessage(None, "Muted audio from '%s'" % (peer_uri_str)) 
     426        else: 
     427            am.startTransmit(ep.Endpoint.instance.audDevManager().getPlaybackDevMedia()) 
     428            self.addMessage(None, "Unmuted audio from '%s'" % (peer_uri_str)) 
     429 
     430    def onRxVol(self, peer_uri_str, vol_pct): 
     431        am = self._getAudioMediaFromUriStr(peer_uri_str) 
     432        if not am: return 
     433        # pjsua volume range = 0:mute, 1:no adjustment, 2:100% louder 
     434        am.adjustRxLevel(vol_pct/50.0) 
     435        self.addMessage(None, "Adjusted volume level audio from '%s'" % (peer_uri_str)) 
     436 
     437    def onTxMute(self, peer_uri_str, mute): 
     438        am = self._getAudioMediaFromUriStr(peer_uri_str) 
     439        if not am: return 
     440        if mute: 
     441            ep.Endpoint.instance.audDevManager().getCaptureDevMedia().stopTransmit(am) 
     442            self.addMessage(None, "Muted audio to '%s'" % (peer_uri_str)) 
     443        else: 
     444            ep.Endpoint.instance.audDevManager().getCaptureDevMedia().startTransmit(am) 
     445            self.addMessage(None, "Unmuted audio to '%s'" % (peer_uri_str)) 
     446 
     447    # Chat room 
     448    def onAddParticipant(self): 
     449        buds = [] 
     450        dlg = AddParticipantDlg(None, self._app, buds) 
     451        if dlg.doModal(): 
     452            for bud in buds: 
     453                uri = ParseSipUri(bud.cfg.uri) 
     454                self.addParticipant(uri) 
     455            if not self.isPrivate(): 
     456                self.startCall() 
     457 
     458    def onStartAudio(self): 
     459        self.startCall() 
     460 
     461    def onStopAudio(self): 
     462        self.stopCall() 
     463 
     464    def onCloseWindow(self): 
     465        self.stopCall() 
     466        # will remove entry from list eventually destroy this chat? 
     467        if self in self._acc.chatList: self._acc.chatList.remove(self) 
     468        self._app.updateWindowMenu() 
     469        # destroy GUI 
     470        self._gui.destroy() 
    470471 
    471472 
    472473class AddParticipantDlg(tk.Toplevel): 
    473         """ 
    474         List of buddies 
    475         """ 
    476         def __init__(self, parent, app, bud_list): 
    477                 tk.Toplevel.__init__(self, parent) 
    478                 self.title('Add participants..') 
    479                 self.transient(parent) 
    480                 self.parent = parent 
    481                 self._app = app 
    482                 self.buddyList = bud_list 
    483                  
    484                 self.isOk = False 
    485                  
    486                 self.createWidgets() 
    487          
    488         def doModal(self): 
    489                 if self.parent: 
    490                         self.parent.wait_window(self) 
    491                 else: 
    492                         self.wait_window(self) 
    493                 return self.isOk 
    494                  
    495         def createWidgets(self): 
    496                 # buddy list 
    497                 list_frame = ttk.Frame(self) 
    498                 list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=1, padx=20, pady=20) 
    499                 #scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=list_frame.yview) 
    500                 #list_frame.config(yscrollcommand=scrl.set) 
    501                 #scrl.pack(side=tk.RIGHT, fill=tk.Y) 
    502                  
    503                 # draw buddy list 
    504                 self.buddies = [] 
    505                 for acc in self._app.accList: 
    506                         self.buddies.append((0, acc.cfg.idUri)) 
    507                         for bud in acc.buddyList: 
    508                                 self.buddies.append((1, bud)) 
    509                  
    510                 self.bud_var = [] 
    511                 for idx,(flag,bud) in enumerate(self.buddies): 
    512                         self.bud_var.append(tk.IntVar()) 
    513                         if flag==0: 
    514                                 s = ttk.Separator(list_frame, orient=tk.HORIZONTAL) 
    515                                 s.pack(fill=tk.X) 
    516                                 l = tk.Label(list_frame, anchor=tk.W, text="Account '%s':" % (bud)) 
    517                                 l.pack(fill=tk.X) 
    518                         else: 
    519                                 c = tk.Checkbutton(list_frame, anchor=tk.W, text=bud.cfg.uri, variable=self.bud_var[idx]) 
    520                                 c.pack(fill=tk.X) 
    521                 s = ttk.Separator(list_frame, orient=tk.HORIZONTAL) 
    522                 s.pack(fill=tk.X) 
    523  
    524                 # Ok/cancel buttons 
    525                 tail_frame = ttk.Frame(self) 
    526                 tail_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) 
    527                  
    528                 btnOk = ttk.Button(tail_frame, text='Ok', default=tk.ACTIVE, command=self.onOk) 
    529                 btnOk.pack(side=tk.LEFT, padx=20, pady=10) 
    530                 btnCancel = ttk.Button(tail_frame, text='Cancel', command=self.onCancel) 
    531                 btnCancel.pack(side=tk.RIGHT, padx=20, pady=10) 
    532                  
    533         def onOk(self): 
    534                 self.buddyList[:] = [] 
    535                 for idx,(flag,bud) in enumerate(self.buddies): 
    536                         if not flag: continue 
    537                         if self.bud_var[idx].get() and not (bud in self.buddyList): 
    538                                 self.buddyList.append(bud) 
    539                          
    540                 self.isOk = True 
    541                 self.destroy() 
    542                  
    543         def onCancel(self): 
    544                 self.destroy() 
     474    """ 
     475    List of buddies 
     476    """ 
     477    def __init__(self, parent, app, bud_list): 
     478        tk.Toplevel.__init__(self, parent) 
     479        self.title('Add participants..') 
     480        self.transient(parent) 
     481        self.parent = parent 
     482        self._app = app 
     483        self.buddyList = bud_list 
     484 
     485        self.isOk = False 
     486 
     487        self.createWidgets() 
     488 
     489    def doModal(self): 
     490        if self.parent: 
     491            self.parent.wait_window(self) 
     492        else: 
     493            self.wait_window(self) 
     494        return self.isOk 
     495 
     496    def createWidgets(self): 
     497        # buddy list 
     498        list_frame = ttk.Frame(self) 
     499        list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=1, padx=20, pady=20) 
     500        #scrl = ttk.Scrollbar(self, orient=tk.VERTICAL, command=list_frame.yview) 
     501        #list_frame.config(yscrollcommand=scrl.set) 
     502        #scrl.pack(side=tk.RIGHT, fill=tk.Y) 
     503 
     504        # draw buddy list 
     505        self.buddies = [] 
     506        for acc in self._app.accList: 
     507            self.buddies.append((0, acc.cfg.idUri)) 
     508            for bud in acc.buddyList: 
     509                self.buddies.append((1, bud)) 
     510 
     511        self.bud_var = [] 
     512        for idx,(flag,bud) in enumerate(self.buddies): 
     513            self.bud_var.append(tk.IntVar()) 
     514            if flag==0: 
     515                s = ttk.Separator(list_frame, orient=tk.HORIZONTAL) 
     516                s.pack(fill=tk.X) 
     517                l = tk.Label(list_frame, anchor=tk.W, text="Account '%s':" % (bud)) 
     518                l.pack(fill=tk.X) 
     519            else: 
     520                c = tk.Checkbutton(list_frame, anchor=tk.W, text=bud.cfg.uri, variable=self.bud_var[idx]) 
     521                c.pack(fill=tk.X) 
     522        s = ttk.Separator(list_frame, orient=tk.HORIZONTAL) 
     523        s.pack(fill=tk.X) 
     524 
     525        # Ok/cancel buttons 
     526        tail_frame = ttk.Frame(self) 
     527        tail_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) 
     528 
     529        btnOk = ttk.Button(tail_frame, text='Ok', default=tk.ACTIVE, command=self.onOk) 
     530        btnOk.pack(side=tk.LEFT, padx=20, pady=10) 
     531        btnCancel = ttk.Button(tail_frame, text='Cancel', command=self.onCancel) 
     532        btnCancel.pack(side=tk.RIGHT, padx=20, pady=10) 
     533 
     534    def onOk(self): 
     535        self.buddyList[:] = [] 
     536        for idx,(flag,bud) in enumerate(self.buddies): 
     537            if not flag: continue 
     538            if self.bud_var[idx].get() and not (bud in self.buddyList): 
     539                self.buddyList.append(bud) 
     540 
     541        self.isOk = True 
     542        self.destroy() 
     543 
     544    def onCancel(self): 
     545        self.destroy() 
Note: See TracChangeset for help on using the changeset viewer.