Changeset 5638 for pjproject/trunk/pjsip-apps/src/pygui/chat.py
- Timestamp:
- Aug 2, 2017 9:45:09 AM (6 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
pjproject/trunk/pjsip-apps/src/pygui/chat.py
r4757 r5638 21 21 import sys 22 22 if sys.version_info[0] >= 3: # Python 3 23 24 23 import tkinter as tk 24 from tkinter import ttk 25 25 else: 26 27 26 import Tkinter as tk 27 import ttk 28 28 29 29 import buddy … … 36 36 SipUriRegex = re.compile('(sip|sips):([^:;>\@]*)@?([^:;>]*):?([^:;>]*)') 37 37 ConfIdx = 1 38 write=sys.stdout.write 38 39 39 40 # Simple SIP uri parser, input URI must have been validated 40 41 def ParseSipUri(sip_uri_str): 41 42 43 44 45 46 47 48 49 50 51 52 53 54 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 56 57 class SipUri: 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 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 76 77 class Chat(gui.ChatObserver): 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 print "=== %s cannot find participant with URI '%s'" % (op, uri_str)107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 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() 470 471 471 472 472 473 class AddParticipantDlg(tk.Toplevel): 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 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.