1# New API for making it easier to write Jingle tests. The idea 2# is not so much to hide away the details (this makes tests 3# unreadable), but to make the expressions denser and more concise. 4# Helper classes support different dialects so the test can 5# be invoked for different (possibly all) dialects. 6 7from functools import partial 8from twisted.words.xish import domish, xpath 9import random 10from gabbletest import sync_stream, exec_test 11from servicetest import EventPattern 12import dbus 13import ns 14import os 15import constants as cs 16 17class JingleProtocol(object): 18 """ 19 Defines a simple DSL for constructing Jingle messages. 20 """ 21 22 def __init__(self, dialect): 23 self.dialect = dialect 24 self.id_seq = 0 25 26 def _simple_xml(self, node): 27 "Construct domish.Element tree from tree of tuples" 28 name, namespace, attribs, children = node 29 el = domish.Element((namespace, name)) 30 for key, val in attribs.items(): 31 el[key] = val 32 for c in children: 33 if isinstance(c, tuple): 34 el.addChild(self._simple_xml(c)) 35 elif isinstance(c, unicode): 36 el.addContent(c) 37 else: 38 raise ValueError("invalid child object %r of type %r" % (c, type(c))) 39 return el 40 41 def xml(self, node): 42 "Returns XML from tree of tuples" 43 return self._simple_xml(node).toXml() 44 45 def Iq(self, type, id, frm, to, children): 46 "Creates an IQ element" 47 if not id: 48 id = 'seq%d' % self.id_seq 49 self.id_seq += 1 50 51 return ('iq', 'jabber:client', 52 { 'type': type, 'from': frm, 'to': to, 'id': id }, 53 children) 54 55 def SetIq(self, frm, to, children): 56 "Creates a set IQ element" 57 return self.Iq('set', None, frm, to, children) 58 59 def ResultIq(self, to, iq, children): 60 "Creates a result IQ element" 61 return self.Iq('result', iq['id'], iq['to'], to, 62 children) 63 64 def ErrorIq(self, iq, errtype, errchild): 65 "Creates an error IQ element, and includes the original stanza" 66 return self.Iq('error', iq['id'], iq['to'], iq['from'], 67 [ iq.firstChildElement(), 68 ('error', None, { 'type': errtype, 'xmlns': ns.STANZA, }, 69 [ errchild ]) ]) 70 71 def PayloadType(self, name, rate, id, parameters={}, **kw): 72 "Creates a <payload-type> element" 73 kw['name'] = name 74 kw['rate'] = rate 75 kw['id'] = id 76 children = [self.Parameter(name, value) 77 for name, value in parameters.iteritems()] 78 return ('payload-type', None, kw, children) 79 80 def Parameter(self, name, value): 81 "Creates a <parameter> element" 82 return ('parameter', None, {'name': name, 'value': value}, []) 83 84 def TransportGoogleP2PCall (self, username, password, call_remote_candidates=[]): 85 candidates = [] 86 for (component, host, port, props) in call_remote_candidates: 87 88 candidates.append(("candidate", None, { 89 "name": "rtp", 90 "address": host, 91 "port": str(port), 92 "protocol": "udp", 93 "preference": str(props["priority"] / 65536.0), 94 "type": ["INVALID NONE", "local", "stun", "INVALID PEER RFLX", "relay"][props["type"]], 95 "network": "0", 96 "generation": "0",# Increment this yourself if you care. 97 "component": str(component), # 1 is rtp, 2 is rtcp 98 "username": props.get("username", username), 99 "password": props.get("password", password), 100 }, [])) #NOTE: subtype and profile are unused 101 return ('transport', ns.GOOGLE_P2P, {}, candidates) 102 103 def TransportGoogleP2P(self, remote_transports=[]): 104 """ 105 Creates a <transport> element for Google P2P transport. 106 If remote_transports is present, and of the form 107 [(host, port, proto, subtype, profile, pref, transtype, user, pwd)] 108 (basically a list of Media_Stream_Handler_Transport without the 109 component number) then it will be converted to xml and added. 110 """ 111 candidates = [] 112 for i, (host, port, proto, subtype, profile, pref, transtype, user, pwd 113 ) in enumerate(remote_transports): 114 candidates.append(("candidate", None, { 115 "name": "rtp", 116 "address": host, 117 "port": str(port), 118 "protocol": ["udp", "tcp"][proto], 119 "preference": str(pref), 120 "type": ["local", "stun", "relay"][transtype], 121 "network": "0", 122 "generation": "0",# Increment this yourself if you care. 123 "component": "1", # 1 is rtp, 2 is rtcp 124 "username": user, 125 "password": pwd, 126 }, [])) #NOTE: subtype and profile are unused 127 return ('transport', ns.GOOGLE_P2P, {}, candidates) 128 129 def TransportIceUdp(self, remote_transports=[]): 130 """ 131 Creates a <transport> element for ICE-UDP transport. 132 If remote_transports is present, and of the form 133 [(host, port, proto, subtype, profile, pref, transtype, user, pwd)] 134 (basically a list of Media_Stream_Handler_Transport without the 135 component number) then it will be converted to xml and added. 136 """ 137 candidates = [] 138 attrs = {} 139 for (host, port, proto, subtype, profile, pref, transtype, user, pwd 140 ) in remote_transports: 141 if "ufrag" not in attrs: 142 attrs = {"ufrag": user, "pwd": pwd} 143 else: 144 assert (user == attrs["ufrag"] and pwd == attrs["pwd"] 145 ), "user and pwd should be the same across all candidates." 146 147 node = ("candidate", None, { 148 # ICE-CORE says it can be arbitrary string, even though XEP 149 # gives an int as an example. 150 "foundation": "fake", 151 "ip": host, 152 "port": str(port), 153 "protocol": ["udp", "tcp"][proto], 154 # Gabble multiplies by 65536 so we should too. 155 "priority": str(int(pref * 65536)), 156 "type": ["host", "srflx", "srflx"][transtype], 157 "network": "0", 158 "generation": "0",# Increment this yourself if you care. 159 "component": "1", # 1 is rtp, 2 is rtcp 160 }, []) #NOTE: subtype and profile are unused 161 candidates.append(node) 162 return ('transport', ns.JINGLE_TRANSPORT_ICEUDP, attrs, candidates) 163 164 def Presence(self, frm, to, caps): 165 "Creates <presence> stanza with specified capabilities" 166 children = [] 167 if caps: 168 children = [ ('c', ns.CAPS, caps, []) ] 169 return ('presence', 'jabber:client', { 'from': frm, 'to': to }, 170 children) 171 172 def Query(self, node, xmlns, children): 173 "Creates <query> element" 174 attrs = {} 175 if node: 176 attrs['node'] = node 177 return ('query', xmlns, attrs, children) 178 179 def Feature(self, var): 180 "Creates <feature> element" 181 return ('feature', None, { 'var': var }, []) 182 183 def action_predicate(self, action): 184 def f(e): 185 return self.match_jingle_action(e.query, action) 186 187 return f 188 189 def match_jingle_action(self, q, action): 190 return q is not None and q.name == 'jingle' and q['action'] == action 191 192 def _extract_session_id(self, query): 193 return query['sid'] 194 195 def validate_session_initiate(self, query): 196 raise NotImplementedError() 197 198 def can_do_video(self): 199 return True 200 201 def can_do_video_only(self): 202 return self.can_do_video() 203 204 def separate_contents(self): 205 return True 206 207 def has_mutable_streams(self): 208 return True 209 210 def is_modern_jingle(self): 211 return False 212 213 def rtp_info_event(self, name): 214 return None 215 216 def rtp_info_event_list(self, name): 217 e = self.rtp_info_event(name) 218 return [e] if e is not None else [] 219 220 221class GtalkProtocol03(JingleProtocol): 222 features = [ ns.GOOGLE_FEAT_VOICE, ns.GOOGLE_FEAT_VIDEO ] 223 224 def __init__(self): 225 JingleProtocol.__init__(self, 'gtalk-v0.3') 226 227 def _action_map(self, action): 228 map = { 229 'session-initiate': 'initiate', 230 'session-terminate': 'terminate', 231 'session-accept': 'accept', 232 'transport-info': 'candidates' 233 } 234 235 if action in map: 236 return map[action] 237 else: 238 return action 239 240 def Jingle(self, sid, initiator, action, children): 241 action = self._action_map(action) 242 return ('session', ns.GOOGLE_SESSION, 243 { 'type': action, 'initiator': initiator, 'id': sid }, children) 244 245 def PayloadType(self, name, rate, id, parameters={}, **kw): 246 p = JingleProtocol.PayloadType(self, name, rate, id, parameters, 247 **kw) 248 if "type" in kw: 249 namespaces = { "audio": ns.GOOGLE_SESSION_PHONE, 250 "video": ns.GOOGLE_SESSION_VIDEO, 251 } 252 p = p[:1] + (namespaces[kw["type"]],) + p[2:] 253 254 return p 255 256 # Gtalk has only one content, and <content> node is implicit. Also it 257 # never mixes payloads and transport information. It's up to the call of 258 # this function to ensure it never calls it with both mixed 259 def Content(self, name, creator, senders=None, 260 description=None, transport=None): 261 # Normally <content> has <description> and <transport>, but we only 262 # use <description> unless <transport> has candidates. 263 assert description == None or len(transport[3]) == 0 264 265 if description != None: 266 return description 267 else: 268 assert len(transport[3]) == 1, \ 269 "gtalk 0.3 only lets you send one candidate at a time." \ 270 "You sent %r" % [transport] 271 return transport[3][0] 272 273 def Description(self, type, children): 274 if type == 'audio': 275 namespace = ns.GOOGLE_SESSION_PHONE 276 elif type == 'video': 277 namespace = ns.GOOGLE_SESSION_VIDEO 278 else: 279 namespace = 'unexistent-namespace' 280 return ('description', namespace, {}, children) 281 282 def match_jingle_action(self, q, action): 283 action = self._action_map(action) 284 return q is not None and q.name == 'session' and q['type'] == action 285 286 def _extract_session_id(self, query): 287 return query['id'] 288 289 def can_do_video_only(self): 290 return False 291 292 def validate_session_initiate(self, query): 293 sid = self._extract_session_id(query) 294 295 # No transport in GTalk03 296 assert xpath.queryForNodes('/session/transport', query) == None 297 298 # Exactly one description in Gtalk03 299 descs = xpath.queryForNodes('/session/description', query) 300 assert len(descs) == 1 301 302 desc = descs[0] 303 304 # the ds is either audio or video 305 assert desc.uri in [ ns.GOOGLE_SESSION_PHONE, ns.GOOGLE_SESSION_VIDEO ] 306 307 if desc.uri == ns.GOOGLE_SESSION_VIDEO: 308 # If it's a video call there should be some audio codecs as well 309 assert xpath.queryForNodes( 310 '/session/description/payload-type[@xmlns="%s"]' % 311 ns.GOOGLE_SESSION_PHONE, query) 312 return (sid, ['fake-audio'], ['fake-video']) 313 else: 314 return (sid, ['fake-audio'], []) 315 316 def separate_contents(self): 317 return False 318 319 def has_mutable_streams(self): 320 return False 321 322class GtalkProtocol04(JingleProtocol): 323 features = [ ns.GOOGLE_FEAT_VOICE, ns.GOOGLE_P2P ] 324 325 def __init__(self): 326 JingleProtocol.__init__(self, 'gtalk-v0.4') 327 328 def _action_map(self, action): 329 map = { 330 'session-initiate': 'initiate', 331 'session-terminate': 'terminate', 332 'session-accept': 'accept', 333 } 334 335 if action in map: 336 return map[action] 337 else: 338 return action 339 340 def Jingle(self, sid, initiator, action, children): 341 # ignore Content and go straight for its children 342 if len(children) == 1 and children[0][0] == 'dummy-content': 343 # Either have just a transport or a description + transport 344 # without candidates 345 children = children[0][3] 346 347 action = self._action_map(action) 348 return ('session', ns.GOOGLE_SESSION, 349 { 'type': action, 'initiator': initiator, 'id': sid }, children) 350 351 # hacky: parent Jingle node should just pick up our children 352 def Content(self, name, creator, senders=None, 353 description=None, transport=None): 354 return ('dummy-content', None, {}, 355 [node for node in [description, transport] if node != None]) 356 357 def Description(self, type, children): 358 return ('description', ns.GOOGLE_SESSION_PHONE, {}, children) 359 360 def match_jingle_action(self, q, action): 361 action = self._action_map(action) 362 return q is not None and q.name == 'session' and q['type'] == action 363 364 def _extract_session_id(self, query): 365 return query['id'] 366 367 def validate_session_initiate(self, query): 368 # FIXME: validate it! 369 return (self._extract_session_id(query), ['fake-audio'], []) 370 371 def can_do_video(self): 372 return False 373 374class JingleProtocol015(JingleProtocol): 375 features = [ ns.GOOGLE_P2P, ns.JINGLE_015, ns.JINGLE_015_AUDIO, 376 ns.JINGLE_015_VIDEO ] 377 378 def __init__(self): 379 JingleProtocol.__init__(self, 'jingle-v0.15') 380 381 def Jingle(self, sid, initiator, action, children): 382 return ('jingle', ns.JINGLE_015, 383 { 'action': action, 'initiator': initiator, 'sid': sid }, children) 384 385 # Note: senders weren't mandatory in this dialect 386 def Content(self, name, creator, senders = None, 387 description=None, transport=None): 388 attribs = { 'name': name, 'creator': creator } 389 if senders: 390 attribs['senders'] = senders 391 return ('content', None, attribs, 392 [node for node in [description, transport] if node != None]) 393 394 def Description(self, type, children): 395 if type == 'audio': 396 namespace = ns.JINGLE_015_AUDIO 397 elif type == 'video': 398 namespace = ns.JINGLE_015_VIDEO 399 else: 400 namespace = 'unexistent-namespace' 401 return ('description', namespace, { 'type': type }, children) 402 403 def validate_session_initiate(self, query): 404 contents = xpath.queryForNodes( 405 '/jingle[@xmlns="%s"]/content' % ns.JINGLE_015, 406 query) 407 408 audio, video = [], [] 409 410 for c in contents: 411 a_desc = xpath.queryForNodes( 412 '/content/description[@xmlns="%s"]' % ns.JINGLE_015_AUDIO, 413 c) 414 v_desc = xpath.queryForNodes( 415 '/content/description[@xmlns="%s"]' % ns.JINGLE_015_VIDEO, 416 c) 417 418 if a_desc is not None: 419 assert len(a_desc) == 1, c.toXml() 420 assert v_desc is None 421 audio.append(c['name']) 422 elif v_desc is not None: 423 assert len(v_desc) == 1, c.toXml() 424 assert a_desc is None 425 video.append(c['name']) 426 else: 427 assert False, c.toXml() 428 429 assert len(audio) + len(video) > 0, query.toXml() 430 431 return (self._extract_session_id(query), audio, video) 432 433class JingleProtocol031(JingleProtocol): 434 features = [ ns.JINGLE, ns.JINGLE_RTP, ns.JINGLE_RTP_AUDIO, 435 ns.JINGLE_RTP_VIDEO, ns.GOOGLE_P2P ] 436 437 def __init__(self): 438 JingleProtocol.__init__(self, 'jingle-v0.31') 439 440 def Jingle(self, sid, initiator, action, children): 441 return ('jingle', ns.JINGLE, 442 { 'action': action, 'initiator': initiator, 'sid': sid }, children) 443 444 def Content(self, name, creator, senders=None, 445 description=None, transport=None): 446 if not senders: 447 senders = 'both' 448 return ('content', None, 449 { 'name': name, 'creator': creator, 'senders': senders }, 450 [node for node in [description, transport] if node != None]) 451 452 def Description(self, type, children): 453 return ('description', ns.JINGLE_RTP, { 'media': type }, children) 454 455 def is_modern_jingle(self): 456 return True 457 458 def rtp_info_event(self, name): 459 def p(e): 460 query = e.query 461 if not self.match_jingle_action(query, 'session-info'): 462 return False 463 n = query.firstChildElement() 464 return n is not None and n.uri == ns.JINGLE_RTP_INFO_1 and \ 465 n.name == name 466 467 return EventPattern('stream-iq', predicate=p) 468 469 def validate_session_initiate(self, query): 470 contents = xpath.queryForNodes( 471 '/jingle[@xmlns="%s"]/content' % ns.JINGLE, 472 query) 473 474 audio, video = [], [] 475 476 for c in contents: 477 descs = xpath.queryForNodes( 478 '/content/description[@xmlns="%s"]' % ns.JINGLE_RTP, 479 c) 480 481 assert len(descs) == 1, c.toXml() 482 483 d = descs[0] 484 485 if d['media'] == 'audio': 486 audio.append(c['name']) 487 elif d['media'] == 'video': 488 video.append(c['name']) 489 else: 490 assert False, c.toXml() 491 492 assert len(audio) + len(video) > 0, query.toXml() 493 494 return (self._extract_session_id(query), audio, video) 495 496class JingleTest2(object): 497 # Default caps for the remote end 498 remote_caps = { 'ext': '', 'ver': '0.0.0', 499 'node': 'http://example.com/fake-client0' } 500 501 # Default audio codecs for the remote end 502 audio_codecs = [ ('GSM', 3, 8000, {}), 503 ('PCMA', 8, 8000, {}), 504 ('PCMU', 0, 8000, {}) ] 505 506 # Default video codecs for the remote end. I have no idea what's 507 # a suitable value here... 508 video_codecs = [ ('WTF', 96, 90000, {}) ] 509 510 511 ufrag = "SessionUfrag" 512 pwd = "SessionPwd" 513 # Default candidates for the remote end 514 remote_call_candidates = [# Local candidates 515 (1, "192.168.0.1", 666, 516 {"type": cs.CALL_STREAM_CANDIDATE_TYPE_HOST, 517 #"Foundation":, 518 "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP, 519 "priority": 10000, 520 #"base-ip": 521 }), 522 (2, "192.168.0.1", 667, 523 {"type": cs.CALL_STREAM_CANDIDATE_TYPE_HOST, 524 #"Foundation":, 525 "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP, 526 "priority": 10000, 527 #"base-ip": 528 }), 529 # STUN candidates have their own ufrag 530 (1, "168.192.0.1", 10666, 531 {"type": cs.CALL_STREAM_CANDIDATE_TYPE_SERVER_REFLEXIVE, 532 #"Foundation":, 533 "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP, 534 "priority": 100, 535 #"base-ip":, 536 "username": "STUNRTPUfrag", 537 "password": "STUNRTPPwd" 538 }), 539 (2, "168.192.0.1", 10667, 540 {"type": cs.CALL_STREAM_CANDIDATE_TYPE_SERVER_REFLEXIVE, 541 #"Foundation":, 542 "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP, 543 "priority": 100, 544 #"base-ip":, 545 "username": "STUNRTCPUfrag", 546 "password": "STUNRTCPPwd" 547 }), 548 # Candidates found using UPnP or somesuch? 549 (1, "131.111.12.50", 10666, 550 {"type": cs.CALL_STREAM_CANDIDATE_TYPE_HOST, 551 #"Foundation":, 552 "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP, 553 "priority": 1000, 554 #"base-ip": 555 }), 556 (2, "131.111.12.50", 10667, 557 {"type": cs.CALL_STREAM_CANDIDATE_TYPE_HOST, 558 #"Foundation":, 559 "protocol": cs.MEDIA_STREAM_BASE_PROTO_UDP, 560 "priority": 1000, 561 #"base-ip": 562 }), 563 ] 564 remote_transports = [ 565 ( "192.168.0.1", # host 566 666, # port 567 0, # protocol = TP_MEDIA_STREAM_BASE_PROTO_UDP 568 "RTP", # protocol subtype 569 "AVP", # profile 570 1.0, # preference 571 0, # transport type = TP_CALL_STREAM_CANDIDATE_TYPE_HOST, 572 "username", 573 "password" ) ] 574 575 576 577 def __init__(self, jp, conn, q, stream, jid, peer): 578 self.jp = jp 579 self.conn = conn 580 self.q = q 581 self.jid = jid 582 self.peer = peer 583 self.peer_bare_jid = peer.split('/', 1)[0] 584 self.stream = stream 585 self.sid = 'sess' + str(int(random.random() * 10000)) 586 587 def prepare(self, send_presence=True, send_roster=True, events=None): 588 # If we need to override remote caps, feats, codecs or caps, 589 # we should do it prior to calling this method. 590 591 if events is None: 592 # Catch events: authentication, our presence update, 593 # status connected, vCard query 594 # If we don't catch the vCard query here, it can trip us up later: 595 # http://bugs.freedesktop.org/show_bug.cgi?id=19161 596 events = self.q.expect_many( 597 EventPattern('stream-iq', to=None, query_ns='vcard-temp', 598 query_name='vCard'), 599 EventPattern('stream-iq', query_ns=ns.ROSTER), 600 ) 601 602 # some Jingle tests care about our roster relationship to the peer 603 if send_roster: 604 roster = events[-1] 605 606 roster.stanza['type'] = 'result' 607 item = roster.query.addElement('item') 608 item['jid'] = 'publish@foo.com' 609 item['subscription'] = 'from' 610 item = roster.query.addElement('item') 611 item['jid'] = 'subscribe@foo.com' 612 item['subscription'] = 'to' 613 item = roster.query.addElement('item') 614 item['jid'] = 'publish-subscribe@foo.com' 615 item['subscription'] = 'both' 616 self.stream.send(roster.stanza) 617 618 if send_presence: 619 self.send_presence_and_caps() 620 621 def send_presence(self): 622 # We need remote end's presence for capabilities 623 self.stream.send(self.jp.xml( 624 self.jp.Presence(self.peer, self.jid, self.remote_caps))) 625 626 # Gabble doesn't trust it, so makes a disco 627 return self.q.expect('stream-iq', query_ns=ns.DISCO_INFO, to=self.peer) 628 629 def send_remote_disco_reply(self, query_stanza): 630 self.stream.send(self.jp.xml(self.jp.ResultIq(self.jid, query_stanza, 631 [ self.jp.Query(None, ns.DISCO_INFO, 632 [ self.jp.Feature(x) for x in self.jp.features ]) ]) )) 633 634 def send_presence_and_caps(self): 635 event = self.send_presence() 636 self.send_remote_disco_reply(event.stanza) 637 638 # Force Gabble to process the caps before doing any more Jingling 639 sync_stream(self.q, self.stream) 640 641 def generate_payloads(self, codecs, **kwargs): 642 return [ self.jp.PayloadType(payload_name, 643 str(rate), str(id), parameters, **kwargs) for 644 (payload_name, id, rate, parameters) in codecs ] 645 646 def generate_contents(self, transports=[]): 647 assert len(self.audio_names + self.video_names) > 0 648 649 jp = self.jp 650 651 assert len(self.video_names) == 0 or jp.can_do_video() 652 653 contents = [] 654 655 if not jp.separate_contents() and self.video_names: 656 assert jp.can_do_video() 657 assert self.audio_names 658 659 payload = self.generate_payloads (self.video_codecs) + \ 660 self.generate_payloads (self.audio_codecs, type="audio") 661 662 contents.append( 663 jp.Content('stream0', 'initiator', 'both', 664 jp.Description('video', payload), 665 jp.TransportGoogleP2P(transports)) 666 ) 667 else: 668 def mk_content(name, media, codecs): 669 payload = self.generate_payloads (codecs) 670 671 contents.append( 672 jp.Content(name, 'initiator', 'both', 673 jp.Description(media, payload), 674 jp.TransportGoogleP2P(transports)) 675 ) 676 677 for name in self.audio_names: 678 mk_content(name, 'audio', self.audio_codecs) 679 680 for name in self.video_names: 681 mk_content(name, 'video', self.video_codecs) 682 683 return contents 684 685 def incoming_call(self, audio = "audio1", video = None): 686 jp = self.jp 687 688 self.audio_names = [ audio ] if audio != None else [] 689 self.video_names = [ video ] if video != None else [] 690 691 contents = self.generate_contents() 692 693 node = jp.SetIq(self.peer, self.jid, [ 694 jp.Jingle(self.sid, self.peer, 'session-initiate', contents), 695 ]) 696 self.stream.send(jp.xml(node)) 697 698 def parse_session_initiate (self, query): 699 # Validate the session initiate and get some useful info from it 700 self.sid, self.audio_names, self.video_names = \ 701 self.jp.validate_session_initiate(query) 702 703 def accept(self): 704 jp = self.jp 705 706 contents = self.generate_contents() 707 node = jp.SetIq(self.peer, self.jid, [ 708 jp.Jingle(self.sid, self.peer, 'session-accept', 709 contents) ]) 710 self.stream.send(jp.xml(node)) 711 712 def content_accept(self, query, media): 713 """ 714 Accepts a content-add stanza containing a single <content> of the given 715 media type. 716 """ 717 jp = self.jp 718 assert jp.separate_contents() 719 c = query.firstChildElement() 720 721 if media == 'audio': 722 codecs = self.audio_codecs 723 elif media == 'video': 724 codecs = self.video_codecs 725 else: 726 assert False 727 728 # Remote end finally accepts 729 node = jp.SetIq(self.peer, self.jid, [ 730 jp.Jingle(self.sid, self.peer, 'content-accept', [ 731 jp.Content(c['name'], c['creator'], c['senders'], 732 jp.Description(media, [ 733 jp.PayloadType(name, str(rate), str(id), parameters) for 734 (name, id, rate, parameters) in codecs ]), 735 jp.TransportGoogleP2P()) ]) ]) 736 self.stream.send(jp.xml(node)) 737 738 def content_modify(self, name, creator, senders): 739 jp = self.jp 740 741 assert jp.separate_contents() 742 node = jp.SetIq(self.peer, self.jid, [ 743 jp.Jingle(self.sid, self.peer, 'content-modify', [ 744 jp.Content(name, creator, senders)])]) 745 self.stream.send(jp.xml(node)) 746 747 748 def terminate(self, reason=None, text=""): 749 jp = self.jp 750 751 if reason is not None and jp.is_modern_jingle(): 752 body = [("reason", None, {}, 753 [(reason, None, {}, []), 754 ("text", None, {}, [text]), 755 ] 756 )] 757 else: 758 body = [] 759 760 iq = jp.SetIq(self.peer, self.jid, [ 761 jp.Jingle(self.sid, self.peer, 'session-terminate', body) ]) 762 self.stream.send(jp.xml(iq)) 763 764 def result_iq(self, iniq, children = []): 765 jp = self.jp 766 iq = jp.ResultIq(self.peer, {'id': iniq.iq_id, 'to': self.peer}, 767 children) 768 self.stream.send(jp.xml(iq)) 769 770 771 def send_remote_candidates_call_xmpp(self, name, creator, candidates=None): 772 jp = self.jp 773 if candidates is None: 774 candidates = self.remote_call_candidates 775 776 node = jp.SetIq(self.peer, self.jid, 777 [ jp.Jingle(self.sid, self.peer, 'transport-info', 778 [ jp.Content(name, creator, 779 transport=jp.TransportGoogleP2PCall (self.ufrag, self.pwd, 780 candidates)) 781 ] ) 782 ]) 783 self.stream.send(jp.xml(node)) 784 785 def remote_candidates(self, name, creator): 786 jp = self.jp 787 788 node = jp.SetIq(self.peer, self.jid, 789 [ jp.Jingle(self.sid, self.peer, 'transport-info', 790 [ jp.Content(name, creator, 791 transport=jp.TransportGoogleP2P (self.remote_transports)) 792 ] ) 793 ]) 794 self.stream.send(jp.xml(node)) 795 796 def dbusify_codecs(self, codecs): 797 dbussed_codecs = [ (id, name, 0, rate, 0, params ) 798 for (name, id, rate, params) in codecs ] 799 return dbus.Array(dbussed_codecs, signature='(usuuua{ss})') 800 801 def dbusify_codecs_with_params (self, codecs): 802 return self.dbusify_codecs(codecs) 803 804 def get_audio_codecs_dbus(self): 805 return self.dbusify_codecs(self.audio_codecs) 806 807 def get_video_codecs_dbus(self): 808 return self.dbusify_codecs(self.video_codecs) 809 810 def dbusify_call_codecs(self, codecs): 811 dbussed_codecs = [ (id, name, rate, 0, False, params) 812 for (name, id, rate, params) in codecs ] 813 return dbus.Array(dbussed_codecs, signature='(usuuba{ss})') 814 815 def dbusify_call_codecs_with_params(self, codecs): 816 return dbusify_call_codecs (self, codecs) 817 818 def __get_call_audio_codecs_dbus(self): 819 return self.dbusify_call_codecs(self.audio_codecs) 820 821 def __get_call_video_codecs_dbus(self): 822 return self.dbusify_call_codecs(self.video_codecs) 823 824 def get_call_audio_md_dbus(self, handle = 0): 825 d = dbus.Dictionary( 826 { cs.CALL_CONTENT_MEDIADESCRIPTION + '.Codecs': self.__get_call_audio_codecs_dbus(), 827 }, signature='sv') 828 if handle != 0: 829 d[cs.CALL_CONTENT_MEDIADESCRIPTION + '.RemoteContact'] = dbus.UInt32 (handle) 830 return d 831 832 def get_call_video_md_dbus(self, handle = 0): 833 d = dbus.Dictionary( 834 { cs.CALL_CONTENT_MEDIADESCRIPTION + '.Codecs': self.__get_call_video_codecs_dbus(), 835 }, signature='sv') 836 if handle != 0: 837 d[cs.CALL_CONTENT_MEDIADESCRIPTION + '.RemoteContact'] = dbus.UInt32 (handle) 838 return d 839 840 def get_remote_transports_dbus(self): 841 return dbus.Array([ 842 (dbus.UInt32(1 + i), host, port, proto, subtype, 843 profile, pref, transtype, user, pwd) 844 for i, (host, port, proto, subtype, profile, 845 pref, transtype, user, pwd) 846 in enumerate(self.remote_transports) ], 847 signature='(usuussduss)') 848 849 def get_call_remote_transports_dbus(self): 850 return dbus.Array(self.remote_call_candidates, 851 signature='(usqa{sv})') 852 853 854def test_dialects(f, dialects, params=None, protocol=None): 855 for dialect in dialects: 856 exec_test(partial(f, dialect()), params=params, protocol=protocol) 857 858def test_all_dialects(f, params=None, protocol=None): 859 dialectmap = { "jingle015": JingleProtocol015, 860 "jingle031": JingleProtocol031, 861 "gtalk03": GtalkProtocol03, 862 "gtalk04": GtalkProtocol04 863 } 864 dialects = [] 865 866 jd = os.getenv("JINGLE_DIALECTS") 867 if jd == None: 868 dialects = dialectmap.values() 869 else: 870 for d in jd.split (','): 871 dialects.append(dialectmap[d]) 872 test_dialects(f, dialects, params=params, protocol=protocol) 873