From b2c5810869f6a62de2dd69c893d7ad0385e79998 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sun, 15 Nov 2009 20:47:06 +0100 Subject: [PATCH] Refactorize a bit jingle.py and split it into different files. There is still room for improvement, but it should be better. --- src/common/jingle.py | 1074 +------------------------------- src/common/jingle_content.py | 102 +++ src/common/jingle_rtp.py | 314 ++++++++++ src/common/jingle_session.py | 613 ++++++++++++++++++ src/common/jingle_transport.py | 139 +++++ 5 files changed, 1175 insertions(+), 1067 deletions(-) create mode 100644 src/common/jingle_content.py create mode 100644 src/common/jingle_rtp.py create mode 100644 src/common/jingle_session.py create mode 100644 src/common/jingle_transport.py diff --git a/src/common/jingle.py b/src/common/jingle.py index 6549ee10c..8a369ff7e 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -13,17 +13,6 @@ ''' Handles the jingle signalling protocol. ''' #TODO: -# * things in XEP 0166, including: -# - 'senders' attribute of 'content' element -# - security preconditions -# * actions: -# - content-modify -# - description-info, session-info -# - security-info -# - transport-accept, transport-reject -# * sid/content related: -# - tiebreaking -# - if there already is a session, use it # * things in XEP 0176, including: # - http://xmpp.org/extensions/xep-0176.html#protocol-restarts # - http://xmpp.org/extensions/xep-0176.html#fallback @@ -36,1064 +25,15 @@ # - codecs # - STUN -# * DONE: figure out why it doesn't work with pidgin: -# That's a bug in pidgin: http://xmpp.org/extensions/xep-0176.html#protocol-checks +# * figure out why it doesn't work with pidgin: +# That's maybe a bug in pidgin: +# http://xmpp.org/extensions/xep-0176.html#protocol-checks -# * timeout - -# * split this file in several modules -# For example, a file dedicated for XEP0166, one for XEP0176, -# and one for XEP0167 - -# * handle different kinds of sink and src elements - -import gobject - -import gajim import xmpp import helpers -import farsight, gst - -def get_first_gst_element(elements): - ''' Returns, if it exists, the first available element of the list. ''' - for name in elements: - factory = gst.element_factory_find(name) - if factory: - return factory.create() - -#FIXME: Move it to JingleSession.States? -class JingleStates(object): - ''' States in which jingle session may exist. ''' - ended = 0 - pending = 1 - active = 2 - -#FIXME: Move it to JingleTransport.Type? -class TransportType(object): - ''' Possible types of a JingleTransport ''' - datagram = 1 - streaming = 2 - -class OutOfOrder(Exception): - ''' Exception that should be raised when an action is received when in the wrong state. ''' - -class TieBreak(Exception): - ''' Exception that should be raised in case of a tie, when we overrule the other action. ''' - -class JingleSession(object): - ''' This represents one jingle session. ''' - def __init__(self, con, weinitiate, jid, sid=None): - ''' con -- connection object, - weinitiate -- boolean, are we the initiator? - jid - jid of the other entity''' - self.contents = {} # negotiated contents - self.connection = con # connection to use - # our full jid - self.ourjid = gajim.get_jid_from_account(self.connection.name) + '/' + \ - con.server_resource - self.peerjid = jid # jid we connect to - # jid we use as the initiator - self.initiator = weinitiate and self.ourjid or self.peerjid - # jid we use as the responder - self.responder = weinitiate and self.peerjid or self.ourjid - # are we an initiator? - self.weinitiate = weinitiate - # what state is session in? (one from JingleStates) - self.state = JingleStates.ended - if not sid: - sid = con.connection.getAnID() - self.sid = sid # sessionid - - self.accepted = True # is this session accepted by user - - # callbacks to call on proper contents - # use .prepend() to add new callbacks, especially when you're going - # to send error instead of ack - self.callbacks = { - 'content-accept': [self.__contentAcceptCB, self.__broadcastCB, - self.__defaultCB], - 'content-add': [self.__contentAddCB, self.__broadcastCB, - self.__defaultCB], #TODO - 'content-modify': [self.__defaultCB], #TODO - 'content-reject': [self.__defaultCB, self.__contentRemoveCB], #TODO - 'content-remove': [self.__defaultCB, self.__contentRemoveCB], - 'description-info': [self.__broadcastCB, self.__defaultCB], #TODO - 'security-info': [self.__defaultCB], #TODO - 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, - self.__broadcastCB, self.__defaultCB], - 'session-info': [self.__sessionInfoCB, self.__broadcastCB, self.__defaultCB], - 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, - self.__defaultCB], - 'session-terminate': [self.__sessionTerminateCB, self.__broadcastAllCB, - self.__defaultCB], - 'transport-info': [self.__broadcastCB, self.__defaultCB], - 'transport-replace': [self.__broadcastCB, self.__transportReplaceCB], #TODO - 'transport-accept': [self.__defaultCB], #TODO - 'transport-reject': [self.__defaultCB], #TODO - 'iq-result': [], - 'iq-error': [self.__errorCB], - } - - ''' Interaction with user ''' - def approve_session(self): - ''' Called when user accepts session in UI (when we aren't the initiator). - ''' - self.accept_session() - - def decline_session(self): - ''' Called when user declines session in UI (when we aren't the initiator) - ''' - reason = xmpp.Node('reason') - reason.addChild('decline') - self._session_terminate(reason) - - def approve_content(self, media): - content = self.get_content(media) - if content: - content.accepted = True - self.on_session_state_changed(content) - - def reject_content(self, media): - content = self.get_content(media) - if content: - if self.state == JingleStates.active: - self.__content_reject(content) - content.destroy() - self.on_session_state_changed() - - def end_session(self): - ''' Called when user stops or cancel session in UI. ''' - reason = xmpp.Node('reason') - if self.state == JingleStates.active: - reason.addChild('success') - else: - reason.addChild('cancel') - self._session_terminate(reason) - - ''' Middle-level functions to manage contents. Handle local content - cache and send change notifications. ''' - def get_content(self, media=None): - if media == 'audio': - cls = JingleVoIP - elif media == 'video': - cls = JingleVideo - #elif media == None: - # cls = JingleContent - else: - return None - - for content in self.contents.values(): - if isinstance(content, cls): - return content - - def add_content(self, name, content, creator='we'): - ''' Add new content to session. If the session is active, - this will send proper stanza to update session. - Creator must be one of ('we', 'peer', 'initiator', 'responder')''' - assert creator in ('we', 'peer', 'initiator', 'responder') - - if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \ - not self.weinitiate): - creator = 'initiator' - elif (creator == 'peer' and self.weinitiate) or (creator == 'we' and \ - not self.weinitiate): - creator = 'responder' - content.creator = creator - content.name = name - self.contents[(creator, name)] = content - - if (creator == 'initiator') == self.weinitiate: - # The content is from us, accept it - content.accepted = True - - def remove_content(self, creator, name): - ''' We do not need this now ''' - #TODO: - if (creator, name) in self.contents: - content = self.contents[(creator, name)] - if len(self.contents) > 1: - self.__content_remove(content) - self.contents[(creator, name)].destroy() - if len(self.contents) == 0: - self.end_session() - - def modify_content(self, creator, name, *someother): - ''' We do not need this now ''' - pass - - def on_session_state_changed(self, content=None): - if self.state == JingleStates.ended: - # Session not yet started, only one action possible: session-initiate - if self.is_ready() and self.weinitiate: - self.__session_initiate() - elif self.state == JingleStates.pending: - # We can either send a session-accept or a content-add - if self.is_ready() and not self.weinitiate: - self.__session_accept() - elif content and (content.creator == 'initiator') == self.weinitiate: - self.__content_add(content) - elif content and self.weinitiate: - self.__content_accept(content) - elif self.state == JingleStates.active: - # We can either send a content-add or a content-accept - if not content: - return - if (content.creator == 'initiator') == self.weinitiate: - # We initiated this content. It's a pending content-add. - self.__content_add(content) - else: - # The other side created this content, we accept it. - self.__content_accept(content) - - def is_ready(self): - ''' Returns True when all codecs and candidates are ready - (for all contents). ''' - return (all((content.is_ready() for content in self.contents.itervalues())) - and self.accepted) - - ''' Middle-level function to do stanza exchange. ''' - def accept_session(self): - ''' Mark the session as accepted. ''' - self.accepted = True - self.on_session_state_changed() - - def start_session(self): - ''' Mark the session as ready to be started. ''' - self.accepted = True - self.on_session_state_changed() - - def send_session_info(self): - pass - - def send_content_accept(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-accept') - jingle.addChild(node=content) - self.connection.connection.send(stanza) - - def send_transport_info(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('transport-info') - jingle.addChild(node=content) - self.connection.connection.send(stanza) - - ''' Session callbacks. ''' - def stanzaCB(self, stanza): - ''' A callback for ConnectionJingle. It gets stanza, then - tries to send it to all internally registered callbacks. - First one to raise xmpp.NodeProcessed breaks function.''' - jingle = stanza.getTag('jingle') - error = stanza.getTag('error') - if error: - # it's an iq-error stanza - action = 'iq-error' - elif jingle: - # it's a jingle action - action = jingle.getAttr('action') - if action not in self.callbacks: - self.__send_error(stanza, 'bad_request') - return - #FIXME: If we aren't initiated and it's not a session-initiate... - if action != 'session-initiate' and self.state == JingleStates.ended: - self.__send_error(stanza, 'item-not-found', 'unknown-session') - return - else: - # it's an iq-result (ack) stanza - action = 'iq-result' - - callables = self.callbacks[action] - - try: - for callable in callables: - callable(stanza=stanza, jingle=jingle, error=error, action=action) - except xmpp.NodeProcessed: - pass - except TieBreak: - self.__send_error(stanza, 'conflict', 'tiebreak') - except OutOfOrder: - self.__send_error(stanza, 'unexpected-request', 'out-of-order')#FIXME - - def __defaultCB(self, stanza, jingle, error, action): - ''' Default callback for action stanzas -- simple ack - and stop processing. ''' - response = stanza.buildReply('result') - self.connection.connection.send(response) - - def __errorCB(self, stanza, jingle, error, action): - #FIXME - text = error.getTagData('text') - jingle_error = None - xmpp_error = None - for child in error.getChildren(): - if child.getNamespace() == xmpp.NS_JINGLE_ERRORS: - jingle_error = child.getName() - elif child.getNamespace() == xmpp.NS_STANZAS: - xmpp_error = child.getName() - self.__dispatch_error(xmpp_error, jingle_error, text) - #FIXME: Not sure when we would want to do that... - if xmpp_error == 'item-not-found': - self.connection.delete_jingle_session(self.peerjid, self.sid) - - def __transportReplaceCB(self, stanza, jingle, error, action): - for content in jingle.iterTags('content'): - creator = content['creator'] - name = content['name'] - if (creator, name) in self.contents: - transport_ns = content.getTag('transport').getNamespace() - if transport_ns == xmpp.JINGLE_ICE_UDP: - #FIXME: We don't manage anything else than ICE-UDP now... - #What was the previous transport?!? - #Anyway, content's transport is not modifiable yet - pass - else: - stanza, jingle = self.__make_jingle('transport-reject') - content = jingle.setTag('content', attrs={'creator': creator, - 'name': name}) - content.setTag('transport', namespace=transport_ns) - self.connection.connection.send(stanza) - raise xmpp.NodeProcessed - else: - #FIXME: This ressource is unknown to us, what should we do? - #For now, reject the transport - stanza, jingle = self.__make_jingle('transport-reject') - c = jingle.setTag('content', attrs={'creator': creator, - 'name': name}) - c.setTag('transport', namespace=transport_ns) - self.connection.connection.send(stanza) - raise xmpp.NodeProcessed - - def __sessionInfoCB(self, stanza, jingle, error, action): - #TODO: ringing, active, (un)hold, (un)mute - payload = jingle.getPayload() - if len(payload) > 0: - self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info') - raise xmpp.NodeProcessed - - def __contentRemoveCB(self, stanza, jingle, error, action): - for content in jingle.iterTags('content'): - creator = content['creator'] - name = content['name'] - if (creator, name) in self.contents: - content = self.contents[(creator, name)] - #TODO: this will fail if content is not an RTP content - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, content.media, 'removed')) - content.destroy() - if len(self.contents) == 0: - reason = xmpp.Node('reason') - reason.setTag('success') - self._session_terminate(reason) - - def __sessionAcceptCB(self, stanza, jingle, error, action): - if self.state != JingleStates.pending: #FIXME - raise OutOfOrder - self.state = JingleStates.active - - def __contentAcceptCB(self, stanza, jingle, error, action): - ''' Called when we get content-accept stanza or equivalent one - (like session-accept).''' - # check which contents are accepted - for content in jingle.iterTags('content'): - creator = content['creator'] - name = content['name']#TODO... - - def __contentAddCB(self, stanza, jingle, error, action): - if self.state == JingleStates.ended: - raise OutOfOrder - - parse_result = self.__parse_contents(jingle) - contents = parse_result[2] - rejected_contents = parse_result[3] - - for name, creator in rejected_contents: - #TODO: - content = JingleContent() - self.add_content(name, content, creator) - self.__content_reject(content) - self.contents[(content.creator, content.name)].destroy() - - self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, - contents)) - - def __sessionInitiateCB(self, stanza, jingle, error, action): - ''' We got a jingle session request from other entity, - therefore we are the receiver... Unpack the data, - inform the user. ''' - - if self.state != JingleStates.ended: - raise OutOfOrder - - self.initiator = jingle['initiator'] - self.responder = self.ourjid - self.peerjid = self.initiator - self.accepted = False # user did not accept this session yet - - # TODO: If the initiator is unknown to the receiver (e.g., via presence - # subscription) and the receiver has a policy of not communicating via - # Jingle with unknown entities, it SHOULD return a - # error. - - # Lets check what kind of jingle session does the peer want - contents_ok, transports_ok, contents, pouet = self.__parse_contents(jingle) - - # If there's no content we understand... - if not contents_ok: - # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate - reason = xmpp.Node('reason') - reason.setTag('unsupported-applications') - self.__defaultCB(stanza, jingle, error, action) - self._session_terminate(reason) - raise xmpp.NodeProcessed - - if not transports_ok: - # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate - reason = xmpp.Node('reason') - reason.setTag('unsupported-transports') - self.__defaultCB(stanza, jingle, error, action) - self._session_terminate(reason) - raise xmpp.NodeProcessed - - self.state = JingleStates.pending - - # Send event about starting a session - self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, - contents)) - - def __broadcastCB(self, stanza, jingle, error, action): - ''' Broadcast the stanza contents to proper content handlers. ''' - for content in jingle.iterTags('content'): - name = content['name'] - creator = content['creator'] - cn = self.contents[(creator, name)] - cn.stanzaCB(stanza, content, error, action) - - def __sessionTerminateCB(self, stanza, jingle, error, action): - self.connection.delete_jingle_session(self.peerjid, self.sid) - reason, text = self.__reason_from_stanza(jingle) - if reason not in ('success', 'cancel', 'decline'): - self.__dispatch_error(reason, reason, text) - if text: - text = '%s (%s)' % (reason, text) - else: - text = reason#TODO - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, None, text)) - - def __broadcastAllCB(self, stanza, jingle, error, action): - ''' Broadcast the stanza to all content handlers. ''' - for content in self.contents.itervalues(): - content.stanzaCB(stanza, None, error, action) - - ''' Internal methods. ''' - def __parse_contents(self, jingle): - #TODO: Needs some reworking - contents = [] - contents_rejected = [] - contents_ok = False - transports_ok = False - - for element in jingle.iterTags('content'): - desc = element.getTag('description') - desc_ns = desc.getNamespace() - tran_ns = element.getTag('transport').getNamespace() - if desc_ns == xmpp.NS_JINGLE_RTP and desc['media'] in ('audio', 'video'): - contents_ok = True - #TODO: Everything here should be moved somewhere else - if tran_ns == xmpp.NS_JINGLE_ICE_UDP: - if desc['media'] == 'audio': - self.add_content(element['name'], JingleVoIP(self), 'peer') - else: - self.add_content(element['name'], JingleVideo(self), 'peer') - contents.append((desc['media'],)) - transports_ok = True - else: - contents_rejected.append((element['name'], 'peer')) - else: - contents_rejected.append((element['name'], 'peer')) - - return (contents_ok, transports_ok, contents, contents_rejected) - - def __dispatch_error(self, error, jingle_error=None, text=None): - if jingle_error: - error = jingle_error - if text: - text = '%s (%s)' % (error, text) - else: - text = error - self.connection.dispatch('JINGLE_ERROR', (self.peerjid, self.sid, text)) - - def __reason_from_stanza(self, stanza): - reason = 'success' - reasons = ['success', 'busy', 'cancel', 'connectivity-error', - 'decline', 'expired', 'failed-application', 'failed-transport', - 'general-error', 'gone', 'incompatible-parameters', 'media-error', - 'security-error', 'timeout', 'unsupported-applications', - 'unsupported-transports'] - tag = stanza.getTag('reason') - if tag: - text = tag.getTagData('text') - for r in reasons: - if tag.getTag(r): - reason = r - break - return (reason, text) - - ''' Methods that make/send proper pieces of XML. They check if the session - is in appropriate state. ''' - def __make_jingle(self, action): - stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) - attrs = {'action': action, - 'sid': self.sid} - if action == 'session-initiate': - attrs['initiator'] = self.initiator - elif action == 'session-accept': - attrs['responder'] = self.responder - jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE) - return stanza, jingle - - def __send_error(self, stanza, error, jingle_error=None, text=None): - err = xmpp.Error(stanza, error) - err.setNamespace(xmpp.NS_STANZAS) - if jingle_error: - err.setTag(jingle_error, namespace=xmpp.NS_JINGLE_ERRORS) - if text: - err.setTagData('text', text) - self.connection.connection.send(err) - self.__dispatch_error(error, jingle_error, text) - - def __append_content(self, jingle, content): - ''' Append element to element, - with (full=True) or without (full=False) - children. ''' - jingle.addChild('content', - attrs={'name': content.name, 'creator': content.creator}) - - def __append_contents(self, jingle): - ''' Append all elements to .''' - # TODO: integrate with __appendContent? - # TODO: parameters 'name', 'content'? - for content in self.contents.values(): - self.__append_content(jingle, content) - - def __session_initiate(self): - assert self.state == JingleStates.ended - stanza, jingle = self.__make_jingle('session-initiate') - self.__append_contents(jingle) - self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent') - self.connection.connection.send(stanza) - self.state = JingleStates.pending - - def __session_accept(self): - assert self.state == JingleStates.pending - stanza, jingle = self.__make_jingle('session-accept') - self.__append_contents(jingle) - self.__broadcastCB(stanza, jingle, None, 'session-accept-sent') - self.connection.connection.send(stanza) - self.state = JingleStates.active - - def __session_info(self, payload=None): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('session-info') - if payload: - jingle.addChild(node=payload) - self.connection.connection.send(stanza) - - def _session_terminate(self, reason=None): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('session-terminate') - if reason is not None: - jingle.addChild(node=reason) - self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent') - self.connection.connection.send(stanza) - reason, text = self.__reason_from_stanza(jingle) - if reason not in ('success', 'cancel', 'decline'): - self.__dispatch_error(reason, reason, text) - if text: - text = '%s (%s)' % (reason, text) - else: - text = reason - self.connection.delete_jingle_session(self.peerjid, self.sid) - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, None, text)) - - def __content_add(self, content): - #TODO: test - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-add') - self.__append_content(jingle, content) - self.__broadcastCB(stanza, jingle, None, 'content-add-sent') - self.connection.connection.send(stanza) - - def __content_accept(self, content): - #TODO: test - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-accept') - self.__append_content(jingle, content) - self.__broadcastCB(stanza, jingle, None, 'content-accept-sent') - self.connection.connection.send(stanza) - - def __content_reject(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-reject') - self.__append_content(jingle, content) - self.connection.connection.send(stanza) - #TODO: this will fail if content is not an RTP content - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, content.media, 'rejected')) - - def __content_modify(self): - assert self.state != JingleStates.ended - - def __content_remove(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-remove') - self.__append_content(jingle, content) - self.connection.connection.send(stanza) - #TODO: this will fail if content is not an RTP content - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, content.media, 'removed')) - - def content_negociated(self, media): - self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid, - media)) - -#TODO: -#class JingleTransport(object): -# ''' An abstraction of a transport in Jingle sessions. ''' -# def __init__(self): -# pass - - -class JingleContent(object): - ''' An abstraction of content in Jingle sessions. ''' - def __init__(self, session, node=None): - self.session = session - # will be filled by JingleSession.add_content() - # don't uncomment these lines, we will catch more buggy code then - # (a JingleContent not added to session shouldn't send anything) - #self.creator = None - #self.name = None - self.accepted = False - self.sent = False - self.candidates = [] # Local transport candidates - self.remote_candidates = [] # Remote transport candidates - - self.senders = 'both' #FIXME - self.allow_sending = True # Used for stream direction, attribute 'senders' - - self.callbacks = { - # these are called when *we* get stanzas - 'content-accept': [self.__transportInfoCB], - 'content-add': [self.__transportInfoCB], - 'content-modify': [], - 'content-reject': [], - 'content-remove': [], - 'description-info': [], - 'security-info': [], - 'session-accept': [self.__transportInfoCB], - 'session-info': [], - 'session-initiate': [self.__transportInfoCB], - 'session-terminate': [], - 'transport-info': [self.__transportInfoCB], - 'transport-replace': [], - 'transport-accept': [], - 'transport-reject': [], - 'iq-result': [], - 'iq-error': [], - # these are called when *we* sent these stanzas - 'content-accept-sent': [self.__fillJingleStanza], - 'content-add-sent': [self.__fillJingleStanza], - 'session-initiate-sent': [self.__fillJingleStanza], - 'session-accept-sent': [self.__fillJingleStanza], - 'session-terminate-sent': [], - } - - def is_ready(self): - #print '[%s] %s, %s' % (self.media, self.candidates_ready, - # self.p2psession.get_property('codecs-ready')) - return (self.accepted and self.candidates_ready and not self.sent - and self.p2psession.get_property('codecs-ready')) - - def stanzaCB(self, stanza, content, error, action): - ''' Called when something related to our content was sent by peer. ''' - if action in self.callbacks: - for callback in self.callbacks[action]: - callback(stanza, content, error, action) - - def __transportInfoCB(self, stanza, content, error, action): - ''' Got a new transport candidate. ''' - candidates = [] - transport = content.getTag('transport') - for candidate in transport.iterTags('candidate'): - cand = farsight.Candidate() - cand.component_id = int(candidate['component']) - cand.ip = str(candidate['ip']) - cand.port = int(candidate['port']) - cand.foundation = str(candidate['foundation']) - #cand.type = farsight.CANDIDATE_TYPE_LOCAL - cand.priority = int(candidate['priority']) - - if candidate['protocol'] == 'udp': - cand.proto = farsight.NETWORK_PROTOCOL_UDP - else: - # we actually don't handle properly different tcp options in jingle - cand.proto = farsight.NETWORK_PROTOCOL_TCP - - cand.username = str(transport['ufrag']) - cand.password = str(transport['pwd']) - - #FIXME: huh? - types = {'host': farsight.CANDIDATE_TYPE_HOST, - 'srflx': farsight.CANDIDATE_TYPE_SRFLX, - 'prflx': farsight.CANDIDATE_TYPE_PRFLX, - 'relay': farsight.CANDIDATE_TYPE_RELAY, - 'multicast': farsight.CANDIDATE_TYPE_MULTICAST} - if 'type' in candidate and candidate['type'] in types: - cand.type = types[candidate['type']] - else: - print 'Unknown type %s', candidate['type'] - candidates.append(cand) - #FIXME: connectivity should not be etablished yet - # Instead, it should be etablished after session-accept! - if len(candidates) > 0: - if self.sent: - self.p2pstream.set_remote_candidates(candidates) - else: - self.remote_candidates.extend(candidates) - #self.p2pstream.set_remote_candidates(candidates) - #print self.media, self.creator, self.name, candidates - - def __content(self, payload=[]): - ''' Build a XML content-wrapper for our data. ''' - return xmpp.Node('content', - attrs={'name': self.name, 'creator': self.creator}, - payload=payload) - - def __candidate(self, candidate): - types = {farsight.CANDIDATE_TYPE_HOST: 'host', - farsight.CANDIDATE_TYPE_SRFLX: 'srflx', - farsight.CANDIDATE_TYPE_PRFLX: 'prflx', - farsight.CANDIDATE_TYPE_RELAY: 'relay', - farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} - attrs = { - 'component': candidate.component_id, - 'foundation': '1', # hack - 'generation': '0', - 'ip': candidate.ip, - 'network': '0', - 'port': candidate.port, - 'priority': int(candidate.priority), # hack - } - if candidate.type in types: - attrs['type'] = types[candidate.type] - if candidate.proto == farsight.NETWORK_PROTOCOL_UDP: - attrs['protocol'] = 'udp' - else: - # we actually don't handle properly different tcp options in jingle - attrs['protocol'] = 'tcp' - return xmpp.Node('candidate', attrs=attrs) - - def iter_candidates(self): - for candidate in self.candidates: - yield self.__candidate(candidate) - - def send_candidate(self, candidate): - content = self.__content() - transport = content.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport') - - if candidate.username and candidate.password: - transport['ufrag'] = candidate.username - transport['pwd'] = candidate.password - - transport.addChild(node=self.__candidate(candidate)) - self.session.send_transport_info(content) - - def __fillJingleStanza(self, stanza, content, error, action): - ''' Add our things to session-initiate stanza. ''' - self._fillContent(content) - - self.sent = True - - if self.candidates and self.candidates[0].username and \ - self.candidates[0].password: - attrs = {'ufrag': self.candidates[0].username, - 'pwd': self.candidates[0].password} - else: - attrs = {} - content.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport', attrs=attrs, - payload=self.iter_candidates()) - - def destroy(self): - self.callbacks = None - del self.session.contents[(self.creator, self.name)] - - -class JingleRTPContent(JingleContent): - def __init__(self, session, media, node=None): - JingleContent.__init__(self, session, node) - self.media = media - self._dtmf_running = False - self.farsight_media = {'audio': farsight.MEDIA_TYPE_AUDIO, - 'video': farsight.MEDIA_TYPE_VIDEO}[media] - self.got_codecs = False - - self.candidates_ready = False # True when local candidates are prepared - - self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB] - self.callbacks['content-add'] += [self.__getRemoteCodecsCB] - self.callbacks['content-accept'] += [self.__getRemoteCodecsCB, - self.__contentAcceptCB] - self.callbacks['session-accept'] += [self.__getRemoteCodecsCB, - self.__contentAcceptCB] - self.callbacks['session-accept-sent'] += [self.__contentAcceptCB] - self.callbacks['content-accept-sent'] += [self.__contentAcceptCB] - self.callbacks['session-terminate'] += [self.__stop] - self.callbacks['session-terminate-sent'] += [self.__stop] - - def setup_stream(self): - # pipeline and bus - self.pipeline = gst.Pipeline() - bus = self.pipeline.get_bus() - bus.add_signal_watch() - bus.connect('message', self._on_gst_message) - - # conference - self.conference = gst.element_factory_make('fsrtpconference') - self.conference.set_property("sdes-cname", self.session.ourjid) - self.pipeline.add(self.conference) - self.funnel = None - - self.p2psession = self.conference.new_session(self.farsight_media) - - participant = self.conference.new_participant(self.session.peerjid) - #FIXME: Consider a workaround, here... - # pidgin and telepathy-gabble don't follow the XEP, and it won't work - # due to bad controlling-mode - params = {'controlling-mode': self.session.weinitiate,# 'debug': False} - 'stun-ip': '69.0.208.27', 'debug': False} - - self.p2pstream = self.p2psession.new_stream(participant, - farsight.DIRECTION_RECV, 'nice', params) - - def batch_dtmf(self, events): - if self._dtmf_running: - raise Exception #TODO: Proper exception - self._dtmf_running = True - self._start_dtmf(events.pop(0)) - gobject.timeout_add(500, self._next_dtmf, events) - - def _next_dtmf(self, events): - self._stop_dtmf() - if events: - self._start_dtmf(events.pop(0)) - gobject.timeout_add(500, self._next_dtmf, events) - else: - self._dtmf_running = False - - def _start_dtmf(self, event): - if event in ('*', '#'): - event = {'*': farsight.DTMF_EVENT_STAR, - '#': farsight.DTMF_EVENT_POUND}[event] - else: - event = int(event) - self.p2psession.start_telephony_event(event, 2, - farsight.DTMF_METHOD_RTP_RFC4733) - - def _stop_dtmf(self): - self.p2psession.stop_telephony_event(farsight.DTMF_METHOD_RTP_RFC4733) - - def _fillContent(self, content): - content.addChild(xmpp.NS_JINGLE_RTP + ' description', - attrs={'media': self.media}, payload=self.iter_codecs()) - - def _setup_funnel(self): - self.funnel = gst.element_factory_make('fsfunnel') - self.pipeline.add(self.funnel) - self.funnel.set_state(gst.STATE_PLAYING) - self.sink.set_state(gst.STATE_PLAYING) - self.funnel.link(self.sink) - - def _on_src_pad_added(self, stream, pad, codec): - if not self.funnel: - self._setup_funnel() - pad.link(self.funnel.get_pad('sink%d')) - - def _on_gst_message(self, bus, message): - if message.type == gst.MESSAGE_ELEMENT: - name = message.structure.get_name() - if name == 'farsight-new-active-candidate-pair': - pass - elif name == 'farsight-recv-codecs-changed': - pass - elif name == 'farsight-codecs-changed': - if self.is_ready(): - self.session.on_session_state_changed(self) - #TODO: description-info - elif name == 'farsight-local-candidates-prepared': - self.candidates_ready = True - if self.is_ready(): - self.session.on_session_state_changed(self) - elif name == 'farsight-new-local-candidate': - candidate = message.structure['candidate'] - self.candidates.append(candidate) - if self.candidates_ready: - #FIXME: Is this case even possible? - self.send_candidate(candidate) - elif name == 'farsight-component-state-changed': - state = message.structure['state'] - print message.structure['component'], state - if state == farsight.STREAM_STATE_FAILED: - reason = xmpp.Node('reason') - reason.setTag('failed-transport') - self.session._session_terminate(reason) - elif name == 'farsight-error': - print 'Farsight error #%d!' % message.structure['error-no'] - print 'Message: %s' % message.structure['error-msg'] - print 'Debug: %s' % message.structure['debug-msg'] - else: - print name - - def __contentAcceptCB(self, stanza, content, error, action): - if self.accepted: - if len(self.remote_candidates) > 0: - self.p2pstream.set_remote_candidates(self.remote_candidates) - self.remote_candidates = [] - #TODO: farsight.DIRECTION_BOTH only if senders='both' - self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) - self.session.content_negociated(self.media) - - def __getRemoteCodecsCB(self, stanza, content, error, action): - ''' Get peer codecs from what we get from peer. ''' - if self.got_codecs: - return - - codecs = [] - for codec in content.getTag('description').iterTags('payload-type'): - c = farsight.Codec(int(codec['id']), codec['name'], - self.farsight_media, int(codec['clockrate'])) - if 'channels' in codec: - c.channels = int(codec['channels']) - else: - c.channels = 1 - c.optional_params = [(str(p['name']), str(p['value'])) for p in \ - codec.iterTags('parameter')] - codecs.append(c) - - if len(codecs) > 0: - #FIXME: Handle this case: - # glib.GError: There was no intersection between the remote codecs and - # the local ones - self.p2pstream.set_remote_codecs(codecs) - self.got_codecs = True - - def iter_codecs(self): - codecs = self.p2psession.get_property('codecs') - for codec in codecs: - attrs = {'name': codec.encoding_name, - 'id': codec.id, - 'channels': codec.channels} - if codec.clock_rate: - attrs['clockrate'] = codec.clock_rate - if codec.optional_params: - payload = (xmpp.Node('parameter', {'name': name, 'value': value}) - for name, value in codec.optional_params) - else: payload = () - yield xmpp.Node('payload-type', attrs, payload) - - def __stop(self, *things): - self.pipeline.set_state(gst.STATE_NULL) - - def __del__(self): - self.__stop() - - def destroy(self): - JingleContent.destroy(self) - self.p2pstream.disconnect_by_func(self._on_src_pad_added) - self.pipeline.get_bus().disconnect_by_func(self._on_gst_message) - - -class JingleVoIP(JingleRTPContent): - ''' Jingle VoIP sessions consist of audio content transported - over an ICE UDP protocol. ''' - def __init__(self, session, node=None): - JingleRTPContent.__init__(self, session, 'audio', node) - self.setup_stream() - - - ''' Things to control the gstreamer's pipeline ''' - def setup_stream(self): - JingleRTPContent.setup_stream(self) - - # Configure SPEEX - # Workaround for psi (not needed since rev - # 147aedcea39b43402fe64c533d1866a25449888a): - # place 16kHz before 8kHz, as buggy psi versions will take in - # account only the first codec - - codecs = [farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', - farsight.MEDIA_TYPE_AUDIO, 16000), - farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', - farsight.MEDIA_TYPE_AUDIO, 8000)] - self.p2psession.set_codec_preferences(codecs) - - # the local parts - # TODO: use gconfaudiosink? - # sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink']) - self.sink = gst.element_factory_make('alsasink') - self.sink.set_property('sync', False) - #sink.set_property('latency-time', 20000) - #sink.set_property('buffer-time', 80000) - - # TODO: use gconfaudiosrc? - src_mic = gst.element_factory_make('alsasrc') - src_mic.set_property('blocksize', 320) - - self.mic_volume = gst.element_factory_make('volume') - self.mic_volume.set_property('volume', 1) - - # link gst elements - self.pipeline.add(self.sink, src_mic, self.mic_volume) - src_mic.link(self.mic_volume) - - self.mic_volume.get_pad('src').link(self.p2psession.get_property( - 'sink-pad')) - self.p2pstream.connect('src-pad-added', self._on_src_pad_added) - - # The following is needed for farsight to process ICE requests: - self.pipeline.set_state(gst.STATE_PLAYING) - - -class JingleVideo(JingleRTPContent): - def __init__(self, session, node=None): - JingleRTPContent.__init__(self, session, 'video', node) - self.setup_stream() - - ''' Things to control the gstreamer's pipeline ''' - def setup_stream(self): - #TODO: Everything is not working properly: - # sometimes, one window won't show up, - # sometimes it'll freeze... - JingleRTPContent.setup_stream(self) - # the local parts - src_vid = gst.element_factory_make('videotestsrc') - src_vid.set_property('is-live', True) - videoscale = gst.element_factory_make('videoscale') - caps = gst.element_factory_make('capsfilter') - caps.set_property('caps', gst.caps_from_string('video/x-raw-yuv, width=320, height=240')) - colorspace = gst.element_factory_make('ffmpegcolorspace') - - self.pipeline.add(src_vid, videoscale, caps, colorspace) - gst.element_link_many(src_vid, videoscale, caps, colorspace) - - self.sink = gst.element_factory_make('xvimagesink') - self.pipeline.add(self.sink) - - colorspace.get_pad('src').link(self.p2psession.get_property('sink-pad')) - self.p2pstream.connect('src-pad-added', self._on_src_pad_added) - - # The following is needed for farsight to process ICE requests: - self.pipeline.set_state(gst.STATE_PLAYING) +from jingle_session import JingleSession +from jingle_rtp import JingleAudio, JingleVideo class ConnectionJingle(object): @@ -1158,11 +98,11 @@ class ConnectionJingle(object): return self.get_jingle_session(jid, media='audio').sid jingle = self.get_jingle_session(jid, media='video') if jingle: - jingle.add_content('voice', JingleVoIP(jingle)) + jingle.add_content('voice', JingleAudio(jingle)) else: jingle = JingleSession(self, weinitiate=True, jid=jid) self.add_jingle(jingle) - jingle.add_content('voice', JingleVoIP(jingle)) + jingle.add_content('voice', JingleAudio(jingle)) jingle.start_session() return jingle.sid diff --git a/src/common/jingle_content.py b/src/common/jingle_content.py new file mode 100644 index 000000000..98b5dfa0a --- /dev/null +++ b/src/common/jingle_content.py @@ -0,0 +1,102 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle contents (XEP 0166). ''' + +contents = {} + +def get_jingle_content(node): + namespace = node.getNamespace() + if namespace in contents: + return contents[namespace](node) + else: + return None + + +class JingleContent(object): + ''' An abstraction of content in Jingle sessions. ''' + def __init__(self, session, transport): + self.session = session + self.transport = transport + # will be filled by JingleSession.add_content() + # don't uncomment these lines, we will catch more buggy code then + # (a JingleContent not added to session shouldn't send anything) + #self.creator = None + #self.name = None + self.accepted = False + self.sent = False + + self.media = None + + self.senders = 'both' #FIXME + self.allow_sending = True # Used for stream direction, attribute 'senders' + + self.callbacks = { + # these are called when *we* get stanzas + 'content-accept': [self.__transportInfoCB], + 'content-add': [self.__transportInfoCB], + 'content-modify': [], + 'content-reject': [], + 'content-remove': [], + 'description-info': [], + 'security-info': [], + 'session-accept': [self.__transportInfoCB], + 'session-info': [], + 'session-initiate': [self.__transportInfoCB], + 'session-terminate': [], + 'transport-info': [self.__transportInfoCB], + 'transport-replace': [], + 'transport-accept': [], + 'transport-reject': [], + 'iq-result': [], + 'iq-error': [], + # these are called when *we* sent these stanzas + 'content-accept-sent': [self.__fillJingleStanza], + 'content-add-sent': [self.__fillJingleStanza], + 'session-initiate-sent': [self.__fillJingleStanza], + 'session-accept-sent': [self.__fillJingleStanza], + 'session-terminate-sent': [], + } + + def is_ready(self): + return (self.accepted and not self.sent) + + def stanzaCB(self, stanza, content, error, action): + ''' Called when something related to our content was sent by peer. ''' + if action in self.callbacks: + for callback in self.callbacks[action]: + callback(stanza, content, error, action) + + def __transportInfoCB(self, stanza, content, error, action): + ''' Got a new transport candidate. ''' + self.transport.transportInfoCB(content.getTag('transport')) + + def __content(self, payload=[]): + ''' Build a XML content-wrapper for our data. ''' + return xmpp.Node('content', + attrs={'name': self.name, 'creator': self.creator}, + payload=payload) + + def send_candidate(self, candidate): + content = self.__content() + content.addChild(self.transport.make_transport([candidate])) + self.session.send_transport_info(content) + + def __fillJingleStanza(self, stanza, content, error, action): + ''' Add our things to session-initiate stanza. ''' + self._fillContent(content) + self.sent = True + content.addChild(node=self.transport.make_transport()) + + def destroy(self): + self.callbacks = None + del self.session.contents[(self.creator, self.name)] diff --git a/src/common/jingle_rtp.py b/src/common/jingle_rtp.py new file mode 100644 index 000000000..7fb9c2611 --- /dev/null +++ b/src/common/jingle_rtp.py @@ -0,0 +1,314 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle RTP sessions (XEP 0167). ''' + + +import gobject + +import xmpp +import farsight, gst + +from jingle_transport import JingleTransportICEUDP +from jingle_content import contents, JingleContent + +# TODO: Will that be even used? +def get_first_gst_element(elements): + ''' Returns, if it exists, the first available element of the list. ''' + for name in elements: + factory = gst.element_factory_find(name) + if factory: + return factory.create() + + +class JingleRTPContent(JingleContent): + def __init__(self, session, media, transport=None): + if transport is None: + transport = JingleTransportICEUDP() + JingleContent.__init__(self, session, transport) + self.media = media + self._dtmf_running = False + self.farsight_media = {'audio': farsight.MEDIA_TYPE_AUDIO, + 'video': farsight.MEDIA_TYPE_VIDEO}[media] + self.got_codecs = False + + self.candidates_ready = False # True when local candidates are prepared + + self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB] + self.callbacks['content-add'] += [self.__getRemoteCodecsCB] + self.callbacks['content-accept'] += [self.__getRemoteCodecsCB, + self.__contentAcceptCB] + self.callbacks['session-accept'] += [self.__getRemoteCodecsCB, + self.__contentAcceptCB] + self.callbacks['session-accept-sent'] += [self.__contentAcceptCB] + self.callbacks['content-accept-sent'] += [self.__contentAcceptCB] + self.callbacks['session-terminate'] += [self.__stop] + self.callbacks['session-terminate-sent'] += [self.__stop] + + def setup_stream(self): + # pipeline and bus + self.pipeline = gst.Pipeline() + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self._on_gst_message) + + # conference + self.conference = gst.element_factory_make('fsrtpconference') + self.conference.set_property("sdes-cname", self.session.ourjid) + self.pipeline.add(self.conference) + self.funnel = None + + self.p2psession = self.conference.new_session(self.farsight_media) + + participant = self.conference.new_participant(self.session.peerjid) + #FIXME: Consider a workaround, here... + # pidgin and telepathy-gabble don't follow the XEP, and it won't work + # due to bad controlling-mode + params = {'controlling-mode': self.session.weinitiate,# 'debug': False} + 'stun-ip': '69.0.208.27', 'debug': False} + + self.p2pstream = self.p2psession.new_stream(participant, + farsight.DIRECTION_RECV, 'nice', params) + + def is_ready(self): + return (JingleContent.is_ready(self) and self.candidates_ready + and self.p2psession.get_property('codecs-ready')) + + def batch_dtmf(self, events): + if self._dtmf_running: + raise Exception #TODO: Proper exception + self._dtmf_running = True + self._start_dtmf(events.pop(0)) + gobject.timeout_add(500, self._next_dtmf, events) + + def _next_dtmf(self, events): + self._stop_dtmf() + if events: + self._start_dtmf(events.pop(0)) + gobject.timeout_add(500, self._next_dtmf, events) + else: + self._dtmf_running = False + + def _start_dtmf(self, event): + if event in ('*', '#'): + event = {'*': farsight.DTMF_EVENT_STAR, + '#': farsight.DTMF_EVENT_POUND}[event] + else: + event = int(event) + self.p2psession.start_telephony_event(event, 2, + farsight.DTMF_METHOD_RTP_RFC4733) + + def _stop_dtmf(self): + self.p2psession.stop_telephony_event(farsight.DTMF_METHOD_RTP_RFC4733) + + def _fillContent(self, content): + content.addChild(xmpp.NS_JINGLE_RTP + ' description', + attrs={'media': self.media}, payload=self.iter_codecs()) + + def _setup_funnel(self): + self.funnel = gst.element_factory_make('fsfunnel') + self.pipeline.add(self.funnel) + self.funnel.set_state(gst.STATE_PLAYING) + self.sink.set_state(gst.STATE_PLAYING) + self.funnel.link(self.sink) + + def _on_src_pad_added(self, stream, pad, codec): + if not self.funnel: + self._setup_funnel() + pad.link(self.funnel.get_pad('sink%d')) + + def _on_gst_message(self, bus, message): + if message.type == gst.MESSAGE_ELEMENT: + name = message.structure.get_name() + if name == 'farsight-new-active-candidate-pair': + pass + elif name == 'farsight-recv-codecs-changed': + pass + elif name == 'farsight-codecs-changed': + if self.is_ready(): + self.session.on_session_state_changed(self) + #TODO: description-info + elif name == 'farsight-local-candidates-prepared': + self.candidates_ready = True + if self.is_ready(): + self.session.on_session_state_changed(self) + elif name == 'farsight-new-local-candidate': + candidate = message.structure['candidate'] + self.transport.candidates.append(candidate) + if self.candidates_ready: + #FIXME: Is this case even possible? + self.send_candidate(candidate) + elif name == 'farsight-component-state-changed': + state = message.structure['state'] + print message.structure['component'], state + if state == farsight.STREAM_STATE_FAILED: + reason = xmpp.Node('reason') + reason.setTag('failed-transport') + self.session._session_terminate(reason) + elif name == 'farsight-error': + print 'Farsight error #%d!' % message.structure['error-no'] + print 'Message: %s' % message.structure['error-msg'] + print 'Debug: %s' % message.structure['debug-msg'] + else: + print name + + def __contentAcceptCB(self, stanza, content, error, action): + if self.accepted: + if self.transport.remote_candidates: + self.p2pstream.set_remote_candidates(self.transport.remote_candidates) + self.transport.remote_candidates = [] + #TODO: farsight.DIRECTION_BOTH only if senders='both' + self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) + self.session.content_negociated(self.media) + + def __getRemoteCodecsCB(self, stanza, content, error, action): + ''' Get peer codecs from what we get from peer. ''' + if self.got_codecs: + return + + codecs = [] + for codec in content.getTag('description').iterTags('payload-type'): + c = farsight.Codec(int(codec['id']), codec['name'], + self.farsight_media, int(codec['clockrate'])) + if 'channels' in codec: + c.channels = int(codec['channels']) + else: + c.channels = 1 + c.optional_params = [(str(p['name']), str(p['value'])) for p in \ + codec.iterTags('parameter')] + codecs.append(c) + + if len(codecs) > 0: + #FIXME: Handle this case: + # glib.GError: There was no intersection between the remote codecs and + # the local ones + self.p2pstream.set_remote_codecs(codecs) + self.got_codecs = True + + def iter_codecs(self): + codecs = self.p2psession.get_property('codecs') + for codec in codecs: + attrs = {'name': codec.encoding_name, + 'id': codec.id, + 'channels': codec.channels} + if codec.clock_rate: + attrs['clockrate'] = codec.clock_rate + if codec.optional_params: + payload = (xmpp.Node('parameter', {'name': name, 'value': value}) + for name, value in codec.optional_params) + else: payload = () + yield xmpp.Node('payload-type', attrs, payload) + + def __stop(self, *things): + self.pipeline.set_state(gst.STATE_NULL) + + def __del__(self): + self.__stop() + + def destroy(self): + JingleContent.destroy(self) + self.p2pstream.disconnect_by_func(self._on_src_pad_added) + self.pipeline.get_bus().disconnect_by_func(self._on_gst_message) + + +class JingleAudio(JingleRTPContent): + ''' Jingle VoIP sessions consist of audio content transported + over an ICE UDP protocol. ''' + def __init__(self, session, transport=None): + JingleRTPContent.__init__(self, session, 'audio', transport) + self.setup_stream() + + + ''' Things to control the gstreamer's pipeline ''' + def setup_stream(self): + JingleRTPContent.setup_stream(self) + + # Configure SPEEX + # Workaround for psi (not needed since rev + # 147aedcea39b43402fe64c533d1866a25449888a): + # place 16kHz before 8kHz, as buggy psi versions will take in + # account only the first codec + + codecs = [farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', + farsight.MEDIA_TYPE_AUDIO, 16000), + farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', + farsight.MEDIA_TYPE_AUDIO, 8000)] + self.p2psession.set_codec_preferences(codecs) + + # the local parts + # TODO: use gconfaudiosink? + # sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink']) + self.sink = gst.element_factory_make('alsasink') + self.sink.set_property('sync', False) + #sink.set_property('latency-time', 20000) + #sink.set_property('buffer-time', 80000) + + # TODO: use gconfaudiosrc? + src_mic = gst.element_factory_make('alsasrc') + src_mic.set_property('blocksize', 320) + + self.mic_volume = gst.element_factory_make('volume') + self.mic_volume.set_property('volume', 1) + + # link gst elements + self.pipeline.add(self.sink, src_mic, self.mic_volume) + src_mic.link(self.mic_volume) + + self.mic_volume.get_pad('src').link(self.p2psession.get_property( + 'sink-pad')) + self.p2pstream.connect('src-pad-added', self._on_src_pad_added) + + # The following is needed for farsight to process ICE requests: + self.pipeline.set_state(gst.STATE_PLAYING) + + +class JingleVideo(JingleRTPContent): + def __init__(self, session, transport=None): + JingleRTPContent.__init__(self, session, 'video', transport) + self.setup_stream() + + ''' Things to control the gstreamer's pipeline ''' + def setup_stream(self): + #TODO: Everything is not working properly: + # sometimes, one window won't show up, + # sometimes it'll freeze... + JingleRTPContent.setup_stream(self) + # the local parts + src_vid = gst.element_factory_make('videotestsrc') + src_vid.set_property('is-live', True) + videoscale = gst.element_factory_make('videoscale') + caps = gst.element_factory_make('capsfilter') + caps.set_property('caps', gst.caps_from_string('video/x-raw-yuv, width=320, height=240')) + colorspace = gst.element_factory_make('ffmpegcolorspace') + + self.pipeline.add(src_vid, videoscale, caps, colorspace) + gst.element_link_many(src_vid, videoscale, caps, colorspace) + + self.sink = gst.element_factory_make('xvimagesink') + self.pipeline.add(self.sink) + + colorspace.get_pad('src').link(self.p2psession.get_property('sink-pad')) + self.p2pstream.connect('src-pad-added', self._on_src_pad_added) + + # The following is needed for farsight to process ICE requests: + self.pipeline.set_state(gst.STATE_PLAYING) + + +def get_content(desc): + if desc['media'] == 'audio': + return JingleAudio + elif desc['media'] == 'video': + return JingleVideo + else: + return None + +contents[xmpp.NS_JINGLE_RTP] = get_content diff --git a/src/common/jingle_session.py b/src/common/jingle_session.py new file mode 100644 index 000000000..9118affdf --- /dev/null +++ b/src/common/jingle_session.py @@ -0,0 +1,613 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle sessions (XEP 0166). ''' + +#TODO: +# * Have JingleContent here +# * 'senders' attribute of 'content' element +# * security preconditions +# * actions: +# - content-modify +# - description-info, session-info +# - security-info +# - transport-accept, transport-reject +# - Tie-breaking +# * timeout + +import gajim #Get rid of that? +import xmpp +from jingle_transport import get_jingle_transport +from jingle_content import get_jingle_content + +#FIXME: Move it to JingleSession.States? +class JingleStates(object): + ''' States in which jingle session may exist. ''' + ended = 0 + pending = 1 + active = 2 + +class OutOfOrder(Exception): + ''' Exception that should be raised when an action is received when in the wrong state. ''' + +class TieBreak(Exception): + ''' Exception that should be raised in case of a tie, when we overrule the other action. ''' + +class JingleSession(object): + ''' This represents one jingle session. ''' + def __init__(self, con, weinitiate, jid, sid=None): + ''' con -- connection object, + weinitiate -- boolean, are we the initiator? + jid - jid of the other entity''' + self.contents = {} # negotiated contents + self.connection = con # connection to use + # our full jid + #FIXME: Get rid of gajim here? + self.ourjid = gajim.get_jid_from_account(self.connection.name) + '/' + \ + con.server_resource + self.peerjid = jid # jid we connect to + # jid we use as the initiator + self.initiator = weinitiate and self.ourjid or self.peerjid + # jid we use as the responder + self.responder = weinitiate and self.peerjid or self.ourjid + # are we an initiator? + self.weinitiate = weinitiate + # what state is session in? (one from JingleStates) + self.state = JingleStates.ended + if not sid: + sid = con.connection.getAnID() + self.sid = sid # sessionid + + self.accepted = True # is this session accepted by user + + # callbacks to call on proper contents + # use .prepend() to add new callbacks, especially when you're going + # to send error instead of ack + self.callbacks = { + 'content-accept': [self.__contentAcceptCB, self.__broadcastCB, + self.__defaultCB], + 'content-add': [self.__contentAddCB, self.__broadcastCB, + self.__defaultCB], #TODO + 'content-modify': [self.__defaultCB], #TODO + 'content-reject': [self.__defaultCB, self.__contentRemoveCB], #TODO + 'content-remove': [self.__defaultCB, self.__contentRemoveCB], + 'description-info': [self.__broadcastCB, self.__defaultCB], #TODO + 'security-info': [self.__defaultCB], #TODO + 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, + self.__broadcastCB, self.__defaultCB], + 'session-info': [self.__sessionInfoCB, self.__broadcastCB, self.__defaultCB], + 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, + self.__defaultCB], + 'session-terminate': [self.__sessionTerminateCB, self.__broadcastAllCB, + self.__defaultCB], + 'transport-info': [self.__broadcastCB, self.__defaultCB], + 'transport-replace': [self.__broadcastCB, self.__transportReplaceCB], #TODO + 'transport-accept': [self.__defaultCB], #TODO + 'transport-reject': [self.__defaultCB], #TODO + 'iq-result': [], + 'iq-error': [self.__errorCB], + } + + ''' Interaction with user ''' + def approve_session(self): + ''' Called when user accepts session in UI (when we aren't the initiator). + ''' + self.accept_session() + + def decline_session(self): + ''' Called when user declines session in UI (when we aren't the initiator) + ''' + reason = xmpp.Node('reason') + reason.addChild('decline') + self._session_terminate(reason) + + def approve_content(self, media): + content = self.get_content(media) + if content: + content.accepted = True + self.on_session_state_changed(content) + + def reject_content(self, media): + content = self.get_content(media) + if content: + if self.state == JingleStates.active: + self.__content_reject(content) + content.destroy() + self.on_session_state_changed() + + def end_session(self): + ''' Called when user stops or cancel session in UI. ''' + reason = xmpp.Node('reason') + if self.state == JingleStates.active: + reason.addChild('success') + else: + reason.addChild('cancel') + self._session_terminate(reason) + + ''' Middle-level functions to manage contents. Handle local content + cache and send change notifications. ''' + def get_content(self, media=None): + if media is None: + return None + + for content in self.contents.values(): + if content.media == media: + return content + + def add_content(self, name, content, creator='we'): + ''' Add new content to session. If the session is active, + this will send proper stanza to update session. + Creator must be one of ('we', 'peer', 'initiator', 'responder')''' + assert creator in ('we', 'peer', 'initiator', 'responder') + + if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \ + not self.weinitiate): + creator = 'initiator' + elif (creator == 'peer' and self.weinitiate) or (creator == 'we' and \ + not self.weinitiate): + creator = 'responder' + content.creator = creator + content.name = name + self.contents[(creator, name)] = content + + if (creator == 'initiator') == self.weinitiate: + # The content is from us, accept it + content.accepted = True + + def remove_content(self, creator, name): + ''' We do not need this now ''' + #TODO: + if (creator, name) in self.contents: + content = self.contents[(creator, name)] + if len(self.contents) > 1: + self.__content_remove(content) + self.contents[(creator, name)].destroy() + if len(self.contents) == 0: + self.end_session() + + def modify_content(self, creator, name, *someother): + ''' We do not need this now ''' + pass + + def on_session_state_changed(self, content=None): + if self.state == JingleStates.ended: + # Session not yet started, only one action possible: session-initiate + if self.is_ready() and self.weinitiate: + self.__session_initiate() + elif self.state == JingleStates.pending: + # We can either send a session-accept or a content-add + if self.is_ready() and not self.weinitiate: + self.__session_accept() + elif content and (content.creator == 'initiator') == self.weinitiate: + self.__content_add(content) + elif content and self.weinitiate: + self.__content_accept(content) + elif self.state == JingleStates.active: + # We can either send a content-add or a content-accept + if not content: + return + if (content.creator == 'initiator') == self.weinitiate: + # We initiated this content. It's a pending content-add. + self.__content_add(content) + else: + # The other side created this content, we accept it. + self.__content_accept(content) + + def is_ready(self): + ''' Returns True when all codecs and candidates are ready + (for all contents). ''' + return (all((content.is_ready() for content in self.contents.itervalues())) + and self.accepted) + + ''' Middle-level function to do stanza exchange. ''' + def accept_session(self): + ''' Mark the session as accepted. ''' + self.accepted = True + self.on_session_state_changed() + + def start_session(self): + ''' Mark the session as ready to be started. ''' + self.accepted = True + self.on_session_state_changed() + + def send_session_info(self): + pass + + def send_content_accept(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-accept') + jingle.addChild(node=content) + self.connection.connection.send(stanza) + + def send_transport_info(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('transport-info') + jingle.addChild(node=content) + self.connection.connection.send(stanza) + + ''' Session callbacks. ''' + def stanzaCB(self, stanza): + ''' A callback for ConnectionJingle. It gets stanza, then + tries to send it to all internally registered callbacks. + First one to raise xmpp.NodeProcessed breaks function.''' + jingle = stanza.getTag('jingle') + error = stanza.getTag('error') + if error: + # it's an iq-error stanza + action = 'iq-error' + elif jingle: + # it's a jingle action + action = jingle.getAttr('action') + if action not in self.callbacks: + self.__send_error(stanza, 'bad_request') + return + #FIXME: If we aren't initiated and it's not a session-initiate... + if action != 'session-initiate' and self.state == JingleStates.ended: + self.__send_error(stanza, 'item-not-found', 'unknown-session') + return + else: + # it's an iq-result (ack) stanza + action = 'iq-result' + + callables = self.callbacks[action] + + try: + for callable in callables: + callable(stanza=stanza, jingle=jingle, error=error, action=action) + except xmpp.NodeProcessed: + pass + except TieBreak: + self.__send_error(stanza, 'conflict', 'tiebreak') + except OutOfOrder: + self.__send_error(stanza, 'unexpected-request', 'out-of-order')#FIXME + + def __defaultCB(self, stanza, jingle, error, action): + ''' Default callback for action stanzas -- simple ack + and stop processing. ''' + response = stanza.buildReply('result') + self.connection.connection.send(response) + + def __errorCB(self, stanza, jingle, error, action): + #FIXME + text = error.getTagData('text') + jingle_error = None + xmpp_error = None + for child in error.getChildren(): + if child.getNamespace() == xmpp.NS_JINGLE_ERRORS: + jingle_error = child.getName() + elif child.getNamespace() == xmpp.NS_STANZAS: + xmpp_error = child.getName() + self.__dispatch_error(xmpp_error, jingle_error, text) + #FIXME: Not sure when we would want to do that... + if xmpp_error == 'item-not-found': + self.connection.delete_jingle_session(self.peerjid, self.sid) + + def __transportReplaceCB(self, stanza, jingle, error, action): + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name'] + if (creator, name) in self.contents: + transport_ns = content.getTag('transport').getNamespace() + if transport_ns == xmpp.JINGLE_ICE_UDP: + #FIXME: We don't manage anything else than ICE-UDP now... + #What was the previous transport?!? + #Anyway, content's transport is not modifiable yet + pass + else: + stanza, jingle = self.__make_jingle('transport-reject') + content = jingle.setTag('content', attrs={'creator': creator, + 'name': name}) + content.setTag('transport', namespace=transport_ns) + self.connection.connection.send(stanza) + raise xmpp.NodeProcessed + else: + #FIXME: This ressource is unknown to us, what should we do? + #For now, reject the transport + stanza, jingle = self.__make_jingle('transport-reject') + c = jingle.setTag('content', attrs={'creator': creator, + 'name': name}) + c.setTag('transport', namespace=transport_ns) + self.connection.connection.send(stanza) + raise xmpp.NodeProcessed + + def __sessionInfoCB(self, stanza, jingle, error, action): + #TODO: ringing, active, (un)hold, (un)mute + payload = jingle.getPayload() + if len(payload) > 0: + self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info') + raise xmpp.NodeProcessed + + def __contentRemoveCB(self, stanza, jingle, error, action): + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name'] + if (creator, name) in self.contents: + content = self.contents[(creator, name)] + #TODO: this will fail if content is not an RTP content + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, content.media, 'removed')) + content.destroy() + if len(self.contents) == 0: + reason = xmpp.Node('reason') + reason.setTag('success') + self._session_terminate(reason) + + def __sessionAcceptCB(self, stanza, jingle, error, action): + if self.state != JingleStates.pending: #FIXME + raise OutOfOrder + self.state = JingleStates.active + + def __contentAcceptCB(self, stanza, jingle, error, action): + ''' Called when we get content-accept stanza or equivalent one + (like session-accept).''' + # check which contents are accepted + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name']#TODO... + + def __contentAddCB(self, stanza, jingle, error, action): + if self.state == JingleStates.ended: + raise OutOfOrder + + parse_result = self.__parse_contents(jingle) + contents = parse_result[2] + rejected_contents = parse_result[3] + + for name, creator in rejected_contents: + #TODO: + content = JingleContent() + self.add_content(name, content, creator) + self.__content_reject(content) + self.contents[(content.creator, content.name)].destroy() + + self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, + contents)) + + def __sessionInitiateCB(self, stanza, jingle, error, action): + ''' We got a jingle session request from other entity, + therefore we are the receiver... Unpack the data, + inform the user. ''' + + if self.state != JingleStates.ended: + raise OutOfOrder + + self.initiator = jingle['initiator'] + self.responder = self.ourjid + self.peerjid = self.initiator + self.accepted = False # user did not accept this session yet + + # TODO: If the initiator is unknown to the receiver (e.g., via presence + # subscription) and the receiver has a policy of not communicating via + # Jingle with unknown entities, it SHOULD return a + # error. + + # Lets check what kind of jingle session does the peer want + contents_ok, transports_ok, contents, pouet = self.__parse_contents(jingle) + + # If there's no content we understand... + if not contents_ok: + # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate + reason = xmpp.Node('reason') + reason.setTag('unsupported-applications') + self.__defaultCB(stanza, jingle, error, action) + self._session_terminate(reason) + raise xmpp.NodeProcessed + + if not transports_ok: + # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate + reason = xmpp.Node('reason') + reason.setTag('unsupported-transports') + self.__defaultCB(stanza, jingle, error, action) + self._session_terminate(reason) + raise xmpp.NodeProcessed + + self.state = JingleStates.pending + + # Send event about starting a session + self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, + contents)) + + def __broadcastCB(self, stanza, jingle, error, action): + ''' Broadcast the stanza contents to proper content handlers. ''' + for content in jingle.iterTags('content'): + name = content['name'] + creator = content['creator'] + cn = self.contents[(creator, name)] + cn.stanzaCB(stanza, content, error, action) + + def __sessionTerminateCB(self, stanza, jingle, error, action): + self.connection.delete_jingle_session(self.peerjid, self.sid) + reason, text = self.__reason_from_stanza(jingle) + if reason not in ('success', 'cancel', 'decline'): + self.__dispatch_error(reason, reason, text) + if text: + text = '%s (%s)' % (reason, text) + else: + text = reason#TODO + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, None, text)) + + def __broadcastAllCB(self, stanza, jingle, error, action): + ''' Broadcast the stanza to all content handlers. ''' + for content in self.contents.itervalues(): + content.stanzaCB(stanza, None, error, action) + + ''' Internal methods. ''' + def __parse_contents(self, jingle): + #TODO: Needs some reworking + contents = [] + contents_rejected = [] + contents_ok = False + transports_ok = False + + for element in jingle.iterTags('content'): + transport = get_jingle_transport(element.getTag('transport')) + content_type = get_jingle_content(element.getTag('description')) + if content_type: + contents_ok = True + if transport: + content = content_type(self, transport) + self.add_content(element['name'], + content, 'peer') + contents.append((content.media,)) + transports_ok = True + else: + contents_rejected.append((element['name'], 'peer')) + else: + contents_rejected.append((element['name'], 'peer')) + + return (contents_ok, transports_ok, contents, contents_rejected) + + def __dispatch_error(self, error, jingle_error=None, text=None): + if jingle_error: + error = jingle_error + if text: + text = '%s (%s)' % (error, text) + else: + text = error + self.connection.dispatch('JINGLE_ERROR', (self.peerjid, self.sid, text)) + + def __reason_from_stanza(self, stanza): + reason = 'success' + reasons = ['success', 'busy', 'cancel', 'connectivity-error', + 'decline', 'expired', 'failed-application', 'failed-transport', + 'general-error', 'gone', 'incompatible-parameters', 'media-error', + 'security-error', 'timeout', 'unsupported-applications', + 'unsupported-transports'] + tag = stanza.getTag('reason') + if tag: + text = tag.getTagData('text') + for r in reasons: + if tag.getTag(r): + reason = r + break + return (reason, text) + + ''' Methods that make/send proper pieces of XML. They check if the session + is in appropriate state. ''' + def __make_jingle(self, action): + stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) + attrs = {'action': action, + 'sid': self.sid} + if action == 'session-initiate': + attrs['initiator'] = self.initiator + elif action == 'session-accept': + attrs['responder'] = self.responder + jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE) + return stanza, jingle + + def __send_error(self, stanza, error, jingle_error=None, text=None): + err = xmpp.Error(stanza, error) + err.setNamespace(xmpp.NS_STANZAS) + if jingle_error: + err.setTag(jingle_error, namespace=xmpp.NS_JINGLE_ERRORS) + if text: + err.setTagData('text', text) + self.connection.connection.send(err) + self.__dispatch_error(error, jingle_error, text) + + def __append_content(self, jingle, content): + ''' Append element to element, + with (full=True) or without (full=False) + children. ''' + jingle.addChild('content', + attrs={'name': content.name, 'creator': content.creator}) + + def __append_contents(self, jingle): + ''' Append all elements to .''' + # TODO: integrate with __appendContent? + # TODO: parameters 'name', 'content'? + for content in self.contents.values(): + self.__append_content(jingle, content) + + def __session_initiate(self): + assert self.state == JingleStates.ended + stanza, jingle = self.__make_jingle('session-initiate') + self.__append_contents(jingle) + self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent') + self.connection.connection.send(stanza) + self.state = JingleStates.pending + + def __session_accept(self): + assert self.state == JingleStates.pending + stanza, jingle = self.__make_jingle('session-accept') + self.__append_contents(jingle) + self.__broadcastCB(stanza, jingle, None, 'session-accept-sent') + self.connection.connection.send(stanza) + self.state = JingleStates.active + + def __session_info(self, payload=None): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('session-info') + if payload: + jingle.addChild(node=payload) + self.connection.connection.send(stanza) + + def _session_terminate(self, reason=None): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('session-terminate') + if reason is not None: + jingle.addChild(node=reason) + self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent') + self.connection.connection.send(stanza) + reason, text = self.__reason_from_stanza(jingle) + if reason not in ('success', 'cancel', 'decline'): + self.__dispatch_error(reason, reason, text) + if text: + text = '%s (%s)' % (reason, text) + else: + text = reason + self.connection.delete_jingle_session(self.peerjid, self.sid) + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, None, text)) + + def __content_add(self, content): + #TODO: test + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-add') + self.__append_content(jingle, content) + self.__broadcastCB(stanza, jingle, None, 'content-add-sent') + self.connection.connection.send(stanza) + + def __content_accept(self, content): + #TODO: test + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-accept') + self.__append_content(jingle, content) + self.__broadcastCB(stanza, jingle, None, 'content-accept-sent') + self.connection.connection.send(stanza) + + def __content_reject(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-reject') + self.__append_content(jingle, content) + self.connection.connection.send(stanza) + #TODO: this will fail if content is not an RTP content + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, content.media, 'rejected')) + + def __content_modify(self): + assert self.state != JingleStates.ended + + def __content_remove(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-remove') + self.__append_content(jingle, content) + self.connection.connection.send(stanza) + #TODO: this will fail if content is not an RTP content + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, content.media, 'removed')) + + def content_negociated(self, media): + self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid, + media)) + diff --git a/src/common/jingle_transport.py b/src/common/jingle_transport.py new file mode 100644 index 000000000..83db8c039 --- /dev/null +++ b/src/common/jingle_transport.py @@ -0,0 +1,139 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle Transports (currently only ICE-UDP). ''' + +import xmpp + +transports = {} + +def get_jingle_transport(node): + namespace = node.getNamespace() + if namespace in transports: + return transports[namespace]() + else: + return None + + +class TransportType(object): + ''' Possible types of a JingleTransport ''' + datagram = 1 + streaming = 2 + + +class JingleTransport(object): + ''' An abstraction of a transport in Jingle sessions. ''' + def __init__(self, type_): + self.type = type_ + self.candidates = [] + self.remote_candidates = [] + + def _iter_candidates(self): + for candidate in self.candidates: + yield self.make_candidate(candidate) + + def make_candidate(self, candidate): + ''' Build a candidate stanza for the given candidate. ''' + pass + + def make_transport(self, candidates=None): + ''' Build a transport stanza with the given candidates (or self.candidates + if candidates is None). ''' + if not candidates: + candidates = self._iter_candidates() + transport = xmpp.Node('transport', payload=candidates) + return transport + + +import farsight + +class JingleTransportICEUDP(JingleTransport): + def __init__(self): + JingleTransport.__init__(self, TransportType.datagram) + + def make_candidate(self, candidate): + types = {farsight.CANDIDATE_TYPE_HOST: 'host', + farsight.CANDIDATE_TYPE_SRFLX: 'srflx', + farsight.CANDIDATE_TYPE_PRFLX: 'prflx', + farsight.CANDIDATE_TYPE_RELAY: 'relay', + farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} + attrs = { + 'component': candidate.component_id, + 'foundation': '1', # hack + 'generation': '0', + 'ip': candidate.ip, + 'network': '0', + 'port': candidate.port, + 'priority': int(candidate.priority), # hack + } + if candidate.type in types: + attrs['type'] = types[candidate.type] + if candidate.proto == farsight.NETWORK_PROTOCOL_UDP: + attrs['protocol'] = 'udp' + else: + # we actually don't handle properly different tcp options in jingle + attrs['protocol'] = 'tcp' + return xmpp.Node('candidate', attrs=attrs) + + def make_transport(self, candidates=None): + transport = JingleTransport.make_transport(self, candidates) + transport.setNamespace(xmpp.NS_JINGLE_ICE_UDP) + if self.candidates and self.candidates[0].username and \ + self.candidates[0].password: + transport.setAttr('ufrag', self.candidates[0].username) + transport.setAttr('pwd', self.candidates[0].password) + return transport + + def transportInfoCB(self, transport): + candidates = [] + for candidate in transport.iterTags('candidate'): + cand = farsight.Candidate() + cand.component_id = int(candidate['component']) + cand.ip = str(candidate['ip']) + cand.port = int(candidate['port']) + cand.foundation = str(candidate['foundation']) + #cand.type = farsight.CANDIDATE_TYPE_LOCAL + cand.priority = int(candidate['priority']) + + if candidate['protocol'] == 'udp': + cand.proto = farsight.NETWORK_PROTOCOL_UDP + else: + # we actually don't handle properly different tcp options in jingle + cand.proto = farsight.NETWORK_PROTOCOL_TCP + + cand.username = str(transport['ufrag']) + cand.password = str(transport['pwd']) + + #FIXME: huh? + types = {'host': farsight.CANDIDATE_TYPE_HOST, + 'srflx': farsight.CANDIDATE_TYPE_SRFLX, + 'prflx': farsight.CANDIDATE_TYPE_PRFLX, + 'relay': farsight.CANDIDATE_TYPE_RELAY, + 'multicast': farsight.CANDIDATE_TYPE_MULTICAST} + if 'type' in candidate and candidate['type'] in types: + cand.type = types[candidate['type']] + else: + print 'Unknown type %s', candidate['type'] + candidates.append(cand) + #FIXME: connectivity should not be etablished yet + # Instead, it should be etablished after session-accept! + #FIXME: + #if len(candidates) > 0: + # if self.sent: + # self.p2pstream.set_remote_candidates(candidates) + # else: + self.remote_candidates.extend(candidates) + #self.p2pstream.set_remote_candidates(candidates) + #print self.media, self.creator, self.name, candidates + +transports[xmpp.NS_JINGLE_ICE_UDP] = JingleTransportICEUDP +