From 9b378d625aa0a8155ed4a4c023e08691baa27ce6 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Mon, 6 Aug 2007 15:53:01 +0000 Subject: [PATCH 01/67] Jingle: Initial support for jingle and jingle-audio. Code isn't linked with the connection code yet. Inside: * A basic form of jingle framework; the goal is to make code reusable for other jingle uses. * Codec "negotiation" for jingle-audio. --- src/common/jingle.py | 345 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 src/common/jingle.py diff --git a/src/common/jingle.py b/src/common/jingle.py new file mode 100644 index 000000000..49b869a5f --- /dev/null +++ b/src/common/jingle.py @@ -0,0 +1,345 @@ +## +## 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 the jingle signalling protocol. ''' + +import xmpp + +class JingleStates(object): + ''' States in which jingle session may exist. ''' + ended=0 + pending=1 + active=2 + +class WrongState(exception): pass +class NoCommonCodec(exception): pass + +class JingleSession(object): + ''' This represents one jingle session. ''' + def __init__(self, con, weinitiate, jid): + ''' 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_full_jid_from_account(self.connection.name) + self.jid=jid # jid we connect to + # jid we use as the initiator + self.initiator=weinitiate and self.ourjid or self.jid + # jid we use as the responder + self.responder=weinitiate and self.jid or self.ourjid + # are we an initiator? + self.weinitiate=weinitiate + # what state is session in? (one from JingleStates) + self.state=JingleStates.ended + self.sid=con.getAnID() # sessionid + + # callbacks to call on proper contents + # use .prepend() to add new callbacks + self.callbacks=dict((key, [self.__defaultCB]) for key in + ('content-accept', 'content-add', 'content-modify', + 'content-remove', 'session-accept', 'session-info', + 'session-initiate', 'session-terminate', + 'transport-info')) + self.callbacks['iq-result']=[] + self.callbacks['iq-error']=[] + + ''' Middle-level functions to manage contents. Handle local content + cache and send change notifications. ''' + def addContent(self, name, description, transport, profile=None): + ''' Add new content to session. If the session is active, + this will send proper stanza to update session. + The protocol prohibits changing that when pending.''' + if self.state==JingleStates.pending: + raise WrongState + + content={'creator': 'initiator', + 'name': name, + 'description': description, + 'transport': transport} + if profile is not None: + content['profile']=profile + self.contents[('initiator', name)]=content + + if self.state==JingleStates.active: + pass # TODO: send proper stanza, shouldn't be needed now + + ''' Middle-level function to do stanza exchange. ''' + def startSession(self): + ''' Start session. ''' + self.__sessionInitiate(self) + + def sendSessionInfo(self): pass + def sendTransportInfo(self): pass + + ''' 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 + callables = 'iq-error' + else if jingle: + # it's a jingle action + action = jingle.getAttr('action') + callables = action + else: + # it's an iq-result (ack) stanza + callables = 'iq-result' + + callables = self.callbacks[callables] + + try: + for callable in callables: + callable(stanza=stanza, jingle=jingle, error=error) + except xmpp.NodeProcessed: + pass + + def __defaultCB(self, stanza, jingle, error): + ''' Default callback for action stanzas -- simple ack + and stop processing. ''' + response = stanza.buildReply('result') + self.connection.send(response) + raise xmpp.NodeProcessed + + ''' Methods that make/send proper pieces of XML. They check if the session + is in appropriate state. ''' + def makeJingle(self, action): + stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.jid)) + jingle = stanza.addChild('jingle', attrs= + 'xmlns': 'http://www.xmpp.org/extensions/xep-0166.html#ns', + 'action': action, + 'initiator': self.initiator, + 'responder': self.responder, + 'sid': self.sid}) + return stanza, jingle + + def appendContent(self, jingle, content, full=True): + ''' Append element to element, + with (full=True) or without (full=False) + children. ''' + c=jingle.addChild('content', attrs={ + 'creator': content['creator'], + 'name': content['name']}) + if 'profile' in content: + c['profile']=content['profile'] + if full: + c.addChild(node=content['description']) + c.addChild(node=content['transport']) + return c + + def appendContents(self, jingle, full=True): + ''' Append all elements to .''' + # TODO: integrate with __appendContent? + # TODO: parameters 'name', 'content'? + for content in self.contents.values(): + self.__appendContent(jingle, content, full=full) + + def __sessionInitiate(self): + assert self.state==JingleStates.ended + + def __sessionAccept(self): + assert self.state==JingleStates.pending + stanza, jingle = self.__jingle('session-accept') + self.__appendContents(jingle, False) + self.connection.send(stanza) + self.state=JingleStates.active + + def __sessionInfo(self, payload=None): + assert self.state!=JingleStates.ended + stanza, jingle = self.__jingle('session-info') + if payload: + jingle.addChild(node=payload) + self.connection.send(stanza) + + def __sessionTerminate(self): + assert self.state!=JingleStates.ended + stanza, jingle = self.__jingle('session-terminate') + self.connection.send(stanza) + + def __contentAdd(self): + assert self.state==JingleStates.active + + def __contentAccept(self): + assert self.state!=JingleStates.ended + + def __contentModify(self): + assert self.state!=JingleStates.ended + + def __contentRemove(self): + assert self.state!=JingleStates.ended + + def __transportInfo(self): + assert self.state!=JingleStates.ended + + '''Callbacks''' + def sessionInitiateCB(self, stanza): + ''' We got a jingle session request from other entity, + therefore we are the receiver... Unpack the data. ''' + jingle = stanza.getTag('jingle') + self.initiator = jingle['initiator'] + self.responder = self.ourjid + self.jid = self.initiator + self.state = JingleStates.pending + self.sid = jingle['sid'] + for element in jingle.iterTags('content'): + content={'creator': 'initiator', + 'name': element['name'] + 'description': element.getTag('description'), + 'transport': element.getTag('transport')} + if element.has_attr('profile'): + content['profile']=element['profile'] + self.contents[('initiator', content['name'])]=content + + def sessionTerminateCB(self, stanza): pass + +class JingleAudioSession(object): + class Codec(object): + ''' This class keeps description of a single codec. ''' + def __init__(self, name, id=None, **params): + ''' Create new codec description. ''' + self.name = name + self.id = id + self.attrs = {'name': self.name, 'id': self.id, 'channels': 1} + for key in ('channels', 'clockrate', 'maxptime', 'ptime'): + if key in params: + self.attrs[key]=params[key] + del params[key] + self.params = params + + def __eq__(a, b): + ''' Compare two codec descriptions. ''' + # TODO: check out what should be tested... + if a.name!=b.name: return False + # ... + return True + + def toXML(self): + return xmpp.Node('payload', + attrs=self.attrs, + payload=(xmpp.Node('parameter', {'name': k, 'value': v}) for k,v in self.params)) + + def __init__(self, con, weinitiate, jid): + JingleSession.__init__(self, con, weinitiate, jid) + if weinitiate: + pass #add voice content + self.callbacks['session-initiate'].prepend( + + self.initiator_codecs=[] + self.responder_codecs=[] + + ''' "Negotiation" of codecs... simply presenting what *we* can do, nothing more... ''' + def getOurCodecs(self, other=None): + ''' Get a list of codecs we support. Try to get them in the same + order as the codecs of our peer. If other!=None, raise + a NoCommonCodec error if no codecs both sides support (None means + we are initiating the connection and we don't know the other + peer's codecs.) ''' + # for now we "understand" only one codec -- speex with clockrate 16000 + # so we have an easy job to do... (codecs sorted in order of preference) + supported_codecs=[ + Codec('speex', clockrate='16000'), + ] + + other_l = other if other is not None else [] + our_l = supported_codecs[:] + out = [] + ids = range(128) + for codec in other: + if codec in our_l: + out.append(codec) + our_l.remove(codec) + try: ids.remove(codec.id) + except ValueError: pass # when id is not a dynamic one + + if other is not None and len(out)==0: + raise NoCommonCodec + + for codec in our_l: + if not codec.id or codec.id not in ids: + codec.id = ids.pop() + out.append(codec) + + return out + + ''' Methods for making proper pieces of XML. ''' + def __codecsList(self, codecs): + ''' Prepares a description element with codecs given as a parameter. ''' + return xmpp.Node('description', + xmlns=xmpp.NS_JINGLE_AUDIO, + payload=(codec.toXML() for codec in codecs)) + +class JingleICEUDPSession(object): + def __init__(self, con, weinitiate, jid): + pass + +class JingleVoiP(JingleSession): + ''' Jingle VoiP sessions consist of audio content transported + over an ICE UDP protocol. ''' + def __init__(*data): + JingleAudioSession.__init__(*data) + JingleICEUDPSession.__init__(*data) + +class ConnectionJingle(object): + ''' This object depends on that it is a part of Connection class. ''' + def __init__(self): + # dictionary: (jid, sessionid) => JingleSession object + self.__sessions = {} + + # dictionary: (jid, iq stanza id) => JingleSession object, + # one time callbacks + self.__iq_responses = {} + + def addJingle(self, jingle): + ''' Add a jingle session to a jingle stanza dispatcher + jingle - a JingleSession object. + ''' + self.__sessions[(jingle.jid, jingle.sid)]=jingle + + def deleteJingle(self, jingle): + ''' Remove a jingle session from a jingle stanza dispatcher ''' + del self.__session[(jingle.jid, jingle.sid)] + + def _jingleCB(self, con, stanza): + ''' The jingle stanza dispatcher. + Route jingle stanza to proper JingleSession object, + or create one if it is a new session. + TODO: Also check if the stanza isn't an error stanza, if so + route it adequatelly.''' + + # get data + jid = stanza.getFrom() + id = stanza.getID() + + if (jid, id) in self.__iq_responses.keys(): + self.__iq_responses[(jid, id)].stanzaCB(stanza) + del self.__iq_responses[(jid, id)] + raise xmpp.NodeProcessed + + jingle = stanza.getTag('jingle') + sid = jingle.getAttr('sid') + + # do we need to create a new jingle object + if (jid, sid) not in self.__sessions: + # we should check its type here... + newjingle = JingleAudioSession(con=self, weinitiate=False, jid=jid) + self.addJingle(newjingle) + + # we already have such session in dispatcher... + return self.__sessions[(jid, sid)].stanzaCB(stanza) + + def addJingleIqCallback(jid, id, jingle): + self.__iq_responses[(jid, id)]=jingle From 459c73f9619c4b83cd6837d77906ebea1b0505d8 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Mon, 6 Aug 2007 23:19:57 +0000 Subject: [PATCH 02/67] Jingle: UI entry point and lots of small changes. --- data/glade/chat_control_popup_menu.glade | 9 ++ src/chat_control.py | 8 ++ src/common/connection_handlers.py | 8 +- src/common/jingle.py | 151 ++++++++++++++++------- src/common/meta.py | 36 ++++++ src/common/xmpp/protocol.py | 4 + 6 files changed, 171 insertions(+), 45 deletions(-) create mode 100644 src/common/meta.py diff --git a/data/glade/chat_control_popup_menu.glade b/data/glade/chat_control_popup_menu.glade index c3ebb1e24..61b267416 100644 --- a/data/glade/chat_control_popup_menu.glade +++ b/data/glade/chat_control_popup_menu.glade @@ -65,6 +65,15 @@ + + + True + Start _Voice chat + True + + + + True diff --git a/src/chat_control.py b/src/chat_control.py index 9bf90ced9..91ca40c13 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1191,6 +1191,10 @@ class ChatControl(ChatControlBase): gajim.config.set_per('contacts', self.contact.jid, 'gpg_enabled', widget.get_active()) + def _on_start_voip_menuitem_activate(self, *things): + print 'Start VoiP' + gajim.connections[self.account].startVoiP(self.contact.jid) + def _update_gpg(self): tb = self.xml.get_widget('gpg_togglebutton') # we can do gpg @@ -1533,6 +1537,7 @@ class ChatControl(ChatControlBase): history_menuitem = xml.get_widget('history_menuitem') toggle_gpg_menuitem = xml.get_widget('toggle_gpg_menuitem') + start_voip_menuitem = xml.get_widget('start_voip_menuitem') add_to_roster_menuitem = xml.get_widget('add_to_roster_menuitem') send_file_menuitem = xml.get_widget('send_file_menuitem') information_menuitem = xml.get_widget('information_menuitem') @@ -1583,6 +1588,9 @@ class ChatControl(ChatControlBase): id = toggle_gpg_menuitem.connect('activate', self._on_toggle_gpg_menuitem_activate) self.handlers[id] = toggle_gpg_menuitem + id = start_voip_menuitem.connect('activate', + self._on_start_voip_menuitem_activate) + self.handlers[id] = start_voip_menuitem id = information_menuitem.connect('activate', self._on_contact_information_menuitem_activate) self.handlers[id] = information_menuitem diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index bf47b95a9..2bd258ae9 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -38,6 +38,7 @@ from common import exceptions from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub from common.caps import ConnectionCaps +from common.jingle import ConnectionJingle STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'error'] @@ -1174,12 +1175,13 @@ class ConnectionVcard: #('VCARD', {entry1: data, entry2: {entry21: data, ...}, ...}) self.dispatch('VCARD', vcard) -class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionCaps): +class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionCaps, ConnectionJingle): def __init__(self): ConnectionVcard.__init__(self) ConnectionBytestream.__init__(self) ConnectionCommands.__init__(self) ConnectionPubSub.__init__(self) + ConnectionJingle.__init__(self) self.gmail_url=None # List of IDs we are waiting answers for {id: (type_of_request, data), } self.awaiting_answers = {} @@ -2099,6 +2101,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, con.RegisterHandler('iq', self._search_fields_received, 'result', common.xmpp.NS_SEARCH) con.RegisterHandler('iq', self._PubSubCB, 'result') + con.RegisterHandler('iq', self._JingleCB, 'result') + con.RegisterHandler('iq', self._JingleCB, 'error') + con.RegisterHandler('iq', self._JingleCB, 'set', + common.xmpp.NS_JINGLE) con.RegisterHandler('iq', self._ErrorCB, 'error') con.RegisterHandler('iq', self._IqCB) con.RegisterHandler('iq', self._StanzaArrivedCB) diff --git a/src/common/jingle.py b/src/common/jingle.py index 49b869a5f..dac64e4c1 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -12,19 +12,24 @@ ## ''' Handles the jingle signalling protocol. ''' +import gajim import xmpp +import meta + class JingleStates(object): ''' States in which jingle session may exist. ''' ended=0 pending=1 active=2 -class WrongState(exception): pass -class NoCommonCodec(exception): pass +class Exception(object): pass +class WrongState(Exception): pass +class NoCommonCodec(Exception): pass class JingleSession(object): ''' This represents one jingle session. ''' + __metaclass__=meta.VerboseClassType def __init__(self, con, weinitiate, jid): ''' con -- connection object, weinitiate -- boolean, are we the initiator? @@ -32,48 +37,59 @@ class JingleSession(object): self.contents={} # negotiated contents self.connection=con # connection to use # our full jid - self.ourjid=gajim.get_full_jid_from_account(self.connection.name) - self.jid=jid # jid we connect to + 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.jid + self.initiator=weinitiate and self.ourjid or self.peerjid # jid we use as the responder - self.responder=weinitiate and self.jid or self.ourjid + 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 - self.sid=con.getAnID() # sessionid + self.sid=con.connection.getAnID() # sessionid # callbacks to call on proper contents # use .prepend() to add new callbacks self.callbacks=dict((key, [self.__defaultCB]) for key in - ('content-accept', 'content-add', 'content-modify', + ('content-add', 'content-modify', 'content-remove', 'session-accept', 'session-info', 'session-initiate', 'session-terminate', 'transport-info')) self.callbacks['iq-result']=[] self.callbacks['iq-error']=[] + self.callbacks['content-accept']=[self.__contentAcceptCB, self.__defaultCB] + ''' Middle-level functions to manage contents. Handle local content cache and send change notifications. ''' - def addContent(self, name, description, transport, profile=None): + def addContent(self, name, content, initiator='we'): ''' Add new content to session. If the session is active, this will send proper stanza to update session. - The protocol prohibits changing that when pending.''' + The protocol prohibits changing that when pending. + Initiator must be one of ('we', 'peer', 'initiator', 'responder')''' if self.state==JingleStates.pending: raise WrongState - content={'creator': 'initiator', - 'name': name, - 'description': description, - 'transport': transport} - if profile is not None: - content['profile']=profile - self.contents[('initiator', name)]=content + if (initiator=='we' and self.weinitiate) or (initiator=='peer' and not self.weinitiate): + initiator='initiator' + elif (initiator=='peer' and self.weinitiate) or (initiator=='we' and not self.weinitiate): + initiator='responder' + content.creator = initiator + content.name = name + self.contents[(initiator,name)]=content if self.state==JingleStates.active: pass # TODO: send proper stanza, shouldn't be needed now + def removeContent(self, creator, name): + ''' We do not need this now ''' + pass + + def modifyContent(self, creator, name, *someother): + ''' We do not need this now ''' + pass + ''' Middle-level function to do stanza exchange. ''' def startSession(self): ''' Start session. ''' @@ -92,7 +108,7 @@ class JingleSession(object): if error: # it's an iq-error stanza callables = 'iq-error' - else if jingle: + elif jingle: # it's a jingle action action = jingle.getAttr('action') callables = action @@ -115,11 +131,20 @@ class JingleSession(object): self.connection.send(response) raise xmpp.NodeProcessed + def __contentAcceptCB(self, stanza, jingle, error): + ''' Called when we get content-accept stanza or equivalent one + (like session-accept).''' + # check which contents are accepted, call their callbacks + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name'] + + ''' Methods that make/send proper pieces of XML. They check if the session is in appropriate state. ''' - def makeJingle(self, action): + def __makeJingle(self, action): stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.jid)) - jingle = stanza.addChild('jingle', attrs= + jingle = stanza.addChild('jingle', attrs={ 'xmlns': 'http://www.xmpp.org/extensions/xep-0166.html#ns', 'action': action, 'initiator': self.initiator, @@ -127,21 +152,18 @@ class JingleSession(object): 'sid': self.sid}) return stanza, jingle - def appendContent(self, jingle, content, full=True): + def __appendContent(self, jingle, content, full=True): ''' Append element to element, with (full=True) or without (full=False) children. ''' - c=jingle.addChild('content', attrs={ - 'creator': content['creator'], - 'name': content['name']}) - if 'profile' in content: - c['profile']=content['profile'] if full: - c.addChild(node=content['description']) - c.addChild(node=content['transport']) + jingle.addChild(node=content.toXML()) + else: + jingle.addChild('content', + attrs={'name': content.name, 'creator': content.creator}) return c - def appendContents(self, jingle, full=True): + def __appendContents(self, jingle, full=True): ''' Append all elements to .''' # TODO: integrate with __appendContent? # TODO: parameters 'name', 'content'? @@ -150,6 +172,9 @@ class JingleSession(object): def __sessionInitiate(self): assert self.state==JingleStates.ended + stanza, jingle = self.__makeJingle('session-initiate') + self.__appendContents(jingle) + self.connection.send(jingle) def __sessionAccept(self): assert self.state==JingleStates.pending @@ -197,7 +222,7 @@ class JingleSession(object): self.sid = jingle['sid'] for element in jingle.iterTags('content'): content={'creator': 'initiator', - 'name': element['name'] + 'name': element['name'], 'description': element.getTag('description'), 'transport': element.getTag('transport')} if element.has_attr('profile'): @@ -207,6 +232,7 @@ class JingleSession(object): def sessionTerminateCB(self, stanza): pass class JingleAudioSession(object): + __metaclass__=meta.VerboseClassType class Codec(object): ''' This class keeps description of a single codec. ''' def __init__(self, name, id=None, **params): @@ -232,15 +258,15 @@ class JingleAudioSession(object): attrs=self.attrs, payload=(xmpp.Node('parameter', {'name': k, 'value': v}) for k,v in self.params)) - def __init__(self, con, weinitiate, jid): - JingleSession.__init__(self, con, weinitiate, jid) - if weinitiate: - pass #add voice content - self.callbacks['session-initiate'].prepend( + def __init__(self, content): + self.content = content self.initiator_codecs=[] self.responder_codecs=[] + def sessionInitiateCB(self, stanza, ourcontent): + pass + ''' "Negotiation" of codecs... simply presenting what *we* can do, nothing more... ''' def getOurCodecs(self, other=None): ''' Get a list of codecs we support. Try to get them in the same @@ -282,16 +308,46 @@ class JingleAudioSession(object): xmlns=xmpp.NS_JINGLE_AUDIO, payload=(codec.toXML() for codec in codecs)) + def toXML(self): + if not self.initiator_codecs: + # we are the initiator, so just send our codecs + self.initiator_codecs = self.getOurCodecs() + return self.__codecsList(self.initiator_codecs) + else: + # we are the responder, we SHOULD adjust our codec list + self.responder_codecs = self.getOurCodecs(self.initiator_codecs) + return self.__codecsList(self.responder_codecs) + class JingleICEUDPSession(object): - def __init__(self, con, weinitiate, jid): + __metaclass__=meta.VerboseClassType + def __init__(self, content): + self.content = content + + def _sessionInitiateCB(self): + ''' Called when we initiate the session. ''' pass -class JingleVoiP(JingleSession): + def toXML(self): + ''' ICE-UDP doesn't send much in its transport stanza... ''' + return xmpp.Node('transport', xmlns=xmpp.JINGLE_ICE_UDP) + +class JingleVoiP(object): ''' Jingle VoiP sessions consist of audio content transported over an ICE UDP protocol. ''' - def __init__(*data): - JingleAudioSession.__init__(*data) - JingleICEUDPSession.__init__(*data) + __metaclass__=meta.VerboseClassType + def __init__(self): + self.audio = JingleAudioSession(self) + self.transport = JingleICEUDPSession(self) + + def toXML(self): + ''' Return proper XML for element. ''' + return xmpp.Node('content', + attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, + childs=[self.audio.toXML(), self.transport.toXML()]) + + def _sessionInitiateCB(self): + ''' Called when we initiate the session. ''' + self.transport._sessionInitiateCB() class ConnectionJingle(object): ''' This object depends on that it is a part of Connection class. ''' @@ -307,13 +363,13 @@ class ConnectionJingle(object): ''' Add a jingle session to a jingle stanza dispatcher jingle - a JingleSession object. ''' - self.__sessions[(jingle.jid, jingle.sid)]=jingle + self.__sessions[(jingle.peerjid, jingle.sid)]=jingle def deleteJingle(self, jingle): ''' Remove a jingle session from a jingle stanza dispatcher ''' - del self.__session[(jingle.jid, jingle.sid)] + del self.__session[(jingle.peerjid, jingle.sid)] - def _jingleCB(self, con, stanza): + def _JingleCB(self, con, stanza): ''' The jingle stanza dispatcher. Route jingle stanza to proper JingleSession object, or create one if it is a new session. @@ -330,6 +386,7 @@ class ConnectionJingle(object): raise xmpp.NodeProcessed jingle = stanza.getTag('jingle') + if not jingle: return sid = jingle.getAttr('sid') # do we need to create a new jingle object @@ -341,5 +398,11 @@ class ConnectionJingle(object): # we already have such session in dispatcher... return self.__sessions[(jid, sid)].stanzaCB(stanza) - def addJingleIqCallback(jid, id, jingle): + def addJingleIqCallback(self, jid, id, jingle): self.__iq_responses[(jid, id)]=jingle + + def startVoiP(self, jid): + jingle = JingleSession(self, weinitiate=True, jid=jid) + self.addJingle(jingle) + jingle.addContent('voice', JingleVoiP()) + jingle.startSession() diff --git a/src/common/meta.py b/src/common/meta.py new file mode 100644 index 000000000..153c2e952 --- /dev/null +++ b/src/common/meta.py @@ -0,0 +1,36 @@ +#!/usr/bin/python + +import types + +class VerboseClassType(type): + indent = '' + + def __init__(cls, name, bases, dict): + super(VerboseClassType, cls).__init__(cls, name, bases, dict) + new = {} + print 'Initializing new class %s:' % cls + for fname, fun in dict.iteritems(): + wrap = hasattr(fun, '__call__') + print '%s%s is %s, we %s wrap it.' % \ + (cls.__class__.indent, fname, fun, wrap and 'will' or "won't") + if not wrap: continue + setattr(cls, fname, cls.wrap(name, fname, fun)) + + def wrap(cls, name, fname, fun): + def verbose(*a, **b): + args = ', '.join(map(repr, a)+map(lambda x:'%s=%r'%x, b.iteritems())) + print '%s%s.%s(%s):' % (cls.__class__.indent, name, fname, args) + cls.__class__.indent += '| ' + r = fun(*a, **b) + cls.__class__.indent = cls.__class__.indent[:-4] + print '%s+=%r' % (cls.__class__.indent, r) + return r + verbose.__name__ = fname + return verbose + +def nested_property(f): + ret = f() + p = {} + for v in ('fget', 'fset', 'fdel', 'doc'): + if v in ret: p[v]=ret[v] + return property(**p) diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 79883c461..0cce76377 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -55,6 +55,10 @@ NS_HTTP_BIND ='http://jabber.org/protocol/httpbind' # XEP-01 NS_IBB ='http://jabber.org/protocol/ibb' NS_INVISIBLE ='presence-invisible' # Jabberd2 NS_IQ ='iq' # Jabberd2 +NS_JINGLE ='http://www.xmpp.org/extensions/xep-0166.html#ns' # XEP-0166 +NS_JINGLE_AUDIO ='http://www.xmpp.org/extensions/xep-0167.html#ns' # XEP-0167 +NS_JINGLE_RAW_UDP='http://www.xmpp.org/extensions/xep-0177.html#ns' # XEP-0177 +NS_JINGLE_ICE_UDP='http://www.xmpp.org/extensions/xep-0176.html#ns-udp' # XEP-0176 NS_LAST ='jabber:iq:last' NS_MESSAGE ='message' # Jabberd2 NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107 From fdf9d42e575b167a78750999884d8c15e4c2548e Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Tue, 7 Aug 2007 16:34:09 +0000 Subject: [PATCH 03/67] Jingle: fixes for content-description negotiation. --- src/chat_control.py | 3 +- src/common/jingle.py | 182 ++++++++++++++++++++++++------------------- 2 files changed, 104 insertions(+), 81 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 91ca40c13..5be477f76 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1192,8 +1192,7 @@ class ChatControl(ChatControlBase): widget.get_active()) def _on_start_voip_menuitem_activate(self, *things): - print 'Start VoiP' - gajim.connections[self.account].startVoiP(self.contact.jid) + gajim.connections[self.account].startVoiP(self.contact.jid+'/'+self.contact.resource) def _update_gpg(self): tb = self.xml.get_widget('gpg_togglebutton') diff --git a/src/common/jingle.py b/src/common/jingle.py index dac64e4c1..737fdfb45 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -30,7 +30,7 @@ class NoCommonCodec(Exception): pass class JingleSession(object): ''' This represents one jingle session. ''' __metaclass__=meta.VerboseClassType - def __init__(self, con, weinitiate, jid): + def __init__(self, con, weinitiate, jid, sid=None): ''' con -- connection object, weinitiate -- boolean, are we the initiator? jid - jid of the other entity''' @@ -47,19 +47,26 @@ class JingleSession(object): self.weinitiate=weinitiate # what state is session in? (one from JingleStates) self.state=JingleStates.ended - self.sid=con.connection.getAnID() # sessionid + if not sid: + sid=con.connection.getAnID() + self.sid=sid # sessionid # callbacks to call on proper contents - # use .prepend() to add new callbacks - self.callbacks=dict((key, [self.__defaultCB]) for key in - ('content-add', 'content-modify', - 'content-remove', 'session-accept', 'session-info', - 'session-initiate', 'session-terminate', - 'transport-info')) - self.callbacks['iq-result']=[] - self.callbacks['iq-error']=[] - - self.callbacks['content-accept']=[self.__contentAcceptCB, self.__defaultCB] + # use .prepend() to add new callbacks, especially when you're going + # to send error instead of ack + self.callbacks={ + 'content-accept': [self.__contentAcceptCB, self.__defaultCB], + 'content-add': [self.__defaultCB], + 'content-modify': [self.__defaultCB], + 'content-remove': [self.__defaultCB], + 'session-accept': [self.__contentAcceptCB, self.__defaultCB], + 'session-info': [self.__defaultCB], + 'session-initiate': [self.__sessionInitiateCB, self.__defaultCB], + 'session-terminate': [self.__defaultCB], + 'transport-info': [self.__defaultCB], + 'iq-result': [], + 'iq-error': [], + } ''' Middle-level functions to manage contents. Handle local content cache and send change notifications. ''' @@ -93,7 +100,7 @@ class JingleSession(object): ''' Middle-level function to do stanza exchange. ''' def startSession(self): ''' Start session. ''' - self.__sessionInitiate(self) + self.__sessionInitiate() def sendSessionInfo(self): pass def sendTransportInfo(self): pass @@ -128,8 +135,7 @@ class JingleSession(object): ''' Default callback for action stanzas -- simple ack and stop processing. ''' response = stanza.buildReply('result') - self.connection.send(response) - raise xmpp.NodeProcessed + self.connection.connection.send(response) def __contentAcceptCB(self, stanza, jingle, error): ''' Called when we get content-accept stanza or equivalent one @@ -140,10 +146,37 @@ class JingleSession(object): name = content['name'] + def sessionInitiateCB(self, stanza): + ''' We got a jingle session request from other entity, + therefore we are the receiver... Unpack the data. ''' + jingle = stanza.getTag('jingle') + self.initiator = jingle['initiator'] + self.responder = self.ourjid + self.jid = self.initiator + + fail = True + for element in jingle.iterTags('content'): + # checking what kind of session this will be + desc_ns = element.getTag('description').getNamespace() + tran_ns = element.getTag('transport').getNamespace() + if desc_ns==xmpp.NS_JINGLE_AUDIO and tran_ns==xmpp.NS_JINGLE_ICE_UDP: + # we've got voip content + self.addContent(element['name'], JingleVoiP(self, node=element), 'peer') + fail = False + + if fail: + # TODO: we should send inside too + self.connection.connection.send( + xmpp.Error(stanza, xmpp.NS_STANZAS + 'feature-not-implemented')) + self.connection.deleteJingle(self) + raise xmpp.NodeProcessed + + self.state = JingleStates.pending + ''' Methods that make/send proper pieces of XML. They check if the session is in appropriate state. ''' def __makeJingle(self, action): - stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.jid)) + stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) jingle = stanza.addChild('jingle', attrs={ 'xmlns': 'http://www.xmpp.org/extensions/xep-0166.html#ns', 'action': action, @@ -161,7 +194,6 @@ class JingleSession(object): else: jingle.addChild('content', attrs={'name': content.name, 'creator': content.creator}) - return c def __appendContents(self, jingle, full=True): ''' Append all elements to .''' @@ -174,13 +206,13 @@ class JingleSession(object): assert self.state==JingleStates.ended stanza, jingle = self.__makeJingle('session-initiate') self.__appendContents(jingle) - self.connection.send(jingle) + self.connection.connection.send(stanza) def __sessionAccept(self): assert self.state==JingleStates.pending stanza, jingle = self.__jingle('session-accept') self.__appendContents(jingle, False) - self.connection.send(stanza) + self.connection.connection.send(stanza) self.state=JingleStates.active def __sessionInfo(self, payload=None): @@ -188,12 +220,12 @@ class JingleSession(object): stanza, jingle = self.__jingle('session-info') if payload: jingle.addChild(node=payload) - self.connection.send(stanza) + self.connection.connection.send(stanza) def __sessionTerminate(self): assert self.state!=JingleStates.ended stanza, jingle = self.__jingle('session-terminate') - self.connection.send(stanza) + self.connection.connection.send(stanza) def __contentAdd(self): assert self.state==JingleStates.active @@ -211,59 +243,49 @@ class JingleSession(object): assert self.state!=JingleStates.ended '''Callbacks''' - def sessionInitiateCB(self, stanza): - ''' We got a jingle session request from other entity, - therefore we are the receiver... Unpack the data. ''' - jingle = stanza.getTag('jingle') - self.initiator = jingle['initiator'] - self.responder = self.ourjid - self.jid = self.initiator - self.state = JingleStates.pending - self.sid = jingle['sid'] - for element in jingle.iterTags('content'): - content={'creator': 'initiator', - 'name': element['name'], - 'description': element.getTag('description'), - 'transport': element.getTag('transport')} - if element.has_attr('profile'): - content['profile']=element['profile'] - self.contents[('initiator', content['name'])]=content - def sessionTerminateCB(self, stanza): pass +class Codec(object): + ''' This class keeps description of a single codec. ''' + def __init__(self, name, id=None, **params): + ''' Create new codec description. ''' + self.name = name + self.id = id + self.attrs = {'name': self.name, 'id': self.id, 'channels': 1} + for key in ('channels', 'clockrate', 'maxptime', 'ptime'): + if key in params: + self.attrs[key]=params[key] + del params[key] + self.params = params + + def __eq__(a, b): + ''' Compare two codec descriptions. ''' + # TODO: check out what should be tested... + if a.name!=b.name: return False + # ... + return True + + def toXML(self): + return xmpp.Node('payload', + attrs=self.attrs, + payload=(xmpp.Node('parameter', {'name': k, 'value': v}) for k,v in self.params)) + class JingleAudioSession(object): __metaclass__=meta.VerboseClassType - class Codec(object): - ''' This class keeps description of a single codec. ''' - def __init__(self, name, id=None, **params): - ''' Create new codec description. ''' - self.name = name - self.id = id - self.attrs = {'name': self.name, 'id': self.id, 'channels': 1} - for key in ('channels', 'clockrate', 'maxptime', 'ptime'): - if key in params: - self.attrs[key]=params[key] - del params[key] - self.params = params - - def __eq__(a, b): - ''' Compare two codec descriptions. ''' - # TODO: check out what should be tested... - if a.name!=b.name: return False - # ... - return True - - def toXML(self): - return xmpp.Node('payload', - attrs=self.attrs, - payload=(xmpp.Node('parameter', {'name': k, 'value': v}) for k,v in self.params)) - - def __init__(self, content): + def __init__(self, content, fromNode): self.content = content self.initiator_codecs=[] self.responder_codecs=[] + if fromNode: + # read all codecs peer understand + for payload in fromNode.iterTags('payload-type'): + attrs = fromNode.getAttrs().copy() + for param in fromNode.iterTags('parameter'): + attrs[param['name']]=param['value'] + self.initiator_codecs.append(Codec(**attrs)) + def sessionInitiateCB(self, stanza, ourcontent): pass @@ -284,7 +306,7 @@ class JingleAudioSession(object): our_l = supported_codecs[:] out = [] ids = range(128) - for codec in other: + for codec in other_l: if codec in our_l: out.append(codec) our_l.remove(codec) @@ -304,8 +326,7 @@ class JingleAudioSession(object): ''' Methods for making proper pieces of XML. ''' def __codecsList(self, codecs): ''' Prepares a description element with codecs given as a parameter. ''' - return xmpp.Node('description', - xmlns=xmpp.NS_JINGLE_AUDIO, + return xmpp.Node(xmpp.NS_JINGLE_AUDIO+' description', payload=(codec.toXML() for codec in codecs)) def toXML(self): @@ -329,25 +350,26 @@ class JingleICEUDPSession(object): def toXML(self): ''' ICE-UDP doesn't send much in its transport stanza... ''' - return xmpp.Node('transport', xmlns=xmpp.JINGLE_ICE_UDP) + return xmpp.Node(xmpp.NS_JINGLE_ICE_UDP+' transport') class JingleVoiP(object): ''' Jingle VoiP sessions consist of audio content transported over an ICE UDP protocol. ''' __metaclass__=meta.VerboseClassType - def __init__(self): - self.audio = JingleAudioSession(self) + def __init__(self, session, node=None): + self.session = session + + if node is None: + self.audio = JingleAudioSession(self) + else: + self.audio = JingleAudioSession(self, node.getTag('content')) self.transport = JingleICEUDPSession(self) def toXML(self): ''' Return proper XML for element. ''' return xmpp.Node('content', attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, - childs=[self.audio.toXML(), self.transport.toXML()]) - - def _sessionInitiateCB(self): - ''' Called when we initiate the session. ''' - self.transport._sessionInitiateCB() + payload=[self.audio.toXML(), self.transport.toXML()]) class ConnectionJingle(object): ''' This object depends on that it is a part of Connection class. ''' @@ -391,12 +413,14 @@ class ConnectionJingle(object): # do we need to create a new jingle object if (jid, sid) not in self.__sessions: - # we should check its type here... - newjingle = JingleAudioSession(con=self, weinitiate=False, jid=jid) + # TODO: we should check its type here... + newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) self.addJingle(newjingle) # we already have such session in dispatcher... - return self.__sessions[(jid, sid)].stanzaCB(stanza) + self.__sessions[(jid, sid)].stanzaCB(stanza) + + raise xmpp.NodeProcessed def addJingleIqCallback(self, jid, id, jingle): self.__iq_responses[(jid, id)]=jingle From d6e626799c355d310c8415926f3c37cf7781e422 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Wed, 8 Aug 2007 22:51:27 +0000 Subject: [PATCH 04/67] Jingle: farsight bindings started --- src/common/farsight/Makefile | 19 + src/common/farsight/README | 3 + src/common/farsight/farsight.defs | 530 ++++++++++++++++++++++++++ src/common/farsight/farsight.override | 36 ++ src/common/farsight/farsightmodule.c | 24 ++ 5 files changed, 612 insertions(+) create mode 100644 src/common/farsight/Makefile create mode 100644 src/common/farsight/README create mode 100644 src/common/farsight/farsight.defs create mode 100644 src/common/farsight/farsight.override create mode 100644 src/common/farsight/farsightmodule.c diff --git a/src/common/farsight/Makefile b/src/common/farsight/Makefile new file mode 100644 index 000000000..5ef6e26f6 --- /dev/null +++ b/src/common/farsight/Makefile @@ -0,0 +1,19 @@ +CFLAGS=`pkg-config --cflags farsight-0.1 pygtk-2.0` -I /usr/include/python2.5 -I. -I farsight/ +LDFLAGS=`pkg-config --libs farsight-0.1` + +farsight.so: farsight.o farsightmodule.o + $(CC) $(LDFLAGS) -shared $^ -o $@ + +farsight.c: farsight.defs farsight.override + pygtk-codegen-2.0 \ + --prefix farsight \ + --override farsight.override \ + --register /usr/share/gst-python/0.10/defs/gst-types.defs \ + farsight.defs >$@ + +farsight.defs: + python /usr/share/pygtk/2.0/codegen/h2def.py \ + farsight/farsight-codec.h \ + farsight/farsight-session.h \ + farsight/farsight-stream.h \ + >$@ diff --git a/src/common/farsight/README b/src/common/farsight/README new file mode 100644 index 000000000..1461ccbd6 --- /dev/null +++ b/src/common/farsight/README @@ -0,0 +1,3 @@ +This directory contains python bindings for farsight, a gstreamer-based +library that allows handling different kinds of audio/video conferencing +protocols. These bindings are neither official nor finished. diff --git a/src/common/farsight/farsight.defs b/src/common/farsight/farsight.defs new file mode 100644 index 000000000..71319fd21 --- /dev/null +++ b/src/common/farsight/farsight.defs @@ -0,0 +1,530 @@ +;; -*- scheme -*- +; object definitions ... +(define-object Session + (in-module "Farsight") + (parent "GObject") + (c-name "FarsightSession") + (gtype-id "FARSIGHT_TYPE_SESSION") +) + +(define-object Stream + (in-module "Farsight") + (parent "GObject") + (c-name "FarsightStream") + (gtype-id "FARSIGHT_TYPE_STREAM") +) + +;; Enumerations and flags ... + +;(define-enum MediaType +; (in-module "Farsight") +; (c-name "FarsightMediaType") +; (gtype-id "FARSIGHT_TYPE_MEDIA_TYPE") +; (values +; '("audio" "FARSIGHT_MEDIA_TYPE_AUDIO") +; '("video" "FARSIGHT_MEDIA_TYPE_VIDEO") +; '("last" "FARSIGHT_MEDIA_TYPE_LAST") +; ) +;) +; +;(define-enum SessionError +; (in-module "Farsight") +; (c-name "FarsightSessionError") +; (gtype-id "FARSIGHT_TYPE_SESSION_ERROR") +; (values +; '("n" "ERROR_UNKNOWN") +; ) +;) +; +;(define-enum StreamDirection +; (in-module "Farsight") +; (c-name "FarsightStreamDirection") +; (gtype-id "FARSIGHT_TYPE_STREAM_DIRECTION") +; (values +; '("none" "FARSIGHT_STREAM_DIRECTION_NONE") +; '("sendonly" "FARSIGHT_STREAM_DIRECTION_SENDONLY") +; '("receiveonly" "FARSIGHT_STREAM_DIRECTION_RECEIVEONLY") +; '("both" "FARSIGHT_STREAM_DIRECTION_BOTH") +; '("last" "FARSIGHT_STREAM_DIRECTION_LAST") +; ) +;) +; +;(define-enum StreamState +; (in-module "Farsight") +; (c-name "FarsightStreamState") +; (gtype-id "FARSIGHT_TYPE_STREAM_STATE") +; (values +; '("disconnected" "FARSIGHT_STREAM_STATE_DISCONNECTED") +; '("connecting" "FARSIGHT_STREAM_STATE_CONNECTING") +; '("connected" "FARSIGHT_STREAM_STATE_CONNECTED") +; ) +;) +; +;(define-enum StreamError +; (in-module "Farsight") +; (c-name "FarsightStreamError") +; (gtype-id "FARSIGHT_TYPE_STREAM_ERROR") +; (values +; '("error-eos" "FARSIGHT_STREAM_ERROR_EOS") +; '("unknown-error" "FARSIGHT_STREAM_UNKNOWN_ERROR") +; '("error-unknown" "FARSIGHT_STREAM_ERROR_UNKNOWN") +; '("error-timeout" "FARSIGHT_STREAM_ERROR_TIMEOUT") +; '("error-network" "FARSIGHT_STREAM_ERROR_NETWORK") +; '("error-pipeline-setup" "FARSIGHT_STREAM_ERROR_PIPELINE_SETUP") +; '("error-resource" "FARSIGHT_STREAM_ERROR_RESOURCE") +; '("error-last" "FARSIGHT_STREAM_ERROR_LAST") +; ) +;) +; +;(define-enum StreamDTMFEvent +; (in-module "Farsight") +; (c-name "FarsightStreamDTMFEvent") +; (gtype-id "FARSIGHT_TYPE_STREAM_DTMF_EVENT") +; (values +; '("0" "FARSIGHT_DTMF_EVENT_0") +; '("1" "FARSIGHT_DTMF_EVENT_1") +; '("2" "FARSIGHT_DTMF_EVENT_2") +; '("3" "FARSIGHT_DTMF_EVENT_3") +; '("4" "FARSIGHT_DTMF_EVENT_4") +; '("5" "FARSIGHT_DTMF_EVENT_5") +; '("6" "FARSIGHT_DTMF_EVENT_6") +; '("7" "FARSIGHT_DTMF_EVENT_7") +; '("8" "FARSIGHT_DTMF_EVENT_8") +; '("9" "FARSIGHT_DTMF_EVENT_9") +; '("star" "FARSIGHT_DTMF_EVENT_STAR") +; '("pound" "FARSIGHT_DTMF_EVENT_POUND") +; '("a" "FARSIGHT_DTMF_EVENT_A") +; '("b" "FARSIGHT_DTMF_EVENT_B") +; '("c" "FARSIGHT_DTMF_EVENT_C") +; '("d" "FARSIGHT_DTMF_EVENT_D") +; ) +;) +; +;(define-enum StreamDTMFMethod +; (in-module "Farsight") +; (c-name "FarsightStreamDTMFMethod") +; (gtype-id "FARSIGHT_TYPE_STREAM_DTMF_METHOD") +; (values +; '("auto" "FARSIGHT_DTMF_METHOD_AUTO") +; '("rtp-rfc4733" "FARSIGHT_DTMF_METHOD_RTP_RFC4733") +; '("sound" "FARSIGHT_DTMF_METHOD_SOUND") +; ) +;) +; + +;; From farsight-codec.h + +(define-method init + (of-object "FarsightCodec") + (c-name "farsight_codec_init") + (return-type "none") + (parameters + '("int" "id") + '("const-char*" "encoding_name") + '("FarsightMediaType" "media_type") + '("guint" "clock_rate") + ) +) + +(define-method destroy + (of-object "FarsightCodec") + (c-name "farsight_codec_destroy") + (return-type "none") +) + +(define-method copy + (of-object "FarsightCodec") + (c-name "farsight_codec_copy") + (return-type "FarsightCodec*") +) + +(define-function farsight_codec_list_destroy + (c-name "farsight_codec_list_destroy") + (return-type "none") + (parameters + '("GList*" "codec_list") + ) +) + +(define-function farsight_codec_list_copy + (c-name "farsight_codec_list_copy") + (return-type "GList*") + (parameters + '("const-GList*" "codec_list") + ) +) + + + +;; From farsight-session.h + +(define-function farsight_session_get_type + (c-name "farsight_session_get_type") + (return-type "GType") +) + +(define-function farsight_session_factory_make + (c-name "farsight_session_factory_make") + (return-type "FarsightSession*") + (parameters + '("const-gchar*" "session_id") + ) +) + +(define-method destroy + (of-object "FarsightSession") + (c-name "farsight_session_destroy") + (return-type "none") +) + +(define-method create_stream + (of-object "FarsightSession") + (c-name "farsight_session_create_stream") + (return-type "FarsightStream*") + (parameters + '("FarsightMediaType" "media_type") + '("FarsightStreamDirection" "dir") + ) +) + +(define-method list_supported_codecs + (of-object "FarsightSession") + (c-name "farsight_session_list_supported_codecs") + (return-type "const-GList*") +) + + + +;; From farsight-stream.h + +(define-function farsight_stream_get_type + (c-name "farsight_stream_get_type") + (return-type "GType") +) + +(define-method get_media_type + (of-object "FarsightStream") + (c-name "farsight_stream_get_media_type") + (return-type "FarsightMediaType") +) + +(define-method prepare_transports + (of-object "FarsightStream") + (c-name "farsight_stream_prepare_transports") + (return-type "none") +) + +(define-method get_native_candidate_list + (of-object "FarsightStream") + (c-name "farsight_stream_get_native_candidate_list") + (return-type "const-GList*") +) + +(define-method get_native_candidate + (of-object "FarsightStream") + (c-name "farsight_stream_get_native_candidate") + (return-type "GList*") + (parameters + '("const-gchar*" "candidate_id") + ) +) + +(define-method set_remote_candidate_list + (of-object "FarsightStream") + (c-name "farsight_stream_set_remote_candidate_list") + (return-type "none") + (parameters + '("const-GList*" "remote_candidates") + ) +) + +(define-method add_remote_candidate + (of-object "FarsightStream") + (c-name "farsight_stream_add_remote_candidate") + (return-type "none") + (parameters + '("const-GList*" "remote_candidate") + ) +) + +(define-method remove_remote_candidate + (of-object "FarsightStream") + (c-name "farsight_stream_remove_remote_candidate") + (return-type "none") + (parameters + '("const-gchar*" "remote_candidate_id") + ) +) + +(define-method set_active_candidate_pair + (of-object "FarsightStream") + (c-name "farsight_stream_set_active_candidate_pair") + (return-type "gboolean") + (parameters + '("const-gchar*" "native_candidate_id") + '("const-gchar*" "remote_candidate_id") + ) +) + +(define-method get_local_codecs + (of-object "FarsightStream") + (c-name "farsight_stream_get_local_codecs") + (return-type "const-GList*") +) + +(define-method set_remote_codecs + (of-object "FarsightStream") + (c-name "farsight_stream_set_remote_codecs") + (return-type "none") + (parameters + '("const-GList*" "codecs") + ) +) + +(define-method get_codec_intersection + (of-object "FarsightStream") + (c-name "farsight_stream_get_codec_intersection") + (return-type "GList*") +) + +(define-method set_codec_preference_list + (of-object "FarsightStream") + (c-name "farsight_stream_set_codec_preference_list") + (return-type "none") + (parameters + '("const-GArray*" "codec_pref") + ) +) + +(define-method set_active_codec + (of-object "FarsightStream") + (c-name "farsight_stream_set_active_codec") + (return-type "none") + (parameters + '("gint" "id") + ) +) + +(define-method get_active_codec + (of-object "FarsightStream") + (c-name "farsight_stream_get_active_codec") + (return-type "gint") +) + +(define-method set_sink + (of-object "FarsightStream") + (c-name "farsight_stream_set_sink") + (return-type "gboolean") + (parameters + '("GstElement*" "sink") + ) +) + +(define-method get_sink + (of-object "FarsightStream") + (c-name "farsight_stream_get_sink") + (return-type "GstElement*") +) + +(define-method set_sink_filter + (of-object "FarsightStream") + (c-name "farsight_stream_set_sink_filter") + (return-type "gboolean") + (parameters + '("GstCaps*" "filter") + ) +) + +(define-method set_source + (of-object "FarsightStream") + (c-name "farsight_stream_set_source") + (return-type "gboolean") + (parameters + '("GstElement*" "source") + ) +) + +(define-method get_source + (of-object "FarsightStream") + (c-name "farsight_stream_get_source") + (return-type "GstElement*") +) + +(define-method set_source_filter + (of-object "FarsightStream") + (c-name "farsight_stream_set_source_filter") + (return-type "gboolean") + (parameters + '("GstCaps*" "filter") + ) +) + +(define-method get_state + (of-object "FarsightStream") + (c-name "farsight_stream_get_state") + (return-type "FarsightStreamState") +) + +(define-method get_direction + (of-object "FarsightStream") + (c-name "farsight_stream_get_direction") + (return-type "FarsightStreamDirection") +) + +(define-method get_current_direction + (of-object "FarsightStream") + (c-name "farsight_stream_get_current_direction") + (return-type "FarsightStreamDirection") +) + +(define-method get_media_type + (of-object "FarsightStream") + (c-name "farsight_stream_get_media_type") + (return-type "FarsightMediaType") +) + +(define-method get_pipeline + (of-object "FarsightStream") + (c-name "farsight_stream_get_pipeline") + (return-type "GstElement*") +) + +(define-method set_pipeline + (of-object "FarsightStream") + (c-name "farsight_stream_set_pipeline") + (return-type "gboolean") + (parameters + '("GstElement*" "pipeline") + ) +) + +(define-method start + (of-object "FarsightStream") + (c-name "farsight_stream_start") + (return-type "gboolean") +) + +(define-method stop + (of-object "FarsightStream") + (c-name "farsight_stream_stop") + (return-type "none") +) + +(define-method set_sending + (of-object "FarsightStream") + (c-name "farsight_stream_set_sending") + (return-type "gboolean") + (parameters + '("gboolean" "sending") + ) +) + +(define-method signal_error + (of-object "FarsightStream") + (c-name "farsight_stream_signal_error") + (return-type "none") + (parameters + '("FarsightStreamError" "err") + '("const-gchar*" "mesg") + ) +) + +(define-method signal_native_candidates_prepared + (of-object "FarsightStream") + (c-name "farsight_stream_signal_native_candidates_prepared") + (return-type "none") +) + +(define-method signal_new_native_candidate + (of-object "FarsightStream") + (c-name "farsight_stream_signal_new_native_candidate") + (return-type "none") + (parameters + '("const-gchar*" "candidate_id") + ) +) + +(define-method signal_new_active_candidate_pair + (of-object "FarsightStream") + (c-name "farsight_stream_signal_new_active_candidate_pair") + (return-type "none") + (parameters + '("const-gchar*" "native_candidate_id") + '("const-gchar*" "remote_candidate_id") + ) +) + +(define-method signal_codec_changed + (of-object "FarsightStream") + (c-name "farsight_stream_signal_codec_changed") + (return-type "none") + (parameters + '("int" "codec_id") + ) +) + +(define-method signal_state_changed + (of-object "FarsightStream") + (c-name "farsight_stream_signal_state_changed") + (return-type "none") + (parameters + '("FarsightStreamState" "state") + '("FarsightStreamDirection" "direction") + ) +) + +;(define-method signal_sink_pad_ready +; (of-object "FarsightStream") +; (c-name "farsight_stream_signal_sink_pad_ready") +; (return-type "none") +; (parameters +; '("GstPad*" "pad") +; ) +;) +; +;(define-method start_telephony_event +; (of-object "FarsightStream") +; (c-name "farsight_stream_start_telephony_event") +; (return-type "gboolean") +; (parameters +; '("guint8" "ev") +; '("guint8" "volume") +; ) +;) +; +;(define-method stop_telephony_event +; (of-object "FarsightStream") +; (c-name "farsight_stream_stop_telephony_event") +; (return-type "gboolean") +;) +; +;(define-method start_telephony_event_full +; (of-object "FarsightStream") +; (c-name "farsight_stream_start_telephony_event_full") +; (return-type "gboolean") +; (parameters +; '("guint8" "ev") +; '("guint8" "volume") +; '("FarsightStreamDTMFMethod" "method") +; ) +;) +; +;(define-method stop_telephony_event_full +; (of-object "FarsightStream") +; (c-name "farsight_stream_stop_telephony_event_full") +; (return-type "gboolean") +; (parameters +; '("FarsightStreamDTMFMethod" "method") +; ) +;) +; +;(define-method preload_receive_pipeline +; (of-object "FarsightStream") +; (c-name "farsight_stream_preload_receive_pipeline") +; (return-type "gboolean") +; (parameters +; '("gint" "payload_type") +; ) +;) +; +; diff --git a/src/common/farsight/farsight.override b/src/common/farsight/farsight.override new file mode 100644 index 000000000..4c43074e9 --- /dev/null +++ b/src/common/farsight/farsight.override @@ -0,0 +1,36 @@ +%% +headers +#include +#include "pygobject.h" +#include "farsight.h" +%% +modulename farsight +%% +import gtk.Plug as PyGtkPlug_Type +import gobject.GObject as PyGObject_Type +import gobject.GType as PyGTypeModule_Type +import gst.Element as PyGstElement_Type +import gst.Pad as PyGstPad_Type +%% +ignore-glob + *_get_type +%% +override farsight_session_list_supported_codecs noargs +static PyObject* _wrap_farsight_session_list_supported_codecs(PyGObject *self) +{ + const GList *list, *tmp; + PyObject* ret; + + list=farsight_session_list_supported_codecs(FARSIGHT_SESSION(self->obj)); + + ret=PyList_New(0); + for (tmp=list; tmp!=NULL; tmp=tmp->next) { + FarsightCodec *codec = tmp->data; + PyObject *item = pygobject_new((GObject *) codec); + + PyList_Append(ret, item); + Py_DECREF(item); + } + // g_list_free(list); (a const list, we don't free it?) + return ret; +} diff --git a/src/common/farsight/farsightmodule.c b/src/common/farsight/farsightmodule.c new file mode 100644 index 000000000..eadf10ed6 --- /dev/null +++ b/src/common/farsight/farsightmodule.c @@ -0,0 +1,24 @@ +#include +#include + +void farsight_register_classes (PyObject *d); +extern PyMethodDef farsight_functions[]; + +DL_EXPORT(void) +initfarsight(void) +{ + PyObject *m, *d; + + init_pygobject (); + + m = Py_InitModule ("farsight", farsight_functions); + d = PyModule_GetDict (m); + + farsight_register_classes (d); + + // farsight_add_constants(m, 'FARSIGHT_TYPE_'); + + if (PyErr_Occurred ()) { + PyErr_Print(); + } +} From 1b3d3aefe4748a4bb632871caa56beebaf277b90 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Fri, 10 Aug 2007 00:53:30 +0000 Subject: [PATCH 05/67] Jingle: bindings for farsight, more functions wrapped --- src/common/farsight/Makefile | 5 + src/common/farsight/farsight.defs | 4 +- src/common/farsight/farsight.override | 172 +++++++++++++++++++++++++- 3 files changed, 178 insertions(+), 3 deletions(-) diff --git a/src/common/farsight/Makefile b/src/common/farsight/Makefile index 5ef6e26f6..dcd51cde4 100644 --- a/src/common/farsight/Makefile +++ b/src/common/farsight/Makefile @@ -17,3 +17,8 @@ farsight.defs: farsight/farsight-session.h \ farsight/farsight-stream.h \ >$@ + +clean: + -rm farsight.c farsightmodule.o farsight.o farsight.so + +.PHONY: clean diff --git a/src/common/farsight/farsight.defs b/src/common/farsight/farsight.defs index 71319fd21..f161042ee 100644 --- a/src/common/farsight/farsight.defs +++ b/src/common/farsight/farsight.defs @@ -182,8 +182,8 @@ (c-name "farsight_session_create_stream") (return-type "FarsightStream*") (parameters - '("FarsightMediaType" "media_type") - '("FarsightStreamDirection" "dir") + '("int" "media_type") + '("int" "dir") ) ) diff --git a/src/common/farsight/farsight.override b/src/common/farsight/farsight.override index 4c43074e9..6a6cacdcb 100644 --- a/src/common/farsight/farsight.override +++ b/src/common/farsight/farsight.override @@ -2,7 +2,13 @@ headers #include #include "pygobject.h" -#include "farsight.h" +#include +#include +#include + +#define GetString(name) PyString_AsString(PyMapping_GetItemString(item, name)) +#define GetLong(name) PyInt_AsLong(PyMapping_GetItemString(item, name)) +#define GetFloat(name) PyFloat_AsDouble(PyMapping_GetItemString(item, name)) %% modulename farsight %% @@ -34,3 +40,167 @@ static PyObject* _wrap_farsight_session_list_supported_codecs(PyGObject *self) // g_list_free(list); (a const list, we don't free it?) return ret; } +%% +override farsight_stream_get_local_codecs noargs +static PyObject* _wrap_farsight_stream_get_local_codecs(PyGObject *self) +{ + const GList *list, *tmp; + PyObject* ret; + + list=farsight_stream_get_local_codecs(FARSIGHT_STREAM(self->obj)); + + ret=PyList_New(0); + for (tmp=list; tmp!=NULL; tmp=g_list_next(tmp)) { + FarsightCodec *codec = tmp->data; + PyObject *item = pygobject_new((GObject *) codec); + + PyList_Append(ret, item); + Py_DECREF(item); + } + // g_list_free(list); (a const list, we don't free it?) + return ret; +} +%% +override farsight_stream_get_native_candidate_list noargs +static PyObject* _wrap_farsight_stream_get_native_candidate_list(PyGObject *self) +{ + const GList *list, *tmp; + PyObject* ret; + + list=farsight_stream_get_native_candidate_list(FARSIGHT_STREAM(self->obj)); + + ret=PyList_New(0); + for (tmp=list; tmp!=NULL; tmp=g_list_next(tmp)) { + FarsightCodec *codec = tmp->data; + PyObject *item = pygobject_new((GObject *) codec); + + PyList_Append(ret, item); + Py_DECREF(item); + } + // g_list_free(list); (a const list, we don't free it?) + return ret; +} +%% +override farsight_stream_set_codec_preference_list kwargs +static PyObject* _wrap_farsight_stream_set_codec_preference_list(PyGObject *self, + PyObject *args, + PyObject *kwargs) +{ + /* one could try to unpack tuples right into the array */ + static char *kwlist[] = {"codec_pref", NULL}; + PyObject* list; + GArray* codec_pref_array; + FarsightCodecPreference codec_pref; + int i; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &list)) + return NULL; + + codec_pref_array = g_array_sized_new(FALSE, FALSE, + sizeof(FarsightCodecPreference), PySequence_Size(list)); + + for(i=0; iobj), + (const GArray*) codec_pref_array); + + g_array_free(codec_pref_array, FALSE); + + Py_INCREF(Py_None); + return Py_None; +} +%% +override farsight_stream_set_remote_candidate_list kwargs +static PyObject* _wrap_farsight_stream_set_remote_candidate_list(PyGObject *self, + PyObject *args, + PyObject *kwargs) +{ + static char* kwlist[] = {"remote_candidates", NULL}; + PyObject* list; + GArray* candidate_array; + GList* candidate_list=NULL; + int i, listsize; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &list)) + return NULL; + + candidate_array=g_array_sized_new(FALSE, TRUE, + sizeof(FarsightTransportInfo), PySequence_Size(list)); + + listsize=PySequence_Size(list); + for(i=0;iobj), candidate_list); + + g_array_free(candidate_array, FALSE); + g_list_free(candidate_list); + + Py_INCREF(Py_None); + return Py_None; +} +%% +override farsight_stream_set_remote_codecs kwargs +static PyObject* _wrap_farsight_stream_set_remote_codecs(PyGObject *self, + PyObject *args, + PyObject *kwargs) +{ + static char* kwlist[] = {"codecs", NULL}; + PyObject* list, * item; + GArray* codecs_array; + GList* codecs_list; + int i, listsize; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &list)) + return NULL; + + codecs_array=g_array_sized_new(FALSE, TRUE, + sizeof(FarsightCodec), PySequence_Size(list)); + + listsize=PySequence_Size(list); + for(i=0;iobj), codecs_list); + + g_array_free(codecs_array, FALSE); + g_list_free(codecs_list); + + Py_INCREF(Py_None); + return Py_None; +} From 5fce20024c716f5b18d255b55f5bf9082bbff084 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Fri, 10 Aug 2007 20:01:45 +0000 Subject: [PATCH 06/67] Jingle: still farsight -- first test program in python work --- src/common/farsight/Makefile | 2 +- src/common/farsight/farsight.override | 36 +++++++++-- src/common/farsight/test.py | 93 +++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 6 deletions(-) create mode 100755 src/common/farsight/test.py diff --git a/src/common/farsight/Makefile b/src/common/farsight/Makefile index dcd51cde4..2c542b2f1 100644 --- a/src/common/farsight/Makefile +++ b/src/common/farsight/Makefile @@ -1,4 +1,4 @@ -CFLAGS=`pkg-config --cflags farsight-0.1 pygtk-2.0` -I /usr/include/python2.5 -I. -I farsight/ +CFLAGS=-g `pkg-config --cflags farsight-0.1 pygtk-2.0` -I /usr/include/python2.5 -I. -I farsight/ LDFLAGS=`pkg-config --libs farsight-0.1` farsight.so: farsight.o farsightmodule.o diff --git a/src/common/farsight/farsight.override b/src/common/farsight/farsight.override index 6a6cacdcb..e05dfa83c 100644 --- a/src/common/farsight/farsight.override +++ b/src/common/farsight/farsight.override @@ -9,6 +9,28 @@ headers #define GetString(name) PyString_AsString(PyMapping_GetItemString(item, name)) #define GetLong(name) PyInt_AsLong(PyMapping_GetItemString(item, name)) #define GetFloat(name) PyFloat_AsDouble(PyMapping_GetItemString(item, name)) + +static PyObject* farsight_codec_to_dict(FarsightCodec* fc) { + PyObject* dict = PyDict_New(); + PyObject* item; + + PyDict_SetItemString(dict, "id", item=PyInt_FromLong(fc->id)); + Py_DECREF(item); + + PyDict_SetItemString(dict, "encoding_name", item=PyString_FromString(fc->encoding_name)); + Py_DECREF(item); + + PyDict_SetItemString(dict, "media_type", item=PyInt_FromLong(fc->media_type)); + Py_DECREF(item); + + PyDict_SetItemString(dict, "clock_rate", item=PyInt_FromLong(fc->clock_rate)); + Py_DECREF(item); + + PyDict_SetItemString(dict, "channels", item=PyInt_FromLong(fc->channels)); + Py_DECREF(item); + + return dict; +} %% modulename farsight %% @@ -32,7 +54,7 @@ static PyObject* _wrap_farsight_session_list_supported_codecs(PyGObject *self) ret=PyList_New(0); for (tmp=list; tmp!=NULL; tmp=tmp->next) { FarsightCodec *codec = tmp->data; - PyObject *item = pygobject_new((GObject *) codec); + PyObject *item = farsight_codec_to_dict(codec); PyList_Append(ret, item); Py_DECREF(item); @@ -49,10 +71,11 @@ static PyObject* _wrap_farsight_stream_get_local_codecs(PyGObject *self) list=farsight_stream_get_local_codecs(FARSIGHT_STREAM(self->obj)); + ret=PyList_New(0); for (tmp=list; tmp!=NULL; tmp=g_list_next(tmp)) { FarsightCodec *codec = tmp->data; - PyObject *item = pygobject_new((GObject *) codec); + PyObject *item = farsight_codec_to_dict(codec); PyList_Append(ret, item); Py_DECREF(item); @@ -135,6 +158,7 @@ static PyObject* _wrap_farsight_stream_set_remote_candidate_list(PyGObject *self listsize=PySequence_Size(list); for(i=0;i>sys.stderr, "usage: test remoteip remoteport" + return + + session = setup_rtp_session() + stream = setup_rtp_stream(session) + + stream.set_remote_candidate_list([ + {'candidate_id': 'L1', + 'component': 1, + 'ip': sys.argv[1], + 'port': int(sys.argv[2]), + 'proto': FARSIGHT_NETWORK_PROTOCOL_UDP, + 'proto_subtype': 'RTP', + 'proto_profile': 'AVP', + 'preference': 1.0, + 'type': FARSIGHT_CANDIDATE_TYPE_LOCAL}]) + + stream.set_remote_codecs(stream.get_local_codecs()) + + gobject.MainLoop().run() + + +main() From 394c544571a3723dec0597887cdfd50da6f45b07 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Sat, 11 Aug 2007 21:47:53 +0000 Subject: [PATCH 07/67] Jingle: another farsight function wrapped + start using farsight in jingle.py --- src/common/farsight/farsight.override | 58 ++++++++++++++++++++++++++ src/common/jingle.py | 60 +++++++++++++++++++++++---- 2 files changed, 109 insertions(+), 9 deletions(-) diff --git a/src/common/farsight/farsight.override b/src/common/farsight/farsight.override index e05dfa83c..782030931 100644 --- a/src/common/farsight/farsight.override +++ b/src/common/farsight/farsight.override @@ -84,6 +84,64 @@ static PyObject* _wrap_farsight_stream_get_local_codecs(PyGObject *self) return ret; } %% +override farsight_stream_get_native_candidate kwargs +static PyObject* _wrap_farsight_stream_get_native_candidate(PyGObject *self, + PyObject *args, + PyObject *kwargs) +{ + static char* kwlist[] = {"candidate_id", NULL}; + char* candidate_id; + GList* list; + FarsightTransportInfo* data; + PyObject* ret; + PyObject* item; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", kwlist, &candidate_id)) + return NULL; + + list = farsight_stream_get_native_candidate(FARSIGHT_STREAM(self->obj), candidate_id); + data = list->data; + + ret = PyDict_New(); + + PyDict_SetItemString(ret, "candidate_id", item=PyString_FromString(data->candidate_id)); + Py_DECREF(item); + + PyDict_SetItemString(ret, "component", item=PyInt_FromLong(data->component)); + Py_DECREF(item); + + PyDict_SetItemString(ret, "ip", item=PyString_FromString(data->ip)); + Py_DECREF(item); + + PyDict_SetItemString(ret, "port", item=PyInt_FromLong(data->port)); + Py_DECREF(item); + + PyDict_SetItemString(ret, "proto", item=PyInt_FromLong(data->proto)); + Py_DECREF(item); + + PyDict_SetItemString(ret, "proto_subtype", item=PyString_FromString(data->proto_subtype)); + Py_DECREF(item); + + PyDict_SetItemString(ret, "proto_profile", item=PyString_FromString(data->proto_profile)); + Py_DECREF(item); + + PyDict_SetItemString(ret, "preference", item=PyFloat_FromDouble(data->preference)); + Py_DECREF(item); + + PyDict_SetItemString(ret, "type", item=PyInt_FromLong(data->type)); + Py_DECREF(item); + + PyDict_SetItemString(ret, "username", item=PyString_FromString(data->username)); + Py_DECREF(item); + + PyDict_SetItemString(ret, "password", item=PyString_FromString(data->password)); + Py_DECREF(item); + + g_list_free(list); + + return ret; +} +%% override farsight_stream_get_native_candidate_list noargs static PyObject* _wrap_farsight_stream_get_native_candidate_list(PyGObject *self) { diff --git a/src/common/jingle.py b/src/common/jingle.py index 737fdfb45..b1fa3c627 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -15,6 +15,16 @@ import gajim import xmpp +# ugly hack +import sys, dl, gst +sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) +import farsight +sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_LOCAL) +FARSIGHT_MEDIA_TYPE_AUDIO=0 +FARSIGHT_STREAM_DIRECTION_BOTH=3 +FARSIGHT_NETWORK_PROTOCOL_UDP=0 +FARSIGHT_CANDIDATE_TYPE_LOCAL=0 + import meta class JingleStates(object): @@ -68,6 +78,10 @@ class JingleSession(object): 'iq-error': [], } + # for making streams using farsight + self.p2psession = farsight.farsight_session_factory_make('rtp') + self.p2psession.connect('error', self.on_p2psession_error) + ''' Middle-level functions to manage contents. Handle local content cache and send change notifications. ''' def addContent(self, name, content, initiator='we'): @@ -146,10 +160,9 @@ class JingleSession(object): name = content['name'] - def sessionInitiateCB(self, stanza): + def __sessionInitiateCB(self, stanza, jingle, error): ''' We got a jingle session request from other entity, therefore we are the receiver... Unpack the data. ''' - jingle = stanza.getTag('jingle') self.initiator = jingle['initiator'] self.responder = self.ourjid self.jid = self.initiator @@ -173,6 +186,9 @@ class JingleSession(object): self.state = JingleStates.pending + def on_p2psession_error(self, *anything): + print "Farsight session error!" + ''' Methods that make/send proper pieces of XML. They check if the session is in appropriate state. ''' def __makeJingle(self, action): @@ -358,18 +374,44 @@ class JingleVoiP(object): __metaclass__=meta.VerboseClassType def __init__(self, session, node=None): self.session = session + self.codecs = None - if node is None: - self.audio = JingleAudioSession(self) - else: - self.audio = JingleAudioSession(self, node.getTag('content')) - self.transport = JingleICEUDPSession(self) + #if node is None: + # self.audio = JingleAudioSession(self) + #else: + # self.audio = JingleAudioSession(self, node.getTag('content')) + #self.transport = JingleICEUDPSession(self) + self.setupStream() def toXML(self): ''' Return proper XML for element. ''' return xmpp.Node('content', attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, - payload=[self.audio.toXML(), self.transport.toXML()]) + payload=[ + xmpp.Node(xmpp.NS_JINGLE_AUDIO+' description', payload=self.getCodecs()), + xmpp.Node(xmpp.NS_JINGLE_ICE_UDP+' transport') + ]) + + def setupStream(self): + self.p2pstream = self.session.p2psession.create_stream(FARSIGHT_MEDIA_TYPE_AUDIO, FARSIGHT_STREAM_DIRECTION_BOTH) + self.p2pstream.set_property('transmitter', 'libjingle') + self.p2pstream.connect('error', self.on_p2pstream_error) + self.p2pstream.connect('new-active-candidate-pair', self.on_p2pstream_new_active_candidate_pair) + self.p2pstream.connect('codec-changed', self.on_p2pstream_codec_changed) + self.p2pstream.connect('native-candidates-prepared', self.on_p2pstream_native_candidates_prepared) + self.p2pstream.connect('state-changed', self.on_p2pstream_state_changed) + self.p2pstream.connect('new-native-candidate', self.on_p2pstream_new_native_candidate) + self.p2pstream.prepare_transports() + + def on_p2pstream_error(self, *whatever): pass + def on_p2pstream_new_active_candidate_pair(self, *whatever): pass + def on_p2pstream_codec_changed(self, *whatever): pass + def on_p2pstream_native_candidates_prepared(self, *whatever): pass + def on_p2pstream_state_changed(self, *whatever): pass + def on_p2pstream_new_native_candidate(self, *whatever): pass + def getCodecs(self): + codecs=self.p2pstream.get_local_codecs() + return (xmpp.Node('payload', attrs=a) for a in codecs) class ConnectionJingle(object): ''' This object depends on that it is a part of Connection class. ''' @@ -428,5 +470,5 @@ class ConnectionJingle(object): def startVoiP(self, jid): jingle = JingleSession(self, weinitiate=True, jid=jid) self.addJingle(jingle) - jingle.addContent('voice', JingleVoiP()) + jingle.addContent('voice', JingleVoiP(jingle)) jingle.startSession() From 5f67848c929c2065c4d013f6c6af54c53513e7a8 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Sun, 12 Aug 2007 00:16:54 +0000 Subject: [PATCH 08/67] Jingle: better farsight wrappers --- src/common/farsight/farsight.defs | 22 +- src/common/farsight/farsight.override | 276 ++++++++++++++++++-------- src/common/jingle.py | 5 +- 3 files changed, 203 insertions(+), 100 deletions(-) diff --git a/src/common/farsight/farsight.defs b/src/common/farsight/farsight.defs index f161042ee..ec9ffad44 100644 --- a/src/common/farsight/farsight.defs +++ b/src/common/farsight/farsight.defs @@ -121,7 +121,7 @@ (parameters '("int" "id") '("const-char*" "encoding_name") - '("FarsightMediaType" "media_type") + '("int" "media_type") '("guint" "clock_rate") ) ) @@ -205,7 +205,7 @@ (define-method get_media_type (of-object "FarsightStream") (c-name "farsight_stream_get_media_type") - (return-type "FarsightMediaType") + (return-type "int") ) (define-method prepare_transports @@ -362,25 +362,19 @@ (define-method get_state (of-object "FarsightStream") (c-name "farsight_stream_get_state") - (return-type "FarsightStreamState") + (return-type "int") ) (define-method get_direction (of-object "FarsightStream") (c-name "farsight_stream_get_direction") - (return-type "FarsightStreamDirection") + (return-type "int") ) (define-method get_current_direction (of-object "FarsightStream") (c-name "farsight_stream_get_current_direction") - (return-type "FarsightStreamDirection") -) - -(define-method get_media_type - (of-object "FarsightStream") - (c-name "farsight_stream_get_media_type") - (return-type "FarsightMediaType") + (return-type "int") ) (define-method get_pipeline @@ -424,7 +418,7 @@ (c-name "farsight_stream_signal_error") (return-type "none") (parameters - '("FarsightStreamError" "err") + '("int" "err") '("const-gchar*" "mesg") ) ) @@ -468,8 +462,8 @@ (c-name "farsight_stream_signal_state_changed") (return-type "none") (parameters - '("FarsightStreamState" "state") - '("FarsightStreamDirection" "direction") + '("int" "state") + '("int" "direction") ) ) diff --git a/src/common/farsight/farsight.override b/src/common/farsight/farsight.override index 782030931..23efb4ee4 100644 --- a/src/common/farsight/farsight.override +++ b/src/common/farsight/farsight.override @@ -6,31 +6,168 @@ headers #include #include -#define GetString(name) PyString_AsString(PyMapping_GetItemString(item, name)) -#define GetLong(name) PyInt_AsLong(PyMapping_GetItemString(item, name)) -#define GetFloat(name) PyFloat_AsDouble(PyMapping_GetItemString(item, name)) +/* functions to put data into dict */ +inline static void insert_long_into_dict(PyObject* dict, char* key, long value) { + PyObject* item=PyInt_FromLong(value); + PyDict_SetItemString(dict, key, item); + Py_DECREF(item); +} -static PyObject* farsight_codec_to_dict(FarsightCodec* fc) { +inline static void insert_str_into_dict(PyObject* dict, char* key, const char* value) { + if (!value) return; + PyObject* item=PyString_FromString(value); + PyDict_SetItemString(dict, key, item); + Py_DECREF(item); +} + +inline static void insert_double_into_dict(PyObject* dict, char* key, double value) { + PyObject* item=PyFloat_FromDouble(value); + PyDict_SetItemString(dict, key, item); + Py_DECREF(item); +} + +static PyObject* farsight_transport_info_to_dict(FarsightTransportInfo* fti) { PyObject* dict = PyDict_New(); - PyObject* item; - PyDict_SetItemString(dict, "id", item=PyInt_FromLong(fc->id)); - Py_DECREF(item); - - PyDict_SetItemString(dict, "encoding_name", item=PyString_FromString(fc->encoding_name)); - Py_DECREF(item); - - PyDict_SetItemString(dict, "media_type", item=PyInt_FromLong(fc->media_type)); - Py_DECREF(item); - - PyDict_SetItemString(dict, "clock_rate", item=PyInt_FromLong(fc->clock_rate)); - Py_DECREF(item); - - PyDict_SetItemString(dict, "channels", item=PyInt_FromLong(fc->channels)); - Py_DECREF(item); + insert_str_into_dict (dict, "candidate_id", fti->candidate_id); + insert_long_into_dict (dict, "component", fti->component); + insert_str_into_dict (dict, "ip", fti->ip); + insert_long_into_dict (dict, "port", fti->port); + insert_long_into_dict (dict, "proto", fti->proto); + insert_str_into_dict (dict, "proto_subtype", fti->proto_subtype); + insert_str_into_dict (dict, "proto_profile", fti->proto_profile); + insert_double_into_dict (dict, "preference", fti->preference); + insert_long_into_dict (dict, "type", fti->type); + insert_str_into_dict (dict, "username", fti->username); + insert_str_into_dict (dict, "password", fti->password); return dict; } + +static PyObject* farsight_codec_to_dict(FarsightCodec* fc) { + PyObject* dict = PyDict_New(); + + /* these two are required */ + insert_long_into_dict(dict, "id", fc->id); + insert_str_into_dict(dict, "encoding_name", fc->encoding_name); + + /* next are optional */ + if (fc->media_type) insert_long_into_dict(dict, "media_type", fc->media_type); + if (fc->clock_rate) insert_long_into_dict(dict, "clock_rate", fc->clock_rate); + if (fc->channels) insert_long_into_dict(dict, "channels", fc->channels); + + if (fc->optional_params) { + PyObject* params = PyDict_New(); + GList* list; + + for(list=fc->optional_params; list; list=g_list_next(list)) { + FarsightCodecParameter *fcp=list->data; + insert_str_into_dict(params, fcp->name, fcp->value); + } + + PyDict_SetItemString(dict, "params", params); + Py_DECREF(params); + } + + return dict; +} + +/* functions to get data from dict */ +/* next three functions might raise an error */ +inline static long get_long_from_dict(PyObject* dict, char* key) { + PyObject* pyint=PyMapping_GetItemString(dict, key); + return(pyint?PyInt_AsLong(pyint):0); +} + +inline static char* get_str_from_dict(PyObject* dict, char* key) { + PyObject* str=PyMapping_GetItemString(dict, key); + return(str?PyString_AsString(str):NULL); +} + +inline static double get_double_from_dict(PyObject* dict, char* key) { + PyObject* pyfloat=PyMapping_GetItemString(dict, key); + return(pyfloat?PyFloat_AsDouble(pyfloat):0.0); +} + +/* may raise an exception */ +static void dict_to_farsight_transport_info(PyObject* dict, FarsightTransportInfo* fti) { + if (!dict) return; + + /* required */ + fti->candidate_id = get_str_from_dict(dict, "candidate_id"); + fti->component = get_long_from_dict(dict, "component"); + fti->ip = get_str_from_dict(dict, "ip"); + fti->port = get_long_from_dict(dict, "port"); + fti->proto = get_long_from_dict(dict, "proto"); + fti->proto_subtype = get_str_from_dict(dict, "proto_subtype"); + fti->proto_profile = get_str_from_dict(dict, "proto_profile"); + fti->preference = get_double_from_dict(dict, "preference"); + fti->type = get_long_from_dict(dict, "type"); + + if (PyError_Occurred()) return; + + /* optional */ + fti->username = get_str_from_dict(dict, "username"); + fti->password = get_str_from_dict(dict, "password"); + + PyError_Clear(); +} + +/* GArray must be freed if not NULL; + may raise an exception */ +static void dict_to_farsight_codec(PyObject* dict, FarsightCodec* fc, GArray** fcp) { + GArray* array=*fcp; + + if (!dict) return; + + /* required data */ + fc->id = get_long_from_dict(dict, "id"); + fc->encoding_name = get_str_from_dict(dict, "encoding_name"); + + if (PyError_Occured()) return; + + /* optional data */ + fc->media_type = get_long_from_dict(dict, "media_type"); + fc->clock_rate = get_long_from_dict(dict, "clock_rate"); + fc->channels = get_long_from_dict(dict, "channels"); + + if (PyMapping_HasKeyString(dict, "params")) { + PyObject* params = PyMapping_GetItemString(dict, "params"); + if (PyDict_Check(params)) { + PyObject *key, *value; + int pos=0; + GList* list=NULL; + int i=0; + + if (!array) + array = g_array_new(FALSE, FALSE, sizeof(FarsightCodecParameter)); + + while (PyDict_Next(params, &pos, &key, &value)) { + if (PyString_Check(key) && PyString_Check(value)) { + FarsightCodecParameter fcp; + fcp.name = PyString_AsString(key); + fcp.value= PyString_AsString(value); + g_array_append_val(array, fcp); + list=g_list_prepend(list, + &g_array_index(array, FarsightCodecParameter, i++)); + } else { + /* this is not a string? not good... */ + puts("keys and values must be strings here!"); + } + } + + fc->optional_params = list; + } else { + /* this is not a dictionary? fail miserably... */ + puts("params must be a dictionary!"); + } + } + + PyErr_Clear(); + + *fcp = array; +} + %% modulename farsight %% @@ -91,7 +228,7 @@ static PyObject* _wrap_farsight_stream_get_native_candidate(PyGObject *self, { static char* kwlist[] = {"candidate_id", NULL}; char* candidate_id; - GList* list; + GList* list, *tmp; FarsightTransportInfo* data; PyObject* ret; PyObject* item; @@ -100,42 +237,15 @@ static PyObject* _wrap_farsight_stream_get_native_candidate(PyGObject *self, return NULL; list = farsight_stream_get_native_candidate(FARSIGHT_STREAM(self->obj), candidate_id); - data = list->data; - ret = PyDict_New(); + ret = PyList_New(0); + for(tmp=list;list;list=g_list_next(list)) { + FarsightTransportInfo *fti = tmp->data; + PyObject *item = farsight_transport_info_to_dict(fti); - PyDict_SetItemString(ret, "candidate_id", item=PyString_FromString(data->candidate_id)); - Py_DECREF(item); - - PyDict_SetItemString(ret, "component", item=PyInt_FromLong(data->component)); - Py_DECREF(item); - - PyDict_SetItemString(ret, "ip", item=PyString_FromString(data->ip)); - Py_DECREF(item); - - PyDict_SetItemString(ret, "port", item=PyInt_FromLong(data->port)); - Py_DECREF(item); - - PyDict_SetItemString(ret, "proto", item=PyInt_FromLong(data->proto)); - Py_DECREF(item); - - PyDict_SetItemString(ret, "proto_subtype", item=PyString_FromString(data->proto_subtype)); - Py_DECREF(item); - - PyDict_SetItemString(ret, "proto_profile", item=PyString_FromString(data->proto_profile)); - Py_DECREF(item); - - PyDict_SetItemString(ret, "preference", item=PyFloat_FromDouble(data->preference)); - Py_DECREF(item); - - PyDict_SetItemString(ret, "type", item=PyInt_FromLong(data->type)); - Py_DECREF(item); - - PyDict_SetItemString(ret, "username", item=PyString_FromString(data->username)); - Py_DECREF(item); - - PyDict_SetItemString(ret, "password", item=PyString_FromString(data->password)); - Py_DECREF(item); + PyList_Append(ret, item); + Py_DECREF(item); + } g_list_free(list); @@ -150,15 +260,15 @@ static PyObject* _wrap_farsight_stream_get_native_candidate_list(PyGObject *self list=farsight_stream_get_native_candidate_list(FARSIGHT_STREAM(self->obj)); - ret=PyList_New(0); - for (tmp=list; tmp!=NULL; tmp=g_list_next(tmp)) { - FarsightCodec *codec = tmp->data; - PyObject *item = pygobject_new((GObject *) codec); + ret = PyList_New(0); + for(tmp=list;list;list=g_list_next(list)) { + FarsightTransportInfo *fti = tmp->data; + PyObject *item = farsight_transport_info_to_dict(fti); PyList_Append(ret, item); Py_DECREF(item); } - // g_list_free(list); (a const list, we don't free it?) + return ret; } %% @@ -217,19 +327,7 @@ static PyObject* _wrap_farsight_stream_set_remote_candidate_list(PyGObject *self for(i=0;iobj), candidate_list); + if(!PyError_Occurred()) { + farsight_stream_set_remote_candidate_list(FARSIGHT_STREAM(self->obj), candidate_list); + } g_array_free(candidate_array, FALSE); g_list_free(candidate_list); - Py_INCREF(Py_None); - return Py_None; + if(!PyError_Occurred()) { + Py_INCREF(Py_None); + return Py_None; + } else { + return NULL; + } } %% override farsight_stream_set_remote_codecs kwargs @@ -254,6 +358,7 @@ static PyObject* _wrap_farsight_stream_set_remote_codecs(PyGObject *self, static char* kwlist[] = {"codecs", NULL}; PyObject* list, * item; GArray* codecs_array; + GArray* fcp_array=NULL; GList* codecs_list=NULL; int i, listsize; @@ -266,25 +371,26 @@ static PyObject* _wrap_farsight_stream_set_remote_codecs(PyGObject *self, listsize=PySequence_Size(list); for(i=0;iobj), codecs_list); + if (!PyError_Occurred()) { + farsight_stream_set_remote_codecs(FARSIGHT_STREAM(self->obj), codecs_list); + } + g_array_free(fcp_array, FALSE); g_array_free(codecs_array, FALSE); g_list_free(codecs_list); - Py_INCREF(Py_None); - return Py_None; + if (!PyError_Occurred()) { + Py_INCREF(Py_None); + return Py_None; + } else { + return NULL; + } } diff --git a/src/common/jingle.py b/src/common/jingle.py index b1fa3c627..bedcba6d8 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -408,7 +408,10 @@ class JingleVoiP(object): def on_p2pstream_codec_changed(self, *whatever): pass def on_p2pstream_native_candidates_prepared(self, *whatever): pass def on_p2pstream_state_changed(self, *whatever): pass - def on_p2pstream_new_native_candidate(self, *whatever): pass + def on_p2pstream_new_native_candidate(self, p2pstream, candidate_id): + candidate = p2pstream.get_native_candidate(candidate_id) + + def getCodecs(self): codecs=self.p2pstream.get_local_codecs() return (xmpp.Node('payload', attrs=a) for a in codecs) From a0af6f7fb8c40b5e14a1ad7e5df09edd41c65c44 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Sun, 12 Aug 2007 21:13:05 +0000 Subject: [PATCH 09/67] Spelling fix --- src/gajim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gajim.py b/src/gajim.py index c21f43669..2a74674bc 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -352,7 +352,7 @@ import config class GlibIdleQueue(idlequeue.IdleQueue): ''' - Extends IdleQueue to use glib io_add_wath, instead of select/poll + Extends IdleQueue to use glib io_add_watch, instead of select/poll In another, `non gui' implementation of Gajim IdleQueue can be used safetly. ''' def init_idle(self): From c00c05dd93b34f73d43c80d33c09fa8397d5e10a Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Sun, 12 Aug 2007 21:21:40 +0000 Subject: [PATCH 10/67] Jingle: ugly temporary fix for not sending stanzas when farsight is connecting --- src/common/xmpp/transports_nb.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index c5115cc1c..f727bc1e5 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -586,6 +586,7 @@ class NonBlockingTcp(PlugIn, IdleObject): '''Append raw_data to the queue of messages to be send. If supplied data is unicode string, encode it to utf-8. ''' + now = True if self.state <= 0: return r = raw_data From 5bb2fa9a0c8e0d172553f39839be8a7c6203f9b7 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Sun, 12 Aug 2007 21:22:25 +0000 Subject: [PATCH 11/67] Jingle: farsight bindings define constants now --- src/common/farsight/farsight.override | 14 +++--- src/common/farsight/farsightmodule.c | 48 +++++++++++++++++- src/common/jingle.py | 70 +++++++++++++++++++-------- 3 files changed, 105 insertions(+), 27 deletions(-) diff --git a/src/common/farsight/farsight.override b/src/common/farsight/farsight.override index 23efb4ee4..6c717514d 100644 --- a/src/common/farsight/farsight.override +++ b/src/common/farsight/farsight.override @@ -104,13 +104,13 @@ static void dict_to_farsight_transport_info(PyObject* dict, FarsightTransportInf fti->preference = get_double_from_dict(dict, "preference"); fti->type = get_long_from_dict(dict, "type"); - if (PyError_Occurred()) return; + if (PyErr_Occurred()) return; /* optional */ fti->username = get_str_from_dict(dict, "username"); fti->password = get_str_from_dict(dict, "password"); - PyError_Clear(); + PyErr_Clear(); } /* GArray must be freed if not NULL; @@ -124,7 +124,7 @@ static void dict_to_farsight_codec(PyObject* dict, FarsightCodec* fc, GArray** f fc->id = get_long_from_dict(dict, "id"); fc->encoding_name = get_str_from_dict(dict, "encoding_name"); - if (PyError_Occured()) return; + if (PyErr_Occurred()) return; /* optional data */ fc->media_type = get_long_from_dict(dict, "media_type"); @@ -335,14 +335,14 @@ static PyObject* _wrap_farsight_stream_set_remote_candidate_list(PyGObject *self &g_array_index(candidate_array, FarsightTransportInfo, i)); } - if(!PyError_Occurred()) { + if(!PyErr_Occurred()) { farsight_stream_set_remote_candidate_list(FARSIGHT_STREAM(self->obj), candidate_list); } g_array_free(candidate_array, FALSE); g_list_free(candidate_list); - if(!PyError_Occurred()) { + if(!PyErr_Occurred()) { Py_INCREF(Py_None); return Py_None; } else { @@ -379,7 +379,7 @@ static PyObject* _wrap_farsight_stream_set_remote_codecs(PyGObject *self, &g_array_index(codecs_array, FarsightCodec, i)); } - if (!PyError_Occurred()) { + if (!PyErr_Occurred()) { farsight_stream_set_remote_codecs(FARSIGHT_STREAM(self->obj), codecs_list); } @@ -387,7 +387,7 @@ static PyObject* _wrap_farsight_stream_set_remote_codecs(PyGObject *self, g_array_free(codecs_array, FALSE); g_list_free(codecs_list); - if (!PyError_Occurred()) { + if (!PyErr_Occurred()) { Py_INCREF(Py_None); return Py_None; } else { diff --git a/src/common/farsight/farsightmodule.c b/src/common/farsight/farsightmodule.c index eadf10ed6..556b284de 100644 --- a/src/common/farsight/farsightmodule.c +++ b/src/common/farsight/farsightmodule.c @@ -1,5 +1,9 @@ #include #include + +#include +#include +#include void farsight_register_classes (PyObject *d); extern PyMethodDef farsight_functions[]; @@ -16,7 +20,49 @@ initfarsight(void) farsight_register_classes (d); - // farsight_add_constants(m, 'FARSIGHT_TYPE_'); + PyModule_AddIntConstant(m, "MEDIA_TYPE_AUDIO", FARSIGHT_MEDIA_TYPE_AUDIO); + PyModule_AddIntConstant(m, "MEDIA_TYPE_VIDEO", FARSIGHT_MEDIA_TYPE_VIDEO); + PyModule_AddIntConstant(m, "STREAM_DIRECTION_NONE", FARSIGHT_STREAM_DIRECTION_NONE); + PyModule_AddIntConstant(m, "STREAM_DIRECTION_SENDONLY", FARSIGHT_STREAM_DIRECTION_SENDONLY); + PyModule_AddIntConstant(m, "STREAM_DIRECTION_RECEIVEONLY", FARSIGHT_STREAM_DIRECTION_RECEIVEONLY); + PyModule_AddIntConstant(m, "STREAM_DIRECTION_BOTH", FARSIGHT_STREAM_DIRECTION_BOTH); + PyModule_AddIntConstant(m, "STREAM_STATE_DISCONNECTED", FARSIGHT_STREAM_STATE_DISCONNECTED); + PyModule_AddIntConstant(m, "STREAM_STATE_CONNECTING", FARSIGHT_STREAM_STATE_CONNECTING); + PyModule_AddIntConstant(m, "STREAM_STATE_CONNECTED", FARSIGHT_STREAM_STATE_CONNECTED); + PyModule_AddIntConstant(m, "STREAM_ERROR_EOS", FARSIGHT_STREAM_ERROR_EOS); + PyModule_AddIntConstant(m, "STREAM_UNKNOWN_ERROR", FARSIGHT_STREAM_UNKNOWN_ERROR); + PyModule_AddIntConstant(m, "STREAM_ERROR_UNKNOWN", FARSIGHT_STREAM_UNKNOWN_ERROR); + PyModule_AddIntConstant(m, "STREAM_ERROR_TIMEOUT", FARSIGHT_STREAM_ERROR_TIMEOUT); + PyModule_AddIntConstant(m, "STREAM_ERROR_NETWORK", FARSIGHT_STREAM_ERROR_NETWORK); + PyModule_AddIntConstant(m, "STREAM_ERROR_PIPELINE_SETUP", FARSIGHT_STREAM_ERROR_PIPELINE_SETUP); + PyModule_AddIntConstant(m, "STREAM_ERROR_RESOURCE", FARSIGHT_STREAM_ERROR_RESOURCE); + PyModule_AddIntConstant(m, "DTMF_EVENT_0", FARSIGHT_DTMF_EVENT_0); + PyModule_AddIntConstant(m, "DTMF_EVENT_1", FARSIGHT_DTMF_EVENT_1); + PyModule_AddIntConstant(m, "DTMF_EVENT_2", FARSIGHT_DTMF_EVENT_2); + PyModule_AddIntConstant(m, "DTMF_EVENT_3", FARSIGHT_DTMF_EVENT_3); + PyModule_AddIntConstant(m, "DTMF_EVENT_4", FARSIGHT_DTMF_EVENT_4); + PyModule_AddIntConstant(m, "DTMF_EVENT_5", FARSIGHT_DTMF_EVENT_5); + PyModule_AddIntConstant(m, "DTMF_EVENT_6", FARSIGHT_DTMF_EVENT_6); + PyModule_AddIntConstant(m, "DTMF_EVENT_7", FARSIGHT_DTMF_EVENT_7); + PyModule_AddIntConstant(m, "DTMF_EVENT_8", FARSIGHT_DTMF_EVENT_8); + PyModule_AddIntConstant(m, "DTMF_EVENT_9", FARSIGHT_DTMF_EVENT_9); + PyModule_AddIntConstant(m, "DTMF_EVENT_STAR", FARSIGHT_DTMF_EVENT_STAR); + PyModule_AddIntConstant(m, "DTMF_EVENT_POUND", FARSIGHT_DTMF_EVENT_POUND); + PyModule_AddIntConstant(m, "DTMF_EVENT_A", FARSIGHT_DTMF_EVENT_A); + PyModule_AddIntConstant(m, "DTMF_EVENT_B", FARSIGHT_DTMF_EVENT_B); + PyModule_AddIntConstant(m, "DTMF_EVENT_C", FARSIGHT_DTMF_EVENT_C); + PyModule_AddIntConstant(m, "DTMF_EVENT_D", FARSIGHT_DTMF_EVENT_D); + PyModule_AddIntConstant(m, "DTMF_METHOD_AUTO", FARSIGHT_DTMF_METHOD_AUTO); + PyModule_AddIntConstant(m, "DTMF_METHOD_RTP_RFC4733", FARSIGHT_DTMF_METHOD_RTP_RFC4733); + PyModule_AddIntConstant(m, "DTMF_METHOD_SOUND", FARSIGHT_DTMF_METHOD_SOUND); + PyModule_AddIntConstant(m, "TRANSMITTER_STATE_DISCONNECTED", FARSIGHT_TRANSMITTER_STATE_DISCONNECTED); + PyModule_AddIntConstant(m, "TRANSMITTER_STATE_CONNECTING", FARSIGHT_TRANSMITTER_STATE_CONNECTING); + PyModule_AddIntConstant(m, "TRANSMITTER_STATE_CONNECTED", FARSIGHT_TRANSMITTER_STATE_CONNECTED); + PyModule_AddIntConstant(m, "CANDIDATE_TYPE_LOCAL", FARSIGHT_CANDIDATE_TYPE_LOCAL); + PyModule_AddIntConstant(m, "CANDIDATE_TYPE_DERIVED", FARSIGHT_CANDIDATE_TYPE_DERIVED); + PyModule_AddIntConstant(m, "CANDIDATE_TYPE_RELAY", FARSIGHT_CANDIDATE_TYPE_RELAY); + PyModule_AddIntConstant(m, "NETWORK_PROTOCOL_UDP", FARSIGHT_NETWORK_PROTOCOL_UDP); + PyModule_AddIntConstant(m, "NETWORK_PROTOCOL_TCP", FARSIGHT_NETWORK_PROTOCOL_TCP); if (PyErr_Occurred ()) { PyErr_Print(); diff --git a/src/common/jingle.py b/src/common/jingle.py index bedcba6d8..2f52c0885 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -20,10 +20,6 @@ import sys, dl, gst sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) import farsight sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_LOCAL) -FARSIGHT_MEDIA_TYPE_AUDIO=0 -FARSIGHT_STREAM_DIRECTION_BOTH=3 -FARSIGHT_NETWORK_PROTOCOL_UDP=0 -FARSIGHT_CANDIDATE_TYPE_LOCAL=0 import meta @@ -84,21 +80,21 @@ class JingleSession(object): ''' Middle-level functions to manage contents. Handle local content cache and send change notifications. ''' - def addContent(self, name, content, initiator='we'): + def addContent(self, name, content, creator='we'): ''' Add new content to session. If the session is active, this will send proper stanza to update session. The protocol prohibits changing that when pending. - Initiator must be one of ('we', 'peer', 'initiator', 'responder')''' + Creator must be one of ('we', 'peer', 'initiator', 'responder')''' if self.state==JingleStates.pending: raise WrongState - if (initiator=='we' and self.weinitiate) or (initiator=='peer' and not self.weinitiate): - initiator='initiator' - elif (initiator=='peer' and self.weinitiate) or (initiator=='we' and not self.weinitiate): - initiator='responder' - content.creator = initiator + 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[(initiator,name)]=content + self.contents[(creator,name)]=content if self.state==JingleStates.active: pass # TODO: send proper stanza, shouldn't be needed now @@ -117,7 +113,6 @@ class JingleSession(object): self.__sessionInitiate() def sendSessionInfo(self): pass - def sendTransportInfo(self): pass ''' Callbacks. ''' def stanzaCB(self, stanza): @@ -165,7 +160,7 @@ class JingleSession(object): therefore we are the receiver... Unpack the data. ''' self.initiator = jingle['initiator'] self.responder = self.ourjid - self.jid = self.initiator + self.peerjid = self.initiator fail = True for element in jingle.iterTags('content'): @@ -255,8 +250,11 @@ class JingleSession(object): def __contentRemove(self): assert self.state!=JingleStates.ended - def __transportInfo(self): + def sendTransportInfo(self, content): assert self.state!=JingleStates.ended + stanza, jingle = self.__makeJingle('transport-info') + jingle.addChild(node=content) + self.connection.connection.send(stanza) '''Callbacks''' def sessionTerminateCB(self, stanza): pass @@ -368,12 +366,22 @@ class JingleICEUDPSession(object): ''' ICE-UDP doesn't send much in its transport stanza... ''' return xmpp.Node(xmpp.NS_JINGLE_ICE_UDP+' transport') -class JingleVoiP(object): +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 + +class JingleVoiP(JingleContent): ''' Jingle VoiP sessions consist of audio content transported over an ICE UDP protocol. ''' __metaclass__=meta.VerboseClassType def __init__(self, session, node=None): - self.session = session + JingleContent.__init__(self, session, node) self.codecs = None #if node is None: @@ -392,8 +400,15 @@ class JingleVoiP(object): xmpp.Node(xmpp.NS_JINGLE_ICE_UDP+' transport') ]) + def __content(self, payload=[]): + ''' Build a XML content-wrapper for our data. ''' + return xmpp.Node('content', + attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, + payload=payload) + def setupStream(self): - self.p2pstream = self.session.p2psession.create_stream(FARSIGHT_MEDIA_TYPE_AUDIO, FARSIGHT_STREAM_DIRECTION_BOTH) + self.p2pstream = self.session.p2psession.create_stream( + farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) self.p2pstream.set_property('transmitter', 'libjingle') self.p2pstream.connect('error', self.on_p2pstream_error) self.p2pstream.connect('new-active-candidate-pair', self.on_p2pstream_new_active_candidate_pair) @@ -409,8 +424,25 @@ class JingleVoiP(object): def on_p2pstream_native_candidates_prepared(self, *whatever): pass def on_p2pstream_state_changed(self, *whatever): pass def on_p2pstream_new_native_candidate(self, p2pstream, candidate_id): - candidate = p2pstream.get_native_candidate(candidate_id) + candidates = p2pstream.get_native_candidate(candidate_id) + for candidate in candidates: + attrs={ + 'component': candidate['component'], + 'foundation': '1', # hack + 'generation': '0', + 'ip': candidate['ip'], + 'network': '0', + 'port': candidate['port'], + 'priority': int(100000*candidate['preference']), # hack + 'protocol': candidate['proto']==farsight.NETWORK_PROTOCOL_UDP and 'udp' or 'tcp', + } + if 'username' in candidate: attrs['ufrag']=candidate['username'] + if 'password' in candidate: attrs['pwd']=candidate['password'] + c=self.__content() + t=c.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') + t.addChild('candidate', attrs=attrs) + self.session.sendTransportInfo(c) def getCodecs(self): codecs=self.p2pstream.get_local_codecs() From e2182686bf19f9676bd76a2e20cc7a7314c56a3c Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Mon, 13 Aug 2007 16:17:22 +0000 Subject: [PATCH 12/67] Jingle: another ugly fix --- src/gajim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gajim.py b/src/gajim.py index 2a74674bc..ec18f4caa 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -2369,7 +2369,7 @@ class Interface: # pygtk2.8+ on win, breaks io_add_watch. # We use good old select.select() - if os.name == 'nt': + if True or os.name == 'nt': gajim.idlequeue = idlequeue.SelectIdleQueue() else: # in a nongui implementation, just call: @@ -2484,7 +2484,7 @@ class Interface: self.last_ftwindow_update = 0 gobject.timeout_add(100, self.autoconnect) - gobject.timeout_add(2000, self.process_connections) + gobject.timeout_add(200, self.process_connections) gobject.timeout_add(10000, self.read_sleepy) if __name__ == '__main__': From b8087cdb536feb8851f1c52b214d5c6aada3a705 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Wed, 15 Aug 2007 09:28:32 +0000 Subject: [PATCH 13/67] Jingle: removing hacks, rising the priority for io_add_watch callbacks. --- src/common/xmpp/transports_nb.py | 1 - src/gajim.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index f727bc1e5..c5115cc1c 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -586,7 +586,6 @@ class NonBlockingTcp(PlugIn, IdleObject): '''Append raw_data to the queue of messages to be send. If supplied data is unicode string, encode it to utf-8. ''' - now = True if self.state <= 0: return r = raw_data diff --git a/src/gajim.py b/src/gajim.py index ec18f4caa..bfb3052a4 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -367,8 +367,7 @@ class GlibIdleQueue(idlequeue.IdleQueue): ''' this method is called when we plug a new idle object. Start listening for events from fd ''' - res = gobject.io_add_watch(fd, flags, self.process_events, - priority=gobject.PRIORITY_LOW) + res = gobject.io_add_watch(fd, flags, self.process_events) # store the id of the watch, so that we can remove it on unplug self.events[fd] = res @@ -2369,7 +2368,7 @@ class Interface: # pygtk2.8+ on win, breaks io_add_watch. # We use good old select.select() - if True or os.name == 'nt': + if os.name == 'nt': gajim.idlequeue = idlequeue.SelectIdleQueue() else: # in a nongui implementation, just call: From a6a3fbbff0a61b56593696f2f426bae17cfbf33a Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Wed, 15 Aug 2007 09:29:27 +0000 Subject: [PATCH 14/67] simplexml: consistent interface for xml attributes -- __contains__ --- src/common/xmpp/simplexml.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/common/xmpp/simplexml.py b/src/common/xmpp/simplexml.py index 89a6cec59..7ab0fe907 100644 --- a/src/common/xmpp/simplexml.py +++ b/src/common/xmpp/simplexml.py @@ -253,6 +253,9 @@ class Node(object): def __delitem__(self,item): """ Deletes node's attribute "item". """ return self.delAttr(item) + def __contains__(self,item): + """ Checks if node has attribute "item" """ + self.has_attr(item) def __getattr__(self,attr): """ Reduce memory usage caused by T/NT classes - use memory only when needed. """ if attr=='T': From 59ab79c39c2234928244d2d7b0b5ae7fcca0264d Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Wed, 15 Aug 2007 09:43:43 +0000 Subject: [PATCH 15/67] Jingle: farsight: last badly needed call wrapped, bugfix to another one --- src/common/farsight/farsight.override | 59 ++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/common/farsight/farsight.override b/src/common/farsight/farsight.override index 6c717514d..273ea55b6 100644 --- a/src/common/farsight/farsight.override +++ b/src/common/farsight/farsight.override @@ -50,9 +50,9 @@ static PyObject* farsight_codec_to_dict(FarsightCodec* fc) { /* these two are required */ insert_long_into_dict(dict, "id", fc->id); insert_str_into_dict(dict, "encoding_name", fc->encoding_name); + insert_long_into_dict(dict, "media_type", fc->media_type); /* next are optional */ - if (fc->media_type) insert_long_into_dict(dict, "media_type", fc->media_type); if (fc->clock_rate) insert_long_into_dict(dict, "clock_rate", fc->clock_rate); if (fc->channels) insert_long_into_dict(dict, "channels", fc->channels); @@ -123,6 +123,10 @@ static void dict_to_farsight_codec(PyObject* dict, FarsightCodec* fc, GArray** f /* required data */ fc->id = get_long_from_dict(dict, "id"); fc->encoding_name = get_str_from_dict(dict, "encoding_name"); + fc->media_type = 0; + fc->clock_rate = 0; + fc->channels = 0; + fc->optional_params = NULL; if (PyErr_Occurred()) return; @@ -394,3 +398,56 @@ static PyObject* _wrap_farsight_stream_set_remote_codecs(PyGObject *self, return NULL; } } +%% +override farsight_stream_add_remote_candidate kwargs +static PyObject* _wrap_farsight_stream_add_remote_candidate(PyGObject *self, + PyObject *args, + PyObject *kwargs) +{ + static char* kwlist[] = {"remote_candidate", NULL}; + PyObject* list; + GArray* candidate_array; + GList* candidate_list=NULL; + int i, listsize; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &list)) + return NULL; + + candidate_array=g_array_sized_new(FALSE, TRUE, + sizeof(FarsightTransportInfo), PySequence_Size(list)); + + listsize=PySequence_Size(list); + for(i=0;idata; + printf ("Remote transport candidate: %s %d %s %s %s %d pref %f", + info->candidate_id, info->component, + (info->proto == FARSIGHT_NETWORK_PROTOCOL_TCP) ? "TCP" : "UDP", + info->proto_subtype, info->ip, info->port, (double) info->preference); + } + + if(!PyErr_Occurred()) { + farsight_stream_add_remote_candidate(FARSIGHT_STREAM(self->obj), candidate_list); + } + + g_array_free(candidate_array, FALSE); + g_list_free(candidate_list); + + if(!PyErr_Occurred()) { + Py_INCREF(Py_None); + return Py_None; + } else { + return NULL; + } +} From 70ddb3e6811952175bc54a711ef1aaf5c5441b6d Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Wed, 15 Aug 2007 09:47:29 +0000 Subject: [PATCH 16/67] Jingle: more farsight in jingle.py --- src/common/jingle.py | 224 ++++++++++++++++++++++++++++++++----------- 1 file changed, 168 insertions(+), 56 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 2f52c0885..10b9ce3ee 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -61,20 +61,23 @@ class JingleSession(object): # use .prepend() to add new callbacks, especially when you're going # to send error instead of ack self.callbacks={ - 'content-accept': [self.__contentAcceptCB, self.__defaultCB], + 'content-accept': [self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], 'content-add': [self.__defaultCB], 'content-modify': [self.__defaultCB], 'content-remove': [self.__defaultCB], - 'session-accept': [self.__contentAcceptCB, self.__defaultCB], + 'session-accept': [self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], 'session-info': [self.__defaultCB], - 'session-initiate': [self.__sessionInitiateCB, self.__defaultCB], + 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, self.__defaultCB], 'session-terminate': [self.__defaultCB], - 'transport-info': [self.__defaultCB], + 'transport-info': [self.__broadcastCB, self.__defaultCB], 'iq-result': [], 'iq-error': [], } # for making streams using farsight + import gc + gc.disable() + print self.weinitiate, "#farsight_session_factory_make" self.p2psession = farsight.farsight_session_factory_make('rtp') self.p2psession.connect('error', self.on_p2psession_error) @@ -123,39 +126,37 @@ class JingleSession(object): error = stanza.getTag('error') if error: # it's an iq-error stanza - callables = 'iq-error' + action = 'iq-error' elif jingle: # it's a jingle action action = jingle.getAttr('action') - callables = action else: # it's an iq-result (ack) stanza - callables = 'iq-result' + action = 'iq-result' - callables = self.callbacks[callables] + callables = self.callbacks[action] try: for callable in callables: - callable(stanza=stanza, jingle=jingle, error=error) + callable(stanza=stanza, jingle=jingle, error=error, action=action) except xmpp.NodeProcessed: pass - def __defaultCB(self, stanza, jingle, error): + 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 __contentAcceptCB(self, stanza, jingle, error): + 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, call their callbacks + # check which contents are accepted for content in jingle.iterTags('content'): creator = content['creator'] name = content['name'] - - def __sessionInitiateCB(self, stanza, jingle, error): + def __sessionInitiateCB(self, stanza, jingle, error, action): ''' We got a jingle session request from other entity, therefore we are the receiver... Unpack the data. ''' self.initiator = jingle['initiator'] @@ -169,7 +170,7 @@ class JingleSession(object): tran_ns = element.getTag('transport').getNamespace() if desc_ns==xmpp.NS_JINGLE_AUDIO and tran_ns==xmpp.NS_JINGLE_ICE_UDP: # we've got voip content - self.addContent(element['name'], JingleVoiP(self, node=element), 'peer') + self.addContent(element['name'], JingleVoiP(self), 'peer') fail = False if fail: @@ -181,8 +182,16 @@ class JingleSession(object): self.state = JingleStates.pending + 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 on_p2psession_error(self, *anything): - print "Farsight session error!" + print self.weinitiate, "Farsight session error!" ''' Methods that make/send proper pieces of XML. They check if the session is in appropriate state. ''' @@ -280,12 +289,12 @@ class Codec(object): return True def toXML(self): - return xmpp.Node('payload', + return xmpp.Node('payload-type', attrs=self.attrs, payload=(xmpp.Node('parameter', {'name': k, 'value': v}) for k,v in self.params)) class JingleAudioSession(object): - __metaclass__=meta.VerboseClassType +# __metaclass__=meta.VerboseClassType def __init__(self, content, fromNode): self.content = content @@ -353,19 +362,6 @@ class JingleAudioSession(object): self.responder_codecs = self.getOurCodecs(self.initiator_codecs) return self.__codecsList(self.responder_codecs) -class JingleICEUDPSession(object): - __metaclass__=meta.VerboseClassType - def __init__(self, content): - self.content = content - - def _sessionInitiateCB(self): - ''' Called when we initiate the session. ''' - pass - - def toXML(self): - ''' ICE-UDP doesn't send much in its transport stanza... ''' - return xmpp.Node(xmpp.NS_JINGLE_ICE_UDP+' transport') - class JingleContent(object): ''' An abstraction of content in Jingle sessions. ''' def __init__(self, session, node=None): @@ -379,10 +375,10 @@ class JingleContent(object): class JingleVoiP(JingleContent): ''' Jingle VoiP sessions consist of audio content transported over an ICE UDP protocol. ''' - __metaclass__=meta.VerboseClassType +# __metaclass__=meta.VerboseClassType def __init__(self, session, node=None): JingleContent.__init__(self, session, node) - self.codecs = None + self.got_codecs = False #if node is None: # self.audio = JingleAudioSession(self) @@ -391,12 +387,73 @@ class JingleVoiP(JingleContent): #self.transport = JingleICEUDPSession(self) self.setupStream() + def stanzaCB(self, stanza, content, error, action): + ''' Called when something related to our content was sent by peer. ''' + callbacks = { + 'content-accept': [self.__getRemoteCodecsCB], + 'content-add': [], + 'content-modify': [], + 'content-remove': [], + 'session-accept': [self.__getRemoteCodecsCB], + 'session-info': [], + 'session-initiate': [self.__getRemoteCodecsCB], + 'session-terminate': [], + 'transport-info': [self.__transportInfoCB], + 'iq-result': [], + 'iq-error': [], + }[action] + for callback in callbacks: + callback(stanza, content, error, action) + + def __getRemoteCodecsCB(self, stanza, content, error, action): + if self.got_codecs: return + + codecs = [] + for codec in content.getTag('description').iterTags('payload-type'): + c = {'id': int(codec['id']), + 'encoding_name': codec['name'], + 'media_type': farsight.MEDIA_TYPE_AUDIO, + 'channels': 1, + 'params': dict((p['name'], p['value']) for p in codec.iterTags('parameter'))} + if 'channels' in codec: c['channels']=codec['channels'] + codecs.append(c) + if len(codecs)==0: return + + print self.session.weinitiate, "#farsight_stream_set_remote_codecs" + self.p2pstream.set_remote_codecs(codecs) + self.got_codecs=True + + def __transportInfoCB(self, stanza, content, error, action): + ''' Got a new transport candidate. ''' + candidates = [] + for candidate in content.getTag('transport').iterTags('candidate'): + cand={ + # 'candidate_id': str(self.session.connection.connection.getAnID()), + 'candidate_id': candidate['cid'], + 'component': int(candidate['component']), + 'ip': candidate['ip'], + 'port': int(candidate['port']), + 'proto': candidate['protocol']=='udp' and farsight.NETWORK_PROTOCOL_UDP \ + or farsight.NETWORK_PROTOCOL_TCP, + 'proto_subtype':'RTP', + 'proto_profile':'AVP', + 'preference': float(candidate['priority'])/100000, + # 'type': farsight.CANDIDATE_TYPE_LOCAL, + 'type': int(candidate['type']), + } + if 'ufrag' in candidate: cand['username']=candidate['ufrag'] + if 'pwd' in candidate: cand['password']=candidate['pwd'] + + candidates.append(cand) + print self.session.weinitiate, "#add_remote_candidate" + self.p2pstream.add_remote_candidate(candidates) + def toXML(self): ''' Return proper XML for element. ''' return xmpp.Node('content', attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, payload=[ - xmpp.Node(xmpp.NS_JINGLE_AUDIO+' description', payload=self.getCodecs()), + xmpp.Node(xmpp.NS_JINGLE_AUDIO+' description', payload=self.iterCodecs()), xmpp.Node(xmpp.NS_JINGLE_ICE_UDP+' transport') ]) @@ -407,6 +464,7 @@ class JingleVoiP(JingleContent): payload=payload) def setupStream(self): + print self.session.weinitiate, "#farsight_session_create_stream" self.p2pstream = self.session.p2psession.create_stream( farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) self.p2pstream.set_property('transmitter', 'libjingle') @@ -416,37 +474,91 @@ class JingleVoiP(JingleContent): self.p2pstream.connect('native-candidates-prepared', self.on_p2pstream_native_candidates_prepared) self.p2pstream.connect('state-changed', self.on_p2pstream_state_changed) self.p2pstream.connect('new-native-candidate', self.on_p2pstream_new_native_candidate) + + self.p2pstream.set_remote_codecs(self.p2pstream.get_local_codecs()) + + print self.session.weinitiate, "#farsight_stream_prepare_transports" self.p2pstream.prepare_transports() + print self.session.weinitiate, "#farsight_stream_set_active_codec" + self.p2pstream.set_active_codec(8) #??? + + sink = gst.element_factory_make('alsasink') + sink.set_property('sync', False) + sink.set_property('latency-time', 20000) + sink.set_property('buffer-time', 80000) + + src = gst.element_factory_make('audiotestsrc') + src.set_property('blocksize', 320) + #src.set_property('latency-time', 20000) + src.set_property('is-live', True) + + print self.session.weinitiate, "#farsight_stream_set_sink" + self.p2pstream.set_sink(sink) + print self.session.weinitiate, "#farsight_stream_set_source" + self.p2pstream.set_source(src) + def on_p2pstream_error(self, *whatever): pass - def on_p2pstream_new_active_candidate_pair(self, *whatever): pass - def on_p2pstream_codec_changed(self, *whatever): pass - def on_p2pstream_native_candidates_prepared(self, *whatever): pass - def on_p2pstream_state_changed(self, *whatever): pass + def on_p2pstream_new_active_candidate_pair(self, stream, native, remote): + print self.session.weinitiate, "##new_active_candidate_pair" + #print "New native candidate pair: %s, %s" % (native, remote) + def on_p2pstream_codec_changed(self, stream, codecid): + print self.session.weinitiate, "##codec_changed" + #print "Codec changed: %d" % codecid + def on_p2pstream_native_candidates_prepared(self, *whatever): + print self.session.weinitiate, "##native_candidates_prepared" + #print "Native candidates prepared: %r" % whatever + for candidate in self.p2pstream.get_native_candidate_list(): + self.send_candidate(candidate) + def on_p2pstream_state_changed(self, stream, state, dir): + print self.session.weinitiate, "##state_changed" + #print "State: %d, Dir: %d" % (state, dir) + if state==farsight.STREAM_STATE_CONNECTED: + print self.session.weinitiate, "#farsight_stream_signal_native_candidates_prepared" + stream.signal_native_candidates_prepared() + print self.session.weinitiate, "#farsight_stream_start" + stream.start() def on_p2pstream_new_native_candidate(self, p2pstream, candidate_id): + print self.session.weinitiate, "##new_native_candidate" + print self.session.weinitiate, "#get_native_candidate" candidates = p2pstream.get_native_candidate(candidate_id) + print self.session.weinitiate, "#!", repr(candidates) for candidate in candidates: - attrs={ - 'component': candidate['component'], - 'foundation': '1', # hack - 'generation': '0', - 'ip': candidate['ip'], - 'network': '0', - 'port': candidate['port'], - 'priority': int(100000*candidate['preference']), # hack - 'protocol': candidate['proto']==farsight.NETWORK_PROTOCOL_UDP and 'udp' or 'tcp', - } - if 'username' in candidate: attrs['ufrag']=candidate['username'] - if 'password' in candidate: attrs['pwd']=candidate['password'] - c=self.__content() - t=c.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') - t.addChild('candidate', attrs=attrs) - self.session.sendTransportInfo(c) + self.send_candidate(candidate) + def send_candidate(self, candidate): + attrs={ + 'cid': candidate['candidate_id'], + 'component': candidate['component'], + 'foundation': '1', # hack + 'generation': '0', + 'type': candidate['type'], + 'ip': candidate['ip'], + 'network': '0', + 'port': candidate['port'], + 'priority': int(100000*candidate['preference']), # hack + 'protocol': candidate['proto']==farsight.NETWORK_PROTOCOL_UDP and 'udp' or 'tcp', + } + if 'username' in candidate: attrs['ufrag']=candidate['username'] + if 'password' in candidate: attrs['pwd']=candidate['password'] + c=self.__content() + t=c.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') + t.addChild('candidate', attrs=attrs) + self.session.sendTransportInfo(c) - def getCodecs(self): + def iterCodecs(self): + print self.session.weinitiate, "#farsight_stream_get_local_codecs" codecs=self.p2pstream.get_local_codecs() - return (xmpp.Node('payload', attrs=a) for a in codecs) + for codec in codecs: + a = {'name': codec['encoding_name'], + 'id': codec['id'], + 'channels': 1} + if 'clock_rate' in codec: a['clockrate']=codec['clock_rate'] + if 'optional_params' in codec: + p = (xmpp.Node('parameter', {'name': name, 'value': value}) + for name, value in codec['optional_params'].iteritems()) + else: p = () + yield xmpp.Node('payload-type', a, p) class ConnectionJingle(object): ''' This object depends on that it is a part of Connection class. ''' From c389921e5346afd8fa34c20190d13d7db18b83f3 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Sun, 19 Aug 2007 19:07:49 +0000 Subject: [PATCH 17/67] Jingle: a try with popen()ing external file for p2p connection --- src/common/jingle-handler.py | 258 ++++++++++++++ src/common/jingle.popen.py | 666 +++++++++++++++++++++++++++++++++++ 2 files changed, 924 insertions(+) create mode 100755 src/common/jingle-handler.py create mode 100644 src/common/jingle.popen.py diff --git a/src/common/jingle-handler.py b/src/common/jingle-handler.py new file mode 100755 index 000000000..6c6916359 --- /dev/null +++ b/src/common/jingle-handler.py @@ -0,0 +1,258 @@ +#!/usr/bin/python + +import sys, dl, gst, gobject +sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) +import farsight +from socket import * + +TYPE_CANDIDATE=0 +TYPE_CODEC=1 +TYPE_READY_BYTE=2 + +MODE_DUPLEX=0 +MODE_AUDIO=1 +MODE_VIDEO=2 + +send_sock = 0 +recv_sock = 0 +serv_addr = None +audio_stream = None +video_stream = None + +def stream_error(stream, error, debug): + pass + #print "stream_error: stream=%r error=%s" % (stream, error) + +def session_error(session, error, debug): + pass + #print "session_error: session=%r error=%s" % (session, error) + +def new_active_candidate_pair(stream, native, remote): + pass + #print "new_active_candidate_pair: native %s, remote %s" % (native, remote) + #send_codecs(stream.get_local_codecs()) + +def codec_changed(stream, codec_id): + #print "codec_changed: codec_id=%d, stream=%r" % (codec_id, stream) + pass + +def native_candidates_prepared(stream): + return + print "native_candidates_prepared: stream=%r" % stream + for info in stream.get_native_candidate_list(): + print "Local transport candidate: %s %d %s %s %s %d pref %f" % ( + info['candidate_id'], info['component'], + (info['proto']==farsight.NETWORK_PROTOCOL_TCP) and "TCP" or "UDP", + info['proto_subtype'], info['ip'], info['port'], info['preference']); + +def show_element(e): + return + print 'element: ...' + +def state_changed(stream, state, dir): + if state==farsight.STREAM_STATE_DISCONNECTED: + #print "state_changed: disconnected" + pass + elif state==farsight.STREAM_STATE_CONNECTING: + #print "state_changed: connecting" + pass + elif state==farsight.STREAM_STATE_CONNECTED: + #print "state_changed: connectied" + #print "WW: stream.signal_native_candidates_prepared()" + #print "WW: stream.start()" + stream.signal_native_candidates_prepared() + stream.start() + +def send_codecs(codecs): + global send_sock, serv_addr + for codec in codecs: + if 'channels' not in codec: codec['channels']=1 + s="%d %d %s %d %d %d " % (TYPE_CODEC, codec['media_type'], + codec['encoding_name'], codec['id'], codec['clock_rate'], + codec['channels']) + print s + s="%d %d %s %d %d %d " % (TYPE_CODEC, codec['media_type'], 'LAST', 0, 0, 0) + print s + +def send_candidate(type, trans): + global send_sock, serv_addr + s="%d %d %s %s %d %s %s" % (TYPE_CANDIDATE, + type, trans['candidate_id'], trans['ip'], trans['port'], + trans['username'], trans['password']) + print s + +def new_native_candidate(stream, candidate_id): + candidate=stream.get_native_candidate(candidate_id) + trans=candidate[0] + + #print "New native candidate: " % ( + # trans['candidate_id'], trans['component'], trans['ip'], trans['port'], trans['proto'], + # trans['proto_subtype'], trans['proto_profile'], trans['preference'], trans['type'], + # trans['username'], trans['password']) + + type=stream.get_property("media-type") + + send_candidate(type, trans) + +remote_codecs_audio = [] +remote_codecs_video = [] +def add_remote_codec(stream, pt, encoding_name, media_type, clock_rate, channels): + global remote_codecs_audio, remote_codecs_video + if encoding_name=="LAST": + if media_type==farsight.MEDIA_TYPE_AUDIO: + #print "WW: set_remote_codecs(remote_codecs_audio), %r" % (remote_codecs_audio,) + stream.set_remote_codecs(remote_codecs_audio) + else: + stream.set_remote_codecs(remote_codecs_video) + else: + codec={'id': pt, 'encoding_name': encoding_name, 'media_type': media_type, + 'clock_rate': clock_rate, 'channels': channels} + if media_type==farsight.MEDIA_TYPE_AUDIO: + remote_codecs_audio.append(codec) + elif media_type==farsight.MEDIA_TYPE_VIDEO: + remote_codecs_video.append(codec) + + #for codec in remote_codecs_audio: + # print "added audio codec: %d: %s/%d found" % (codec['id'], codec['encoding_name'], codec['clock_rate']) + #for codec in remote_codecs_video: + # print "added video codec: %d: %s/%d found" % (codec['id'], codec['encoding_name'], codec['clock_rate']) + +def add_remote_candidate(stream, id, ip, port, username, password): + if not stream: return + + trans={'candidate_id': id, 'component': 1, 'ip': ip, 'port': port, 'proto': farsight.NETWORK_PROTOCOL_UDP, + 'proto_subtype': 'RTP', 'proto_profile': 'AVP', 'preference': 1.0, + 'type': farsight.CANDIDATE_TYPE_LOCAL, 'username': username, 'password': password} + + #print "WW: add_remote_candidate(%r)" % ([trans],) + stream.add_remote_candidate([trans]); + +def receive_loop(ch, cond, *data): + #print 'receive_loop called!' + global recv_sock, serv_addr, audio_stream, remote_stream + #print "waiting for msg from %r" % (serv_addr,) + buf=raw_input() + #buf,a = recv_sock.recvfrom(1500) + #print "got message! ",buf + msg = buf.split(' ') + type = int(msg.pop(0)) + #print msg + if type==TYPE_CANDIDATE: + media_type, id, ip, port, username, password = msg[:6] + media_type=int(media_type) + port=int(port) + #print "Received %d %s %s %d %s %s" % (media_type, id, ip, port, username, password) + if media_type==farsight.MEDIA_TYPE_AUDIO: + add_remote_candidate(audio_stream, id, ip, port, username, password) + elif media_type==farsight.MEDIA_TYPE_VIDEO: + add_remote_candidate(video_stream, id, ip, port, username, password) + elif type==TYPE_CODEC: + media_type, encoding_name, pt, clock_rate, channels=msg[:5] + media_type=int(media_type) + pt=int(pt) + clock_rate=int(clock_rate) + channels=int(channels) + + #print "Received %d %s %d %d %d" % (media_type, encoding_name, pt, clock_rate, channels) + + if media_type==farsight.MEDIA_TYPE_AUDIO: + add_remote_codec(audio_stream, pt, encoding_name, media_type, clock_rate, channels) + elif media_type==farsight.MEDIA_TYPE_VIDEO: + add_remote_codec(video_stream, pt, encoding_name, media_type, clock_rate, channels) + elif type==TYPE_READY_BYTE: + #print "got ready byte" + pass + return True + +def set_xoverlay(bus, message, xid): + print "set_xoverlay: called" + if message!=gst.MESSAGE_ELEMENT: return gst.BUS_PASS + + if not message.structure.has_name('prepare-xwindow-id'): return gst.BUS_PASS + + print "set_xoverlay: setting x overlay window id" + message.set_xwindow_id(xid) + + return gst.BUS_DROP + +def main(): + global audio_stream, remote_stream + #if len(sys.argv)<4 or len(sys.argv)>6: + # #print 'usage: %s remoteip remoteport localport [mode] [xid]' % sys.argv[0] + # return -1 + + #if len(sys.argv)>=5: + # mode = int(sys.argv[4]) + #else: + mode = MODE_DUPLEX + + #setup_send(sys.argv[1], int(sys.argv[2])) + #print "Sending to %s %d listening on port %d" % (sys.argv[1], int(sys.argv[2]), int(sys.argv[3])) + + #recv_chan=setup_recv(int(sys.argv[3])) + + #do_handshake(recv_chan) + + gobject.io_add_watch(0, gobject.IO_IN, receive_loop) + + session = setup_rtp_session() + + if mode==MODE_AUDIO or mode==MODE_VIDEO: + audio_stream = setup_rtp_stream(session, farsight.MEDIA_TYPE_AUDIO) + if mode==MODE_VIDEO or mode==MODE_DUPLEX: + video_stream = setup_rtp_stream(session, farsight.MEDIA_TYPE_VIDEO) + + if audio_stream: + audio_stream.set_active_codec(8) + + alsasrc = gst.element_factory_make('audiotestsrc', 'src') + alsasink= gst.element_factory_make('alsasink', 'alsasink') + + alsasink.set_property('sync', False) + alsasink.set_property('latency-time', 20000) + alsasink.set_property('buffer-time', 80000) + alsasrc.set_property('blocksize', 320) + #alsasrc.set_property('latency-time', 20000) + alsasrc.set_property('is-live', True) + + #print "WW: set_sink(%r)" % (alsasink,) + audio_stream.set_sink(alsasink) + #print "WW: set_source(%r)" % (alsasrc,) + audio_stream.set_source(alsasrc) + + #if video_stream: assert False + + gobject.MainLoop().run() + #print 'bu!' + +def setup_rtp_session(): + session = farsight.farsight_session_factory_make('rtp') + + session.connect('error', session_error) + return session + +def setup_rtp_stream(session, type): + stream = session.create_stream(type, farsight.STREAM_DIRECTION_BOTH) + + stream.set_property('transmitter', 'libjingle') + + stream.connect('error', stream_error) + stream.connect('new-active-candidate-pair', new_active_candidate_pair) + stream.connect('codec-changed', codec_changed) + stream.connect('native-candidates-prepared', native_candidates_prepared) + stream.connect('state-changed', state_changed) + stream.connect('new-native-candidate', new_native_candidate) + + possible_codecs = stream.get_local_codecs() + + #for codec in possible_codecs: + # print "codec: %d: %s/%d found" % (codec['id'], codec['encoding_name'], codec['clock_rate']) + + send_codecs(possible_codecs) + + stream.prepare_transports() + + return stream + + +main() diff --git a/src/common/jingle.popen.py b/src/common/jingle.popen.py new file mode 100644 index 000000000..e991f6b5c --- /dev/null +++ b/src/common/jingle.popen.py @@ -0,0 +1,666 @@ +## +## 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 the jingle signalling protocol. ''' + +import gajim +import xmpp +import gobject + +import sys, dl, gst, gobject +sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) +import farsight + + +import meta + +class JingleStates(object): + ''' States in which jingle session may exist. ''' + ended=0 + pending=1 + active=2 + +class Exception(object): pass +class WrongState(Exception): pass +class NoCommonCodec(Exception): pass + +class JingleSession(object): + ''' This represents one jingle session. ''' + __metaclass__=meta.VerboseClassType + 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 + + # 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.__defaultCB], + 'content-modify': [self.__defaultCB], + 'content-remove': [self.__defaultCB], + 'session-accept': [self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], + 'session-info': [self.__defaultCB], + 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, self.__defaultCB], + 'session-terminate': [self.__defaultCB], + 'transport-info': [self.__broadcastCB, self.__defaultCB], + 'iq-result': [], + 'iq-error': [], + } + + ''' Middle-level functions to manage contents. Handle local content + cache and send change notifications. ''' + def addContent(self, name, content, creator='we'): + ''' Add new content to session. If the session is active, + this will send proper stanza to update session. + The protocol prohibits changing that when pending. + Creator must be one of ('we', 'peer', 'initiator', 'responder')''' + if self.state==JingleStates.pending: + raise WrongState + + 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 self.state==JingleStates.active: + pass # TODO: send proper stanza, shouldn't be needed now + + def removeContent(self, creator, name): + ''' We do not need this now ''' + pass + + def modifyContent(self, creator, name, *someother): + ''' We do not need this now ''' + pass + + ''' Middle-level function to do stanza exchange. ''' + def startSession(self): + ''' Start session. ''' + self.__sessionInitiate() + + def sendSessionInfo(self): pass + + ''' 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') + 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 + + 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 __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'] + + def __sessionInitiateCB(self, stanza, jingle, error, action): + ''' We got a jingle session request from other entity, + therefore we are the receiver... Unpack the data. ''' + self.initiator = jingle['initiator'] + self.responder = self.ourjid + self.peerjid = self.initiator + + fail = True + for element in jingle.iterTags('content'): + # checking what kind of session this will be + desc_ns = element.getTag('description').getNamespace() + tran_ns = element.getTag('transport').getNamespace() + if desc_ns==xmpp.NS_JINGLE_AUDIO and tran_ns==xmpp.NS_JINGLE_ICE_UDP: + # we've got voip content + self.addContent(element['name'], JingleVoiP(self), 'peer') + fail = False + + if fail: + # TODO: we should send inside too + self.connection.connection.send( + xmpp.Error(stanza, xmpp.NS_STANZAS + 'feature-not-implemented')) + self.connection.deleteJingle(self) + raise xmpp.NodeProcessed + + self.state = JingleStates.pending + + 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 on_p2psession_error(self, *anything): + print self.weinitiate, "Farsight session error!" + + ''' Methods that make/send proper pieces of XML. They check if the session + is in appropriate state. ''' + def __makeJingle(self, action): + stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) + jingle = stanza.addChild('jingle', attrs={ + 'xmlns': 'http://www.xmpp.org/extensions/xep-0166.html#ns', + 'action': action, + 'initiator': self.initiator, + 'responder': self.responder, + 'sid': self.sid}) + return stanza, jingle + + def __appendContent(self, jingle, content, full=True): + ''' Append element to element, + with (full=True) or without (full=False) + children. ''' + if full: + jingle.addChild(node=content.toXML()) + else: + jingle.addChild('content', + attrs={'name': content.name, 'creator': content.creator}) + + def __appendContents(self, jingle, full=True): + ''' Append all elements to .''' + # TODO: integrate with __appendContent? + # TODO: parameters 'name', 'content'? + for content in self.contents.values(): + self.__appendContent(jingle, content, full=full) + + def __sessionInitiate(self): + assert self.state==JingleStates.ended + stanza, jingle = self.__makeJingle('session-initiate') + self.__appendContents(jingle) + self.connection.connection.send(stanza) + + def __sessionAccept(self): + assert self.state==JingleStates.pending + stanza, jingle = self.__jingle('session-accept') + self.__appendContents(jingle, False) + self.connection.connection.send(stanza) + self.state=JingleStates.active + + def __sessionInfo(self, payload=None): + assert self.state!=JingleStates.ended + stanza, jingle = self.__jingle('session-info') + if payload: + jingle.addChild(node=payload) + self.connection.connection.send(stanza) + + def __sessionTerminate(self): + assert self.state!=JingleStates.ended + stanza, jingle = self.__jingle('session-terminate') + self.connection.connection.send(stanza) + + def __contentAdd(self): + assert self.state==JingleStates.active + + def __contentAccept(self): + assert self.state!=JingleStates.ended + + def __contentModify(self): + assert self.state!=JingleStates.ended + + def __contentRemove(self): + assert self.state!=JingleStates.ended + + def sendTransportInfo(self, content): + assert self.state!=JingleStates.ended + stanza, jingle = self.__makeJingle('transport-info') + jingle.addChild(node=content) + self.connection.connection.send(stanza) + + '''Callbacks''' + def sessionTerminateCB(self, stanza): pass + +class Codec(object): + ''' This class keeps description of a single codec. ''' + def __init__(self, name, id=None, **params): + ''' Create new codec description. ''' + self.name = name + self.id = id + self.attrs = {'name': self.name, 'id': self.id, 'channels': 1} + for key in ('channels', 'clockrate', 'maxptime', 'ptime'): + if key in params: + self.attrs[key]=params[key] + del params[key] + self.params = params + + def __eq__(a, b): + ''' Compare two codec descriptions. ''' + # TODO: check out what should be tested... + if a.name!=b.name: return False + # ... + return True + + def toXML(self): + return xmpp.Node('payload-type', + attrs=self.attrs, + payload=(xmpp.Node('parameter', {'name': k, 'value': v}) for k,v in self.params)) + +class JingleAudioSession(object): +# __metaclass__=meta.VerboseClassType + def __init__(self, content, fromNode): + self.content = content + + self.initiator_codecs=[] + self.responder_codecs=[] + + if fromNode: + # read all codecs peer understand + for payload in fromNode.iterTags('payload-type'): + attrs = fromNode.getAttrs().copy() + for param in fromNode.iterTags('parameter'): + attrs[param['name']]=param['value'] + self.initiator_codecs.append(Codec(**attrs)) + + def sessionInitiateCB(self, stanza, ourcontent): + pass + + ''' "Negotiation" of codecs... simply presenting what *we* can do, nothing more... ''' + def getOurCodecs(self, other=None): + ''' Get a list of codecs we support. Try to get them in the same + order as the codecs of our peer. If other!=None, raise + a NoCommonCodec error if no codecs both sides support (None means + we are initiating the connection and we don't know the other + peer's codecs.) ''' + # for now we "understand" only one codec -- speex with clockrate 16000 + # so we have an easy job to do... (codecs sorted in order of preference) + supported_codecs=[ + Codec('speex', clockrate='16000'), + ] + + other_l = other if other is not None else [] + our_l = supported_codecs[:] + out = [] + ids = range(128) + for codec in other_l: + if codec in our_l: + out.append(codec) + our_l.remove(codec) + try: ids.remove(codec.id) + except ValueError: pass # when id is not a dynamic one + + if other is not None and len(out)==0: + raise NoCommonCodec + + for codec in our_l: + if not codec.id or codec.id not in ids: + codec.id = ids.pop() + out.append(codec) + + return out + + ''' Methods for making proper pieces of XML. ''' + def __codecsList(self, codecs): + ''' Prepares a description element with codecs given as a parameter. ''' + return xmpp.Node(xmpp.NS_JINGLE_AUDIO+' description', + payload=(codec.toXML() for codec in codecs)) + + def toXML(self): + if not self.initiator_codecs: + # we are the initiator, so just send our codecs + self.initiator_codecs = self.getOurCodecs() + return self.__codecsList(self.initiator_codecs) + else: + # we are the responder, we SHOULD adjust our codec list + self.responder_codecs = self.getOurCodecs(self.initiator_codecs) + return self.__codecsList(self.responder_codecs) + +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 + +class JingleVoiP(JingleContent): + ''' Jingle VoiP sessions consist of audio content transported + over an ICE UDP protocol. ''' +# __metaclass__=meta.VerboseClassType + def __init__(self, session, node=None): + JingleContent.__init__(self, session, node) + self.got_codecs = False + self.codecs = [] + + #if node is None: + # self.audio = JingleAudioSession(self) + #else: + # self.audio = JingleAudioSession(self, node.getTag('content')) + #self.transport = JingleICEUDPSession(self) + self.setupStream() + + def stanzaCB(self, stanza, content, error, action): + ''' Called when something related to our content was sent by peer. ''' + callbacks = { + 'content-accept': [self.__getRemoteCodecsCB], + 'content-add': [], + 'content-modify': [], + 'content-remove': [], + 'session-accept': [self.__getRemoteCodecsCB], + 'session-info': [], + 'session-initiate': [self.__getRemoteCodecsCB], + 'session-terminate': [], + 'transport-info': [self.__transportInfoCB], + 'iq-result': [], + 'iq-error': [], + }[action] + for callback in callbacks: + callback(stanza, content, error, action) + + def __getRemoteCodecsCB(self, stanza, content, error, action): + if self.got_codecs: return + + codecs = [] + for codec in content.getTag('description').iterTags('payload-type'): + c = {'id': int(codec['id']), + 'encoding_name': codec['name'], + 'media_type': farsight.MEDIA_TYPE_AUDIO, + 'channels': 1, + 'params': dict((p['name'], p['value']) for p in codec.iterTags('parameter'))} + if 'channels' in codec: c['channels']=codec['channels'] + codecs.append(c) + self.p2pin.write('%d %d %s %d %d %d\n' % (1, farsight.MEDIA_TYPE_AUDIO, + codec['encoding_name'], codec['id'], codec['clock_rate'], codec['channels'])) + if len(codecs)==0: return + + print self.session.weinitiate, "#farsight_stream_set_remote_codecs" + #self.p2pstream.set_remote_codecs(codecs) + self.p2pin.write('%d %d %s %d %d %d\n' % (1, farsight.MEDIA_TYPE_AUDIO, + 'LAST', 0, 0, 0)) + self.got_codecs=True + + def __transportInfoCB(self, stanza, content, error, action): + ''' Got a new transport candidate. ''' + candidates = [] + for candidate in content.getTag('transport').iterTags('candidate'): + cand={ + # 'candidate_id': str(self.session.connection.connection.getAnID()), + 'candidate_id': candidate['cid'], + 'component': int(candidate['component']), + 'ip': candidate['ip'], + 'port': int(candidate['port']), + 'proto': candidate['protocol']=='udp' and farsight.NETWORK_PROTOCOL_UDP \ + or farsight.NETWORK_PROTOCOL_TCP, + 'proto_subtype':'RTP', + 'proto_profile':'AVP', + 'preference': float(candidate['priority'])/100000, + # 'type': farsight.CANDIDATE_TYPE_LOCAL, + 'type': int(candidate['type']), + } + if 'ufrag' in candidate: cand['username']=candidate['ufrag'] + if 'pwd' in candidate: cand['password']=candidate['pwd'] + + self.p2pin.write('%d %d %s %s %d %s %s\n' % (0, int(candidate['type']), candidate['cid'], candidate['ip'], int(candidate['port']), candidate['ufrag'], candidate['pwd'])) + candidates.append(cand) + print self.session.weinitiate, "#add_remote_candidate" + #self.p2pstream.add_remote_candidate(candidates) + + def toXML(self): + ''' Return proper XML for element. ''' + return xmpp.Node('content', + attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, + payload=[ + xmpp.Node(xmpp.NS_JINGLE_AUDIO+' description', payload=self.iterCodecs()), + xmpp.Node(xmpp.NS_JINGLE_ICE_UDP+' transport') + ]) + + def __content(self, payload=[]): + ''' Build a XML content-wrapper for our data. ''' + return xmpp.Node('content', + attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, + payload=payload) + + def new_data(self, *things): + buf = self.p2pout.readline() + print "RECV: %r"%buf + msg = buf.split(' ') + try: + type = int(msg.pop(0)) + except: + return True + #print msg + if type==0: + media_type, id, ip, port, username, password = msg[:6] + media_type=int(media_type) + port=int(port) + #print "Received %d %s %s %d %s %s" % (media_type, id, ip, port, username, password) + self.send_candidate({'candidate_id': id, 'component': 1, 'ip': ip, + 'port': port, 'username': username, 'password': password, + 'proto': farsight.NETWORK_PROTOCOL_UDP, 'proto_subtype': 'RTP', + 'proto_profile': 'AVP', 'preference': 1.0, 'type': 0}) + elif type==1: + media_type, encoding_name, pt, clock_rate, channels=msg[:5] + media_type=int(media_type) + pt=int(pt) + clock_rate=int(clock_rate) + channels=int(channels) + + #print "Received %d %s %d %d %d" % (media_type, encoding_name, pt, clock_rate, channels) + if encoding_name!='LAST': + self.codecs.append((media_type, encoding_name, pt, clock_rate, channels)) + else: + self.got_codecs=True + return True + + def setupStream(self): + import popen2 + self.p2pout, self.p2pin = popen2.popen2('/home/liori/Projekty/SoC/trunk/src/common/jingle-handler.py') + gobject.io_add_watch(self.p2pout, gobject.IO_IN, self.new_data) + while not self.got_codecs: + self.new_data() + return + print self.session.weinitiate, "#farsight_session_create_stream" + self.p2pstream = self.session.p2psession.create_stream( + farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) + self.p2pstream.set_property('transmitter', 'libjingle') + self.p2pstream.connect('error', self.on_p2pstream_error) + self.p2pstream.connect('new-active-candidate-pair', self.on_p2pstream_new_active_candidate_pair) + self.p2pstream.connect('codec-changed', self.on_p2pstream_codec_changed) + self.p2pstream.connect('native-candidates-prepared', self.on_p2pstream_native_candidates_prepared) + self.p2pstream.connect('state-changed', self.on_p2pstream_state_changed) + self.p2pstream.connect('new-native-candidate', self.on_p2pstream_new_native_candidate) + + self.p2pstream.set_remote_codecs(self.p2pstream.get_local_codecs()) + + print self.session.weinitiate, "#farsight_stream_prepare_transports" + self.p2pstream.prepare_transports() + + print self.session.weinitiate, "#farsight_stream_set_active_codec" + self.p2pstream.set_active_codec(8) #??? + + sink = gst.element_factory_make('alsasink') + sink.set_property('sync', False) + sink.set_property('latency-time', 20000) + sink.set_property('buffer-time', 80000) + + src = gst.element_factory_make('audiotestsrc') + src.set_property('blocksize', 320) + #src.set_property('latency-time', 20000) + src.set_property('is-live', True) + + print self.session.weinitiate, "#farsight_stream_set_sink" + self.p2pstream.set_sink(sink) + print self.session.weinitiate, "#farsight_stream_set_source" + self.p2pstream.set_source(src) + + def on_p2pstream_error(self, *whatever): pass + def on_p2pstream_new_active_candidate_pair(self, stream, native, remote): + print self.session.weinitiate, "##new_active_candidate_pair" + #print "New native candidate pair: %s, %s" % (native, remote) + def on_p2pstream_codec_changed(self, stream, codecid): + print self.session.weinitiate, "##codec_changed" + #print "Codec changed: %d" % codecid + def on_p2pstream_native_candidates_prepared(self, *whatever): + print self.session.weinitiate, "##native_candidates_prepared" + #print "Native candidates prepared: %r" % whatever + for candidate in self.p2pstream.get_native_candidate_list(): + self.send_candidate(candidate) + def on_p2pstream_state_changed(self, stream, state, dir): + print self.session.weinitiate, "##state_changed" + #print "State: %d, Dir: %d" % (state, dir) + if state==farsight.STREAM_STATE_CONNECTED: + print self.session.weinitiate, "#farsight_stream_signal_native_candidates_prepared" + stream.signal_native_candidates_prepared() + print self.session.weinitiate, "#farsight_stream_start" + stream.start() + def on_p2pstream_new_native_candidate(self, p2pstream, candidate_id): + print self.session.weinitiate, "##new_native_candidate" + print self.session.weinitiate, "#get_native_candidate" + candidates = p2pstream.get_native_candidate(candidate_id) + print self.session.weinitiate, "#!", repr(candidates) + + for candidate in candidates: + self.send_candidate(candidate) + def send_candidate(self, candidate): + attrs={ + 'cid': candidate['candidate_id'], + 'component': candidate['component'], + 'foundation': '1', # hack + 'generation': '0', + 'type': candidate['type'], + 'ip': candidate['ip'], + 'network': '0', + 'port': candidate['port'], + 'priority': int(100000*candidate['preference']), # hack + 'protocol': candidate['proto']==farsight.NETWORK_PROTOCOL_UDP and 'udp' or 'tcp', + } + if 'username' in candidate: attrs['ufrag']=candidate['username'] + if 'password' in candidate: attrs['pwd']=candidate['password'] + c=self.__content() + t=c.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') + t.addChild('candidate', attrs=attrs) + self.session.sendTransportInfo(c) + + def iterCodecs(self): + for codec in self.codecs: + media_type, encoding_name, pt, clock_rate, channels=codec + yield xmpp.Node('payload-type', { + 'name': encoding_name, + 'id': pt, + 'channels': channels, + 'clock_rate': clock_rate}) + +# print self.session.weinitiate, "#farsight_stream_get_local_codecs" +# codecs=self.p2pstream.get_local_codecs() +# for codec in codecs: +# a = {'name': codec['encoding_name'], +# 'id': codec['id'], +# 'channels': 1} +# if 'clock_rate' in codec: a['clockrate']=codec['clock_rate'] +# if 'optional_params' in codec: +# p = (xmpp.Node('parameter', {'name': name, 'value': value}) +# for name, value in codec['optional_params'].iteritems()) +# else: p = () +# yield xmpp.Node('payload-type', a, p) + +class ConnectionJingle(object): + ''' This object depends on that it is a part of Connection class. ''' + def __init__(self): + # dictionary: (jid, sessionid) => JingleSession object + self.__sessions = {} + + # dictionary: (jid, iq stanza id) => JingleSession object, + # one time callbacks + self.__iq_responses = {} + + def addJingle(self, jingle): + ''' Add a jingle session to a jingle stanza dispatcher + jingle - a JingleSession object. + ''' + self.__sessions[(jingle.peerjid, jingle.sid)]=jingle + + def deleteJingle(self, jingle): + ''' Remove a jingle session from a jingle stanza dispatcher ''' + del self.__session[(jingle.peerjid, jingle.sid)] + + def _JingleCB(self, con, stanza): + ''' The jingle stanza dispatcher. + Route jingle stanza to proper JingleSession object, + or create one if it is a new session. + TODO: Also check if the stanza isn't an error stanza, if so + route it adequatelly.''' + + # get data + jid = stanza.getFrom() + id = stanza.getID() + + if (jid, id) in self.__iq_responses.keys(): + self.__iq_responses[(jid, id)].stanzaCB(stanza) + del self.__iq_responses[(jid, id)] + raise xmpp.NodeProcessed + + jingle = stanza.getTag('jingle') + if not jingle: return + sid = jingle.getAttr('sid') + + # do we need to create a new jingle object + if (jid, sid) not in self.__sessions: + # TODO: we should check its type here... + newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) + self.addJingle(newjingle) + + # we already have such session in dispatcher... + self.__sessions[(jid, sid)].stanzaCB(stanza) + + raise xmpp.NodeProcessed + + def addJingleIqCallback(self, jid, id, jingle): + self.__iq_responses[(jid, id)]=jingle + + def startVoiP(self, jid): + jingle = JingleSession(self, weinitiate=True, jid=jid) + self.addJingle(jingle) + jingle.addContent('voice', JingleVoiP(jingle)) + jingle.startSession() From 6a6527e1f3f174af8d8ab6b9c9b7d7750ef8b026 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Sun, 19 Aug 2007 20:01:38 +0000 Subject: [PATCH 18/67] Jingle: fix py2.5 construct --- src/common/jingle.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 10b9ce3ee..f5cbc0ec3 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -325,7 +325,10 @@ class JingleAudioSession(object): Codec('speex', clockrate='16000'), ] - other_l = other if other is not None else [] + if other is not None: + other_l = other + else: + other_l = [] our_l = supported_codecs[:] out = [] ids = range(128) From 7f57a9d2e8bbaa78d3667c5fcf89a1b87d04ff8c Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Mon, 20 Aug 2007 23:14:57 +0000 Subject: [PATCH 19/67] Jingle: wrongly decoded connection parameters from transport-info --- src/common/jingle.py | 23 +++++++++++++++-------- src/common/xmpp/simplexml.py | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index f5cbc0ec3..8092e6f96 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -431,21 +431,24 @@ class JingleVoiP(JingleContent): candidates = [] for candidate in content.getTag('transport').iterTags('candidate'): cand={ - # 'candidate_id': str(self.session.connection.connection.getAnID()), 'candidate_id': candidate['cid'], 'component': int(candidate['component']), 'ip': candidate['ip'], 'port': int(candidate['port']), - 'proto': candidate['protocol']=='udp' and farsight.NETWORK_PROTOCOL_UDP \ - or farsight.NETWORK_PROTOCOL_TCP, 'proto_subtype':'RTP', 'proto_profile':'AVP', 'preference': float(candidate['priority'])/100000, - # 'type': farsight.CANDIDATE_TYPE_LOCAL, - 'type': int(candidate['type']), + 'type': farsight.CANDIDATE_TYPE_LOCAL, } - if 'ufrag' in candidate: cand['username']=candidate['ufrag'] - if 'pwd' in candidate: cand['password']=candidate['pwd'] + 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 + if 'ufrag' in candidate: + cand['username']=candidate['ufrag'] + if 'pwd' in candidate: + cand['password']=candidate['pwd'] candidates.append(cand) print self.session.weinitiate, "#add_remote_candidate" @@ -540,8 +543,12 @@ class JingleVoiP(JingleContent): 'network': '0', 'port': candidate['port'], 'priority': int(100000*candidate['preference']), # hack - 'protocol': candidate['proto']==farsight.NETWORK_PROTOCOL_UDP and 'udp' or 'tcp', } + 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' if 'username' in candidate: attrs['ufrag']=candidate['username'] if 'password' in candidate: attrs['pwd']=candidate['password'] c=self.__content() diff --git a/src/common/xmpp/simplexml.py b/src/common/xmpp/simplexml.py index 7ab0fe907..a4770008f 100644 --- a/src/common/xmpp/simplexml.py +++ b/src/common/xmpp/simplexml.py @@ -255,7 +255,7 @@ class Node(object): return self.delAttr(item) def __contains__(self,item): """ Checks if node has attribute "item" """ - self.has_attr(item) + return self.has_attr(item) def __getattr__(self,attr): """ Reduce memory usage caused by T/NT classes - use memory only when needed. """ if attr=='T': From 389ec35954c325995f0143d0846b444dfdde26d4 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Mon, 20 Aug 2007 23:20:34 +0000 Subject: [PATCH 20/67] Jingle: removing unused files --- src/common/jingle-handler.py | 258 -------------- src/common/jingle.popen.py | 666 ----------------------------------- 2 files changed, 924 deletions(-) delete mode 100755 src/common/jingle-handler.py delete mode 100644 src/common/jingle.popen.py diff --git a/src/common/jingle-handler.py b/src/common/jingle-handler.py deleted file mode 100755 index 6c6916359..000000000 --- a/src/common/jingle-handler.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/python - -import sys, dl, gst, gobject -sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) -import farsight -from socket import * - -TYPE_CANDIDATE=0 -TYPE_CODEC=1 -TYPE_READY_BYTE=2 - -MODE_DUPLEX=0 -MODE_AUDIO=1 -MODE_VIDEO=2 - -send_sock = 0 -recv_sock = 0 -serv_addr = None -audio_stream = None -video_stream = None - -def stream_error(stream, error, debug): - pass - #print "stream_error: stream=%r error=%s" % (stream, error) - -def session_error(session, error, debug): - pass - #print "session_error: session=%r error=%s" % (session, error) - -def new_active_candidate_pair(stream, native, remote): - pass - #print "new_active_candidate_pair: native %s, remote %s" % (native, remote) - #send_codecs(stream.get_local_codecs()) - -def codec_changed(stream, codec_id): - #print "codec_changed: codec_id=%d, stream=%r" % (codec_id, stream) - pass - -def native_candidates_prepared(stream): - return - print "native_candidates_prepared: stream=%r" % stream - for info in stream.get_native_candidate_list(): - print "Local transport candidate: %s %d %s %s %s %d pref %f" % ( - info['candidate_id'], info['component'], - (info['proto']==farsight.NETWORK_PROTOCOL_TCP) and "TCP" or "UDP", - info['proto_subtype'], info['ip'], info['port'], info['preference']); - -def show_element(e): - return - print 'element: ...' - -def state_changed(stream, state, dir): - if state==farsight.STREAM_STATE_DISCONNECTED: - #print "state_changed: disconnected" - pass - elif state==farsight.STREAM_STATE_CONNECTING: - #print "state_changed: connecting" - pass - elif state==farsight.STREAM_STATE_CONNECTED: - #print "state_changed: connectied" - #print "WW: stream.signal_native_candidates_prepared()" - #print "WW: stream.start()" - stream.signal_native_candidates_prepared() - stream.start() - -def send_codecs(codecs): - global send_sock, serv_addr - for codec in codecs: - if 'channels' not in codec: codec['channels']=1 - s="%d %d %s %d %d %d " % (TYPE_CODEC, codec['media_type'], - codec['encoding_name'], codec['id'], codec['clock_rate'], - codec['channels']) - print s - s="%d %d %s %d %d %d " % (TYPE_CODEC, codec['media_type'], 'LAST', 0, 0, 0) - print s - -def send_candidate(type, trans): - global send_sock, serv_addr - s="%d %d %s %s %d %s %s" % (TYPE_CANDIDATE, - type, trans['candidate_id'], trans['ip'], trans['port'], - trans['username'], trans['password']) - print s - -def new_native_candidate(stream, candidate_id): - candidate=stream.get_native_candidate(candidate_id) - trans=candidate[0] - - #print "New native candidate: " % ( - # trans['candidate_id'], trans['component'], trans['ip'], trans['port'], trans['proto'], - # trans['proto_subtype'], trans['proto_profile'], trans['preference'], trans['type'], - # trans['username'], trans['password']) - - type=stream.get_property("media-type") - - send_candidate(type, trans) - -remote_codecs_audio = [] -remote_codecs_video = [] -def add_remote_codec(stream, pt, encoding_name, media_type, clock_rate, channels): - global remote_codecs_audio, remote_codecs_video - if encoding_name=="LAST": - if media_type==farsight.MEDIA_TYPE_AUDIO: - #print "WW: set_remote_codecs(remote_codecs_audio), %r" % (remote_codecs_audio,) - stream.set_remote_codecs(remote_codecs_audio) - else: - stream.set_remote_codecs(remote_codecs_video) - else: - codec={'id': pt, 'encoding_name': encoding_name, 'media_type': media_type, - 'clock_rate': clock_rate, 'channels': channels} - if media_type==farsight.MEDIA_TYPE_AUDIO: - remote_codecs_audio.append(codec) - elif media_type==farsight.MEDIA_TYPE_VIDEO: - remote_codecs_video.append(codec) - - #for codec in remote_codecs_audio: - # print "added audio codec: %d: %s/%d found" % (codec['id'], codec['encoding_name'], codec['clock_rate']) - #for codec in remote_codecs_video: - # print "added video codec: %d: %s/%d found" % (codec['id'], codec['encoding_name'], codec['clock_rate']) - -def add_remote_candidate(stream, id, ip, port, username, password): - if not stream: return - - trans={'candidate_id': id, 'component': 1, 'ip': ip, 'port': port, 'proto': farsight.NETWORK_PROTOCOL_UDP, - 'proto_subtype': 'RTP', 'proto_profile': 'AVP', 'preference': 1.0, - 'type': farsight.CANDIDATE_TYPE_LOCAL, 'username': username, 'password': password} - - #print "WW: add_remote_candidate(%r)" % ([trans],) - stream.add_remote_candidate([trans]); - -def receive_loop(ch, cond, *data): - #print 'receive_loop called!' - global recv_sock, serv_addr, audio_stream, remote_stream - #print "waiting for msg from %r" % (serv_addr,) - buf=raw_input() - #buf,a = recv_sock.recvfrom(1500) - #print "got message! ",buf - msg = buf.split(' ') - type = int(msg.pop(0)) - #print msg - if type==TYPE_CANDIDATE: - media_type, id, ip, port, username, password = msg[:6] - media_type=int(media_type) - port=int(port) - #print "Received %d %s %s %d %s %s" % (media_type, id, ip, port, username, password) - if media_type==farsight.MEDIA_TYPE_AUDIO: - add_remote_candidate(audio_stream, id, ip, port, username, password) - elif media_type==farsight.MEDIA_TYPE_VIDEO: - add_remote_candidate(video_stream, id, ip, port, username, password) - elif type==TYPE_CODEC: - media_type, encoding_name, pt, clock_rate, channels=msg[:5] - media_type=int(media_type) - pt=int(pt) - clock_rate=int(clock_rate) - channels=int(channels) - - #print "Received %d %s %d %d %d" % (media_type, encoding_name, pt, clock_rate, channels) - - if media_type==farsight.MEDIA_TYPE_AUDIO: - add_remote_codec(audio_stream, pt, encoding_name, media_type, clock_rate, channels) - elif media_type==farsight.MEDIA_TYPE_VIDEO: - add_remote_codec(video_stream, pt, encoding_name, media_type, clock_rate, channels) - elif type==TYPE_READY_BYTE: - #print "got ready byte" - pass - return True - -def set_xoverlay(bus, message, xid): - print "set_xoverlay: called" - if message!=gst.MESSAGE_ELEMENT: return gst.BUS_PASS - - if not message.structure.has_name('prepare-xwindow-id'): return gst.BUS_PASS - - print "set_xoverlay: setting x overlay window id" - message.set_xwindow_id(xid) - - return gst.BUS_DROP - -def main(): - global audio_stream, remote_stream - #if len(sys.argv)<4 or len(sys.argv)>6: - # #print 'usage: %s remoteip remoteport localport [mode] [xid]' % sys.argv[0] - # return -1 - - #if len(sys.argv)>=5: - # mode = int(sys.argv[4]) - #else: - mode = MODE_DUPLEX - - #setup_send(sys.argv[1], int(sys.argv[2])) - #print "Sending to %s %d listening on port %d" % (sys.argv[1], int(sys.argv[2]), int(sys.argv[3])) - - #recv_chan=setup_recv(int(sys.argv[3])) - - #do_handshake(recv_chan) - - gobject.io_add_watch(0, gobject.IO_IN, receive_loop) - - session = setup_rtp_session() - - if mode==MODE_AUDIO or mode==MODE_VIDEO: - audio_stream = setup_rtp_stream(session, farsight.MEDIA_TYPE_AUDIO) - if mode==MODE_VIDEO or mode==MODE_DUPLEX: - video_stream = setup_rtp_stream(session, farsight.MEDIA_TYPE_VIDEO) - - if audio_stream: - audio_stream.set_active_codec(8) - - alsasrc = gst.element_factory_make('audiotestsrc', 'src') - alsasink= gst.element_factory_make('alsasink', 'alsasink') - - alsasink.set_property('sync', False) - alsasink.set_property('latency-time', 20000) - alsasink.set_property('buffer-time', 80000) - alsasrc.set_property('blocksize', 320) - #alsasrc.set_property('latency-time', 20000) - alsasrc.set_property('is-live', True) - - #print "WW: set_sink(%r)" % (alsasink,) - audio_stream.set_sink(alsasink) - #print "WW: set_source(%r)" % (alsasrc,) - audio_stream.set_source(alsasrc) - - #if video_stream: assert False - - gobject.MainLoop().run() - #print 'bu!' - -def setup_rtp_session(): - session = farsight.farsight_session_factory_make('rtp') - - session.connect('error', session_error) - return session - -def setup_rtp_stream(session, type): - stream = session.create_stream(type, farsight.STREAM_DIRECTION_BOTH) - - stream.set_property('transmitter', 'libjingle') - - stream.connect('error', stream_error) - stream.connect('new-active-candidate-pair', new_active_candidate_pair) - stream.connect('codec-changed', codec_changed) - stream.connect('native-candidates-prepared', native_candidates_prepared) - stream.connect('state-changed', state_changed) - stream.connect('new-native-candidate', new_native_candidate) - - possible_codecs = stream.get_local_codecs() - - #for codec in possible_codecs: - # print "codec: %d: %s/%d found" % (codec['id'], codec['encoding_name'], codec['clock_rate']) - - send_codecs(possible_codecs) - - stream.prepare_transports() - - return stream - - -main() diff --git a/src/common/jingle.popen.py b/src/common/jingle.popen.py deleted file mode 100644 index e991f6b5c..000000000 --- a/src/common/jingle.popen.py +++ /dev/null @@ -1,666 +0,0 @@ -## -## 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 the jingle signalling protocol. ''' - -import gajim -import xmpp -import gobject - -import sys, dl, gst, gobject -sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) -import farsight - - -import meta - -class JingleStates(object): - ''' States in which jingle session may exist. ''' - ended=0 - pending=1 - active=2 - -class Exception(object): pass -class WrongState(Exception): pass -class NoCommonCodec(Exception): pass - -class JingleSession(object): - ''' This represents one jingle session. ''' - __metaclass__=meta.VerboseClassType - 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 - - # 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.__defaultCB], - 'content-modify': [self.__defaultCB], - 'content-remove': [self.__defaultCB], - 'session-accept': [self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], - 'session-info': [self.__defaultCB], - 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, self.__defaultCB], - 'session-terminate': [self.__defaultCB], - 'transport-info': [self.__broadcastCB, self.__defaultCB], - 'iq-result': [], - 'iq-error': [], - } - - ''' Middle-level functions to manage contents. Handle local content - cache and send change notifications. ''' - def addContent(self, name, content, creator='we'): - ''' Add new content to session. If the session is active, - this will send proper stanza to update session. - The protocol prohibits changing that when pending. - Creator must be one of ('we', 'peer', 'initiator', 'responder')''' - if self.state==JingleStates.pending: - raise WrongState - - 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 self.state==JingleStates.active: - pass # TODO: send proper stanza, shouldn't be needed now - - def removeContent(self, creator, name): - ''' We do not need this now ''' - pass - - def modifyContent(self, creator, name, *someother): - ''' We do not need this now ''' - pass - - ''' Middle-level function to do stanza exchange. ''' - def startSession(self): - ''' Start session. ''' - self.__sessionInitiate() - - def sendSessionInfo(self): pass - - ''' 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') - 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 - - 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 __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'] - - def __sessionInitiateCB(self, stanza, jingle, error, action): - ''' We got a jingle session request from other entity, - therefore we are the receiver... Unpack the data. ''' - self.initiator = jingle['initiator'] - self.responder = self.ourjid - self.peerjid = self.initiator - - fail = True - for element in jingle.iterTags('content'): - # checking what kind of session this will be - desc_ns = element.getTag('description').getNamespace() - tran_ns = element.getTag('transport').getNamespace() - if desc_ns==xmpp.NS_JINGLE_AUDIO and tran_ns==xmpp.NS_JINGLE_ICE_UDP: - # we've got voip content - self.addContent(element['name'], JingleVoiP(self), 'peer') - fail = False - - if fail: - # TODO: we should send inside too - self.connection.connection.send( - xmpp.Error(stanza, xmpp.NS_STANZAS + 'feature-not-implemented')) - self.connection.deleteJingle(self) - raise xmpp.NodeProcessed - - self.state = JingleStates.pending - - 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 on_p2psession_error(self, *anything): - print self.weinitiate, "Farsight session error!" - - ''' Methods that make/send proper pieces of XML. They check if the session - is in appropriate state. ''' - def __makeJingle(self, action): - stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) - jingle = stanza.addChild('jingle', attrs={ - 'xmlns': 'http://www.xmpp.org/extensions/xep-0166.html#ns', - 'action': action, - 'initiator': self.initiator, - 'responder': self.responder, - 'sid': self.sid}) - return stanza, jingle - - def __appendContent(self, jingle, content, full=True): - ''' Append element to element, - with (full=True) or without (full=False) - children. ''' - if full: - jingle.addChild(node=content.toXML()) - else: - jingle.addChild('content', - attrs={'name': content.name, 'creator': content.creator}) - - def __appendContents(self, jingle, full=True): - ''' Append all elements to .''' - # TODO: integrate with __appendContent? - # TODO: parameters 'name', 'content'? - for content in self.contents.values(): - self.__appendContent(jingle, content, full=full) - - def __sessionInitiate(self): - assert self.state==JingleStates.ended - stanza, jingle = self.__makeJingle('session-initiate') - self.__appendContents(jingle) - self.connection.connection.send(stanza) - - def __sessionAccept(self): - assert self.state==JingleStates.pending - stanza, jingle = self.__jingle('session-accept') - self.__appendContents(jingle, False) - self.connection.connection.send(stanza) - self.state=JingleStates.active - - def __sessionInfo(self, payload=None): - assert self.state!=JingleStates.ended - stanza, jingle = self.__jingle('session-info') - if payload: - jingle.addChild(node=payload) - self.connection.connection.send(stanza) - - def __sessionTerminate(self): - assert self.state!=JingleStates.ended - stanza, jingle = self.__jingle('session-terminate') - self.connection.connection.send(stanza) - - def __contentAdd(self): - assert self.state==JingleStates.active - - def __contentAccept(self): - assert self.state!=JingleStates.ended - - def __contentModify(self): - assert self.state!=JingleStates.ended - - def __contentRemove(self): - assert self.state!=JingleStates.ended - - def sendTransportInfo(self, content): - assert self.state!=JingleStates.ended - stanza, jingle = self.__makeJingle('transport-info') - jingle.addChild(node=content) - self.connection.connection.send(stanza) - - '''Callbacks''' - def sessionTerminateCB(self, stanza): pass - -class Codec(object): - ''' This class keeps description of a single codec. ''' - def __init__(self, name, id=None, **params): - ''' Create new codec description. ''' - self.name = name - self.id = id - self.attrs = {'name': self.name, 'id': self.id, 'channels': 1} - for key in ('channels', 'clockrate', 'maxptime', 'ptime'): - if key in params: - self.attrs[key]=params[key] - del params[key] - self.params = params - - def __eq__(a, b): - ''' Compare two codec descriptions. ''' - # TODO: check out what should be tested... - if a.name!=b.name: return False - # ... - return True - - def toXML(self): - return xmpp.Node('payload-type', - attrs=self.attrs, - payload=(xmpp.Node('parameter', {'name': k, 'value': v}) for k,v in self.params)) - -class JingleAudioSession(object): -# __metaclass__=meta.VerboseClassType - def __init__(self, content, fromNode): - self.content = content - - self.initiator_codecs=[] - self.responder_codecs=[] - - if fromNode: - # read all codecs peer understand - for payload in fromNode.iterTags('payload-type'): - attrs = fromNode.getAttrs().copy() - for param in fromNode.iterTags('parameter'): - attrs[param['name']]=param['value'] - self.initiator_codecs.append(Codec(**attrs)) - - def sessionInitiateCB(self, stanza, ourcontent): - pass - - ''' "Negotiation" of codecs... simply presenting what *we* can do, nothing more... ''' - def getOurCodecs(self, other=None): - ''' Get a list of codecs we support. Try to get them in the same - order as the codecs of our peer. If other!=None, raise - a NoCommonCodec error if no codecs both sides support (None means - we are initiating the connection and we don't know the other - peer's codecs.) ''' - # for now we "understand" only one codec -- speex with clockrate 16000 - # so we have an easy job to do... (codecs sorted in order of preference) - supported_codecs=[ - Codec('speex', clockrate='16000'), - ] - - other_l = other if other is not None else [] - our_l = supported_codecs[:] - out = [] - ids = range(128) - for codec in other_l: - if codec in our_l: - out.append(codec) - our_l.remove(codec) - try: ids.remove(codec.id) - except ValueError: pass # when id is not a dynamic one - - if other is not None and len(out)==0: - raise NoCommonCodec - - for codec in our_l: - if not codec.id or codec.id not in ids: - codec.id = ids.pop() - out.append(codec) - - return out - - ''' Methods for making proper pieces of XML. ''' - def __codecsList(self, codecs): - ''' Prepares a description element with codecs given as a parameter. ''' - return xmpp.Node(xmpp.NS_JINGLE_AUDIO+' description', - payload=(codec.toXML() for codec in codecs)) - - def toXML(self): - if not self.initiator_codecs: - # we are the initiator, so just send our codecs - self.initiator_codecs = self.getOurCodecs() - return self.__codecsList(self.initiator_codecs) - else: - # we are the responder, we SHOULD adjust our codec list - self.responder_codecs = self.getOurCodecs(self.initiator_codecs) - return self.__codecsList(self.responder_codecs) - -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 - -class JingleVoiP(JingleContent): - ''' Jingle VoiP sessions consist of audio content transported - over an ICE UDP protocol. ''' -# __metaclass__=meta.VerboseClassType - def __init__(self, session, node=None): - JingleContent.__init__(self, session, node) - self.got_codecs = False - self.codecs = [] - - #if node is None: - # self.audio = JingleAudioSession(self) - #else: - # self.audio = JingleAudioSession(self, node.getTag('content')) - #self.transport = JingleICEUDPSession(self) - self.setupStream() - - def stanzaCB(self, stanza, content, error, action): - ''' Called when something related to our content was sent by peer. ''' - callbacks = { - 'content-accept': [self.__getRemoteCodecsCB], - 'content-add': [], - 'content-modify': [], - 'content-remove': [], - 'session-accept': [self.__getRemoteCodecsCB], - 'session-info': [], - 'session-initiate': [self.__getRemoteCodecsCB], - 'session-terminate': [], - 'transport-info': [self.__transportInfoCB], - 'iq-result': [], - 'iq-error': [], - }[action] - for callback in callbacks: - callback(stanza, content, error, action) - - def __getRemoteCodecsCB(self, stanza, content, error, action): - if self.got_codecs: return - - codecs = [] - for codec in content.getTag('description').iterTags('payload-type'): - c = {'id': int(codec['id']), - 'encoding_name': codec['name'], - 'media_type': farsight.MEDIA_TYPE_AUDIO, - 'channels': 1, - 'params': dict((p['name'], p['value']) for p in codec.iterTags('parameter'))} - if 'channels' in codec: c['channels']=codec['channels'] - codecs.append(c) - self.p2pin.write('%d %d %s %d %d %d\n' % (1, farsight.MEDIA_TYPE_AUDIO, - codec['encoding_name'], codec['id'], codec['clock_rate'], codec['channels'])) - if len(codecs)==0: return - - print self.session.weinitiate, "#farsight_stream_set_remote_codecs" - #self.p2pstream.set_remote_codecs(codecs) - self.p2pin.write('%d %d %s %d %d %d\n' % (1, farsight.MEDIA_TYPE_AUDIO, - 'LAST', 0, 0, 0)) - self.got_codecs=True - - def __transportInfoCB(self, stanza, content, error, action): - ''' Got a new transport candidate. ''' - candidates = [] - for candidate in content.getTag('transport').iterTags('candidate'): - cand={ - # 'candidate_id': str(self.session.connection.connection.getAnID()), - 'candidate_id': candidate['cid'], - 'component': int(candidate['component']), - 'ip': candidate['ip'], - 'port': int(candidate['port']), - 'proto': candidate['protocol']=='udp' and farsight.NETWORK_PROTOCOL_UDP \ - or farsight.NETWORK_PROTOCOL_TCP, - 'proto_subtype':'RTP', - 'proto_profile':'AVP', - 'preference': float(candidate['priority'])/100000, - # 'type': farsight.CANDIDATE_TYPE_LOCAL, - 'type': int(candidate['type']), - } - if 'ufrag' in candidate: cand['username']=candidate['ufrag'] - if 'pwd' in candidate: cand['password']=candidate['pwd'] - - self.p2pin.write('%d %d %s %s %d %s %s\n' % (0, int(candidate['type']), candidate['cid'], candidate['ip'], int(candidate['port']), candidate['ufrag'], candidate['pwd'])) - candidates.append(cand) - print self.session.weinitiate, "#add_remote_candidate" - #self.p2pstream.add_remote_candidate(candidates) - - def toXML(self): - ''' Return proper XML for element. ''' - return xmpp.Node('content', - attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, - payload=[ - xmpp.Node(xmpp.NS_JINGLE_AUDIO+' description', payload=self.iterCodecs()), - xmpp.Node(xmpp.NS_JINGLE_ICE_UDP+' transport') - ]) - - def __content(self, payload=[]): - ''' Build a XML content-wrapper for our data. ''' - return xmpp.Node('content', - attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, - payload=payload) - - def new_data(self, *things): - buf = self.p2pout.readline() - print "RECV: %r"%buf - msg = buf.split(' ') - try: - type = int(msg.pop(0)) - except: - return True - #print msg - if type==0: - media_type, id, ip, port, username, password = msg[:6] - media_type=int(media_type) - port=int(port) - #print "Received %d %s %s %d %s %s" % (media_type, id, ip, port, username, password) - self.send_candidate({'candidate_id': id, 'component': 1, 'ip': ip, - 'port': port, 'username': username, 'password': password, - 'proto': farsight.NETWORK_PROTOCOL_UDP, 'proto_subtype': 'RTP', - 'proto_profile': 'AVP', 'preference': 1.0, 'type': 0}) - elif type==1: - media_type, encoding_name, pt, clock_rate, channels=msg[:5] - media_type=int(media_type) - pt=int(pt) - clock_rate=int(clock_rate) - channels=int(channels) - - #print "Received %d %s %d %d %d" % (media_type, encoding_name, pt, clock_rate, channels) - if encoding_name!='LAST': - self.codecs.append((media_type, encoding_name, pt, clock_rate, channels)) - else: - self.got_codecs=True - return True - - def setupStream(self): - import popen2 - self.p2pout, self.p2pin = popen2.popen2('/home/liori/Projekty/SoC/trunk/src/common/jingle-handler.py') - gobject.io_add_watch(self.p2pout, gobject.IO_IN, self.new_data) - while not self.got_codecs: - self.new_data() - return - print self.session.weinitiate, "#farsight_session_create_stream" - self.p2pstream = self.session.p2psession.create_stream( - farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) - self.p2pstream.set_property('transmitter', 'libjingle') - self.p2pstream.connect('error', self.on_p2pstream_error) - self.p2pstream.connect('new-active-candidate-pair', self.on_p2pstream_new_active_candidate_pair) - self.p2pstream.connect('codec-changed', self.on_p2pstream_codec_changed) - self.p2pstream.connect('native-candidates-prepared', self.on_p2pstream_native_candidates_prepared) - self.p2pstream.connect('state-changed', self.on_p2pstream_state_changed) - self.p2pstream.connect('new-native-candidate', self.on_p2pstream_new_native_candidate) - - self.p2pstream.set_remote_codecs(self.p2pstream.get_local_codecs()) - - print self.session.weinitiate, "#farsight_stream_prepare_transports" - self.p2pstream.prepare_transports() - - print self.session.weinitiate, "#farsight_stream_set_active_codec" - self.p2pstream.set_active_codec(8) #??? - - sink = gst.element_factory_make('alsasink') - sink.set_property('sync', False) - sink.set_property('latency-time', 20000) - sink.set_property('buffer-time', 80000) - - src = gst.element_factory_make('audiotestsrc') - src.set_property('blocksize', 320) - #src.set_property('latency-time', 20000) - src.set_property('is-live', True) - - print self.session.weinitiate, "#farsight_stream_set_sink" - self.p2pstream.set_sink(sink) - print self.session.weinitiate, "#farsight_stream_set_source" - self.p2pstream.set_source(src) - - def on_p2pstream_error(self, *whatever): pass - def on_p2pstream_new_active_candidate_pair(self, stream, native, remote): - print self.session.weinitiate, "##new_active_candidate_pair" - #print "New native candidate pair: %s, %s" % (native, remote) - def on_p2pstream_codec_changed(self, stream, codecid): - print self.session.weinitiate, "##codec_changed" - #print "Codec changed: %d" % codecid - def on_p2pstream_native_candidates_prepared(self, *whatever): - print self.session.weinitiate, "##native_candidates_prepared" - #print "Native candidates prepared: %r" % whatever - for candidate in self.p2pstream.get_native_candidate_list(): - self.send_candidate(candidate) - def on_p2pstream_state_changed(self, stream, state, dir): - print self.session.weinitiate, "##state_changed" - #print "State: %d, Dir: %d" % (state, dir) - if state==farsight.STREAM_STATE_CONNECTED: - print self.session.weinitiate, "#farsight_stream_signal_native_candidates_prepared" - stream.signal_native_candidates_prepared() - print self.session.weinitiate, "#farsight_stream_start" - stream.start() - def on_p2pstream_new_native_candidate(self, p2pstream, candidate_id): - print self.session.weinitiate, "##new_native_candidate" - print self.session.weinitiate, "#get_native_candidate" - candidates = p2pstream.get_native_candidate(candidate_id) - print self.session.weinitiate, "#!", repr(candidates) - - for candidate in candidates: - self.send_candidate(candidate) - def send_candidate(self, candidate): - attrs={ - 'cid': candidate['candidate_id'], - 'component': candidate['component'], - 'foundation': '1', # hack - 'generation': '0', - 'type': candidate['type'], - 'ip': candidate['ip'], - 'network': '0', - 'port': candidate['port'], - 'priority': int(100000*candidate['preference']), # hack - 'protocol': candidate['proto']==farsight.NETWORK_PROTOCOL_UDP and 'udp' or 'tcp', - } - if 'username' in candidate: attrs['ufrag']=candidate['username'] - if 'password' in candidate: attrs['pwd']=candidate['password'] - c=self.__content() - t=c.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') - t.addChild('candidate', attrs=attrs) - self.session.sendTransportInfo(c) - - def iterCodecs(self): - for codec in self.codecs: - media_type, encoding_name, pt, clock_rate, channels=codec - yield xmpp.Node('payload-type', { - 'name': encoding_name, - 'id': pt, - 'channels': channels, - 'clock_rate': clock_rate}) - -# print self.session.weinitiate, "#farsight_stream_get_local_codecs" -# codecs=self.p2pstream.get_local_codecs() -# for codec in codecs: -# a = {'name': codec['encoding_name'], -# 'id': codec['id'], -# 'channels': 1} -# if 'clock_rate' in codec: a['clockrate']=codec['clock_rate'] -# if 'optional_params' in codec: -# p = (xmpp.Node('parameter', {'name': name, 'value': value}) -# for name, value in codec['optional_params'].iteritems()) -# else: p = () -# yield xmpp.Node('payload-type', a, p) - -class ConnectionJingle(object): - ''' This object depends on that it is a part of Connection class. ''' - def __init__(self): - # dictionary: (jid, sessionid) => JingleSession object - self.__sessions = {} - - # dictionary: (jid, iq stanza id) => JingleSession object, - # one time callbacks - self.__iq_responses = {} - - def addJingle(self, jingle): - ''' Add a jingle session to a jingle stanza dispatcher - jingle - a JingleSession object. - ''' - self.__sessions[(jingle.peerjid, jingle.sid)]=jingle - - def deleteJingle(self, jingle): - ''' Remove a jingle session from a jingle stanza dispatcher ''' - del self.__session[(jingle.peerjid, jingle.sid)] - - def _JingleCB(self, con, stanza): - ''' The jingle stanza dispatcher. - Route jingle stanza to proper JingleSession object, - or create one if it is a new session. - TODO: Also check if the stanza isn't an error stanza, if so - route it adequatelly.''' - - # get data - jid = stanza.getFrom() - id = stanza.getID() - - if (jid, id) in self.__iq_responses.keys(): - self.__iq_responses[(jid, id)].stanzaCB(stanza) - del self.__iq_responses[(jid, id)] - raise xmpp.NodeProcessed - - jingle = stanza.getTag('jingle') - if not jingle: return - sid = jingle.getAttr('sid') - - # do we need to create a new jingle object - if (jid, sid) not in self.__sessions: - # TODO: we should check its type here... - newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) - self.addJingle(newjingle) - - # we already have such session in dispatcher... - self.__sessions[(jid, sid)].stanzaCB(stanza) - - raise xmpp.NodeProcessed - - def addJingleIqCallback(self, jid, id, jingle): - self.__iq_responses[(jid, id)]=jingle - - def startVoiP(self, jid): - jingle = JingleSession(self, weinitiate=True, jid=jid) - self.addJingle(jingle) - jingle.addContent('voice', JingleVoiP(jingle)) - jingle.startSession() From 0a9c7f67ec9d9346fd9187917cb21ba2feeb47e3 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Tue, 21 Aug 2007 18:18:48 +0000 Subject: [PATCH 21/67] Jingle: lots of debug prints cut --- src/common/jingle.py | 136 ++----------------------------------------- 1 file changed, 4 insertions(+), 132 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 8092e6f96..fbb641f9e 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -21,8 +21,6 @@ sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) import farsight sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_LOCAL) -import meta - class JingleStates(object): ''' States in which jingle session may exist. ''' ended=0 @@ -35,7 +33,6 @@ class NoCommonCodec(Exception): pass class JingleSession(object): ''' This represents one jingle session. ''' - __metaclass__=meta.VerboseClassType def __init__(self, con, weinitiate, jid, sid=None): ''' con -- connection object, weinitiate -- boolean, are we the initiator? @@ -75,9 +72,6 @@ class JingleSession(object): } # for making streams using farsight - import gc - gc.disable() - print self.weinitiate, "#farsight_session_factory_make" self.p2psession = farsight.farsight_session_factory_make('rtp') self.p2psession.connect('error', self.on_p2psession_error) @@ -190,8 +184,7 @@ class JingleSession(object): cn = self.contents[(creator, name)] cn.stanzaCB(stanza, content, error, action) - def on_p2psession_error(self, *anything): - print self.weinitiate, "Farsight session error!" + def on_p2psession_error(self, *anything): pass ''' Methods that make/send proper pieces of XML. They check if the session is in appropriate state. ''' @@ -268,103 +261,6 @@ class JingleSession(object): '''Callbacks''' def sessionTerminateCB(self, stanza): pass -class Codec(object): - ''' This class keeps description of a single codec. ''' - def __init__(self, name, id=None, **params): - ''' Create new codec description. ''' - self.name = name - self.id = id - self.attrs = {'name': self.name, 'id': self.id, 'channels': 1} - for key in ('channels', 'clockrate', 'maxptime', 'ptime'): - if key in params: - self.attrs[key]=params[key] - del params[key] - self.params = params - - def __eq__(a, b): - ''' Compare two codec descriptions. ''' - # TODO: check out what should be tested... - if a.name!=b.name: return False - # ... - return True - - def toXML(self): - return xmpp.Node('payload-type', - attrs=self.attrs, - payload=(xmpp.Node('parameter', {'name': k, 'value': v}) for k,v in self.params)) - -class JingleAudioSession(object): -# __metaclass__=meta.VerboseClassType - def __init__(self, content, fromNode): - self.content = content - - self.initiator_codecs=[] - self.responder_codecs=[] - - if fromNode: - # read all codecs peer understand - for payload in fromNode.iterTags('payload-type'): - attrs = fromNode.getAttrs().copy() - for param in fromNode.iterTags('parameter'): - attrs[param['name']]=param['value'] - self.initiator_codecs.append(Codec(**attrs)) - - def sessionInitiateCB(self, stanza, ourcontent): - pass - - ''' "Negotiation" of codecs... simply presenting what *we* can do, nothing more... ''' - def getOurCodecs(self, other=None): - ''' Get a list of codecs we support. Try to get them in the same - order as the codecs of our peer. If other!=None, raise - a NoCommonCodec error if no codecs both sides support (None means - we are initiating the connection and we don't know the other - peer's codecs.) ''' - # for now we "understand" only one codec -- speex with clockrate 16000 - # so we have an easy job to do... (codecs sorted in order of preference) - supported_codecs=[ - Codec('speex', clockrate='16000'), - ] - - if other is not None: - other_l = other - else: - other_l = [] - our_l = supported_codecs[:] - out = [] - ids = range(128) - for codec in other_l: - if codec in our_l: - out.append(codec) - our_l.remove(codec) - try: ids.remove(codec.id) - except ValueError: pass # when id is not a dynamic one - - if other is not None and len(out)==0: - raise NoCommonCodec - - for codec in our_l: - if not codec.id or codec.id not in ids: - codec.id = ids.pop() - out.append(codec) - - return out - - ''' Methods for making proper pieces of XML. ''' - def __codecsList(self, codecs): - ''' Prepares a description element with codecs given as a parameter. ''' - return xmpp.Node(xmpp.NS_JINGLE_AUDIO+' description', - payload=(codec.toXML() for codec in codecs)) - - def toXML(self): - if not self.initiator_codecs: - # we are the initiator, so just send our codecs - self.initiator_codecs = self.getOurCodecs() - return self.__codecsList(self.initiator_codecs) - else: - # we are the responder, we SHOULD adjust our codec list - self.responder_codecs = self.getOurCodecs(self.initiator_codecs) - return self.__codecsList(self.responder_codecs) - class JingleContent(object): ''' An abstraction of content in Jingle sessions. ''' def __init__(self, session, node=None): @@ -378,7 +274,6 @@ class JingleContent(object): class JingleVoiP(JingleContent): ''' Jingle VoiP sessions consist of audio content transported over an ICE UDP protocol. ''' -# __metaclass__=meta.VerboseClassType def __init__(self, session, node=None): JingleContent.__init__(self, session, node) self.got_codecs = False @@ -422,7 +317,6 @@ class JingleVoiP(JingleContent): codecs.append(c) if len(codecs)==0: return - print self.session.weinitiate, "#farsight_stream_set_remote_codecs" self.p2pstream.set_remote_codecs(codecs) self.got_codecs=True @@ -431,7 +325,7 @@ class JingleVoiP(JingleContent): candidates = [] for candidate in content.getTag('transport').iterTags('candidate'): cand={ - 'candidate_id': candidate['cid'], + 'candidate_id': self.session.connection.connection.getAnID(), 'component': int(candidate['component']), 'ip': candidate['ip'], 'port': int(candidate['port']), @@ -451,7 +345,6 @@ class JingleVoiP(JingleContent): cand['password']=candidate['pwd'] candidates.append(cand) - print self.session.weinitiate, "#add_remote_candidate" self.p2pstream.add_remote_candidate(candidates) def toXML(self): @@ -470,7 +363,6 @@ class JingleVoiP(JingleContent): payload=payload) def setupStream(self): - print self.session.weinitiate, "#farsight_session_create_stream" self.p2pstream = self.session.p2psession.create_stream( farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) self.p2pstream.set_property('transmitter', 'libjingle') @@ -483,10 +375,8 @@ class JingleVoiP(JingleContent): self.p2pstream.set_remote_codecs(self.p2pstream.get_local_codecs()) - print self.session.weinitiate, "#farsight_stream_prepare_transports" self.p2pstream.prepare_transports() - print self.session.weinitiate, "#farsight_stream_set_active_codec" self.p2pstream.set_active_codec(8) #??? sink = gst.element_factory_make('alsasink') @@ -499,46 +389,29 @@ class JingleVoiP(JingleContent): #src.set_property('latency-time', 20000) src.set_property('is-live', True) - print self.session.weinitiate, "#farsight_stream_set_sink" self.p2pstream.set_sink(sink) - print self.session.weinitiate, "#farsight_stream_set_source" self.p2pstream.set_source(src) def on_p2pstream_error(self, *whatever): pass - def on_p2pstream_new_active_candidate_pair(self, stream, native, remote): - print self.session.weinitiate, "##new_active_candidate_pair" - #print "New native candidate pair: %s, %s" % (native, remote) - def on_p2pstream_codec_changed(self, stream, codecid): - print self.session.weinitiate, "##codec_changed" - #print "Codec changed: %d" % codecid + def on_p2pstream_new_active_candidate_pair(self, stream, native, remote): pass + def on_p2pstream_codec_changed(self, stream, codecid): pass def on_p2pstream_native_candidates_prepared(self, *whatever): - print self.session.weinitiate, "##native_candidates_prepared" - #print "Native candidates prepared: %r" % whatever for candidate in self.p2pstream.get_native_candidate_list(): self.send_candidate(candidate) def on_p2pstream_state_changed(self, stream, state, dir): - print self.session.weinitiate, "##state_changed" - #print "State: %d, Dir: %d" % (state, dir) if state==farsight.STREAM_STATE_CONNECTED: - print self.session.weinitiate, "#farsight_stream_signal_native_candidates_prepared" stream.signal_native_candidates_prepared() - print self.session.weinitiate, "#farsight_stream_start" stream.start() def on_p2pstream_new_native_candidate(self, p2pstream, candidate_id): - print self.session.weinitiate, "##new_native_candidate" - print self.session.weinitiate, "#get_native_candidate" candidates = p2pstream.get_native_candidate(candidate_id) - print self.session.weinitiate, "#!", repr(candidates) for candidate in candidates: self.send_candidate(candidate) def send_candidate(self, candidate): attrs={ - 'cid': candidate['candidate_id'], 'component': candidate['component'], 'foundation': '1', # hack 'generation': '0', - 'type': candidate['type'], 'ip': candidate['ip'], 'network': '0', 'port': candidate['port'], @@ -557,7 +430,6 @@ class JingleVoiP(JingleContent): self.session.sendTransportInfo(c) def iterCodecs(self): - print self.session.weinitiate, "#farsight_stream_get_local_codecs" codecs=self.p2pstream.get_local_codecs() for codec in codecs: a = {'name': codec['encoding_name'], From c2c8efe2cc6ead9ce079691daf99c2a61a49a10d Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Thu, 23 Aug 2007 19:03:11 +0000 Subject: [PATCH 22/67] Jingle: farsight testing program --- src/common/farsight/test-jingle.py | 281 +++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100755 src/common/farsight/test-jingle.py diff --git a/src/common/farsight/test-jingle.py b/src/common/farsight/test-jingle.py new file mode 100755 index 000000000..b9ee494dc --- /dev/null +++ b/src/common/farsight/test-jingle.py @@ -0,0 +1,281 @@ +#!/usr/bin/python + +import sys, dl, gst, gobject +sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) +import farsight +from socket import * + +TYPE_CANDIDATE=0 +TYPE_CODEC=1 +TYPE_READY_BYTE=2 + +MODE_DUPLEX=0 +MODE_AUDIO=1 +MODE_VIDEO=2 + +send_sock = 0 +recv_sock = 0 +serv_addr = None +audio_stream = None +video_stream = None + +def stream_error(stream, error, debug): + print "stream_error: stream=%r error=%s" % (stream, error) + +def session_error(session, error, debug): + print "session_error: session=%r error=%s" % (session, error) + +def new_active_candidate_pair(stream, native, remote): + print "new_active_candidate_pair: native %s, remote %s" % (native, remote) + send_codecs(stream.get_local_codecs()) + +def codec_changed(stream, codec_id): + print "codec_changed: codec_id=%d, stream=%r" % (codec_id, stream) + +def native_candidates_prepared(stream): + print "native_candidates_prepared: stream=%r" % stream + for info in stream.get_native_candidate_list(): + print "Local transport candidate: %s %d %s %s %s %d pref %f" % ( + info['candidate_id'], info['component'], + (info['proto']==farsight.NETWORK_PROTOCOL_TCP) and "TCP" or "UDP", + info['proto_subtype'], info['ip'], info['port'], info['preference']); + +def show_element(e): + print 'element: ...' + +def state_changed(stream, state, dir): + if state==farsight.STREAM_STATE_DISCONNECTED: + print "state_changed: disconnected" + elif state==farsight.STREAM_STATE_CONNECTING: + print "state_changed: connecting" + elif state==farsight.STREAM_STATE_CONNECTED: + print "state_changed: connectied" + print "WW: stream.signal_native_candidates_prepared()" + print "WW: stream.start()" + exit() + stream.signal_native_candidates_prepared() + stream.start() + +def setup_send(host, port): + global send_sock, serv_addr + send_sock = socket(AF_INET, SOCK_DGRAM) + rx_addr = gethostbyname(host) + serv_addr=(rx_addr, port) + +def setup_recv(port): + global recv_sock + sock = socket(AF_INET, SOCK_DGRAM); + sock.bind(('0.0.0.0', port)) + print "setup_recv: %r(%d)" % (sock, sock.fileno()) + recv_sock = sock + return sock + +def send_codecs(codecs): + global send_sock, serv_addr + for codec in codecs: + print "Sending codec" + print codec + if 'channels' not in codec: codec['channels']=1 + s="%d %d %s %d %d %d " % (TYPE_CODEC, codec['media_type'], + codec['encoding_name'], codec['id'], codec['clock_rate'], + codec['channels']) + send_sock.sendto(s, serv_addr) + s="%d %d %s %d %d %d " % (TYPE_CODEC, codec['media_type'], 'LAST', 0, 0, 0) + print "Sending end of list packet" + send_sock.sendto(s, serv_addr) + +def send_ready_byte(): + global send_sock, serv_addr + send_sock.sendto('%d ' % TYPE_READY_BYTE, serv_addr) + print "Sending ready byte" + +def do_handshake(sockfd): + global send_sock + send_ready_byte() + print "Waiting for ready byte" + buf, recv_addr=recv_sock.recvfrom(1500) + print "got message!" + if int(buf.split(' ')[0]) == TYPE_READY_BYTE: + send_ready_byte(); + +def send_candidate(type, trans): + global send_sock, serv_addr + s="%d %d %s %s %d %s %s" % (TYPE_CANDIDATE, + type, trans['candidate_id'], trans['ip'], trans['port'], + trans['username'], trans['password']) + print "Sending..." + send_sock.sendto(s, serv_addr) + +def new_native_candidate(stream, candidate_id): + candidate=stream.get_native_candidate(candidate_id) + trans=candidate[0] + + print "New native candidate: " % ( + trans['candidate_id'], trans['component'], trans['ip'], trans['port'], trans['proto'], + trans['proto_subtype'], trans['proto_profile'], trans['preference'], trans['type'], + trans['username'], trans['password']) + + type=stream.get_property("media-type") + + send_candidate(type, trans) + +remote_codecs_audio = [] +remote_codecs_video = [] +def add_remote_codec(stream, pt, encoding_name, media_type, clock_rate, channels): + global remote_codecs_audio, remote_codecs_video + if encoding_name=="LAST": + if media_type==farsight.MEDIA_TYPE_AUDIO: + print "WW: set_remote_codecs(remote_codecs_audio), %r" % (remote_codecs_audio,) + stream.set_remote_codecs(remote_codecs_audio) + else: + stream.set_remote_codecs(remote_codecs_video) + else: + codec={'id': pt, 'encoding_name': encoding_name, 'media_type': media_type, + 'clock_rate': clock_rate, 'channels': channels} + if media_type==farsight.MEDIA_TYPE_AUDIO: + remote_codecs_audio.append(codec) + elif media_type==farsight.MEDIA_TYPE_VIDEO: + remote_codecs_video.append(codec) + + for codec in remote_codecs_audio: + print "added audio codec: %d: %s/%d found" % (codec['id'], codec['encoding_name'], codec['clock_rate']) + for codec in remote_codecs_video: + print "added video codec: %d: %s/%d found" % (codec['id'], codec['encoding_name'], codec['clock_rate']) + +def add_remote_candidate(stream, id, ip, port, username, password): + if not stream: return + + trans={'candidate_id': id, 'component': 1, 'ip': ip, 'port': port, 'proto': farsight.NETWORK_PROTOCOL_UDP, + 'proto_subtype': 'RTP', 'proto_profile': 'AVP', 'preference': 1.0, + 'type': farsight.CANDIDATE_TYPE_LOCAL, 'username': username, 'password': password} + + print "WW: add_remote_candidate(%r)" % ([trans],) + stream.add_remote_candidate([trans]); + +def receive_loop(ch, cond, *data): + print 'receive_loop called!' + global recv_sock, serv_addr, audio_stream, remote_stream + print "waiting for msg from %r" % (serv_addr,) + buf,a = recv_sock.recvfrom(1500) + print "got message! ",buf + msg = buf.split(' ') + type = int(msg.pop(0)) + print msg + if type==TYPE_CANDIDATE: + media_type, id, ip, port, username, password = msg[:6] + media_type=int(media_type) + port=int(port) + print "Received %d %s %s %d %s %s" % (media_type, id, ip, port, username, password) + if media_type==farsight.MEDIA_TYPE_AUDIO: + add_remote_candidate(audio_stream, id, ip, port, username, password) + elif media_type==farsight.MEDIA_TYPE_VIDEO: + add_remote_candidate(video_stream, id, ip, port, username, password) + elif type==TYPE_CODEC: + media_type, encoding_name, pt, clock_rate, channels=msg[:5] + media_type=int(media_type) + pt=int(pt) + clock_rate=int(clock_rate) + channels=int(channels) + + print "Received %d %s %d %d %d" % (media_type, encoding_name, pt, clock_rate, channels) + + if media_type==farsight.MEDIA_TYPE_AUDIO: + add_remote_codec(audio_stream, pt, encoding_name, media_type, clock_rate, channels) + elif media_type==farsight.MEDIA_TYPE_VIDEO: + add_remote_codec(video_stream, pt, encoding_name, media_type, clock_rate, channels) + elif type==TYPE_READY_BYTE: + print "got ready byte" + return True + +def set_xoverlay(bus, message, xid): + print "set_xoverlay: called" + if message!=gst.MESSAGE_ELEMENT: return gst.BUS_PASS + + if not message.structure.has_name('prepare-xwindow-id'): return gst.BUS_PASS + + print "set_xoverlay: setting x overlay window id" + message.set_xwindow_id(xid) + + return gst.BUS_DROP + +def main(): + global audio_stream, remote_stream + if len(sys.argv)<4 or len(sys.argv)>6: + print 'usage: %s remoteip remoteport localport [mode] [xid]' % sys.argv[0] + return -1 + + if len(sys.argv)>=5: + mode = int(sys.argv[4]) + else: + mode = MODE_DUPLEX + + setup_send(sys.argv[1], int(sys.argv[2])) + print "Sending to %s %d listening on port %d" % (sys.argv[1], int(sys.argv[2]), int(sys.argv[3])) + + recv_chan=setup_recv(int(sys.argv[3])) + + do_handshake(recv_chan) + + gobject.io_add_watch(recv_chan.fileno(), gobject.IO_IN, receive_loop) + + session = setup_rtp_session() + + if mode==MODE_AUDIO or mode==MODE_VIDEO: + audio_stream = setup_rtp_stream(session, farsight.MEDIA_TYPE_AUDIO) + if mode==MODE_VIDEO or mode==MODE_DUPLEX: + video_stream = setup_rtp_stream(session, farsight.MEDIA_TYPE_VIDEO) + + if audio_stream: + audio_stream.set_active_codec(8) + + alsasrc = gst.element_factory_make('audiotestsrc', 'src') + alsasink= gst.element_factory_make('alsasink', 'alsasink') + + alsasink.set_property('sync', False) + alsasink.set_property('latency-time', 20000) + alsasink.set_property('buffer-time', 80000) + alsasrc.set_property('blocksize', 320) + #alsasrc.set_property('latency-time', 20000) + alsasrc.set_property('is-live', True) + + print "WW: set_sink(%r)" % (alsasink,) + audio_stream.set_sink(alsasink) + print "WW: set_source(%r)" % (alsasrc,) + audio_stream.set_source(alsasrc) + + #if video_stream: assert False + + gobject.MainLoop().run() + print 'bu!' + +def setup_rtp_session(): + session = farsight.farsight_session_factory_make('rtp') + + session.connect('error', session_error) + return session + +def setup_rtp_stream(session, type): + stream = session.create_stream(type, farsight.STREAM_DIRECTION_BOTH) + + stream.set_property('transmitter', 'libjingle') + + stream.connect('error', stream_error) + stream.connect('new-active-candidate-pair', new_active_candidate_pair) + stream.connect('codec-changed', codec_changed) + stream.connect('native-candidates-prepared', native_candidates_prepared) + stream.connect('state-changed', state_changed) + stream.connect('new-native-candidate', new_native_candidate) + + possible_codecs = stream.get_local_codecs() + + for codec in possible_codecs: + print "codec: %d: %s/%d found" % (codec['id'], codec['encoding_name'], codec['clock_rate']) + + #send_codecs(possible_codecs) + + stream.prepare_transports() + + return stream + + +main() From 2a7f1a654ae50652873f8488b1bfe4b524bd7f4e Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Thu, 23 Aug 2007 23:42:31 +0000 Subject: [PATCH 23/67] Jingle: dialog for accepting voice calls --- data/glade/voip_call_received_dialog.glade | 39 ++++ src/common/farsight/test-jingle.py | 1 - src/common/jingle.py | 234 ++++++++++++++++----- src/dialogs.py | 34 +++ src/gajim.py | 36 ++++ 5 files changed, 290 insertions(+), 54 deletions(-) create mode 100644 data/glade/voip_call_received_dialog.glade diff --git a/data/glade/voip_call_received_dialog.glade b/data/glade/voip_call_received_dialog.glade new file mode 100644 index 000000000..6b869f0f6 --- /dev/null +++ b/data/glade/voip_call_received_dialog.glade @@ -0,0 +1,39 @@ + + + + + + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + False + GTK_WIN_POS_CENTER_ON_PARENT + GDK_WINDOW_TYPE_HINT_DIALOG + True + False + GTK_MESSAGE_QUESTION + GTK_BUTTONS_YES_NO + <b><big>Incoming call</big></b> + True + %(contact)s wants to start a voice chat with you. Do you want to answer the call? + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 2 + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + GTK_BUTTONBOX_END + + + False + GTK_PACK_END + + + + + + diff --git a/src/common/farsight/test-jingle.py b/src/common/farsight/test-jingle.py index b9ee494dc..f45fed218 100755 --- a/src/common/farsight/test-jingle.py +++ b/src/common/farsight/test-jingle.py @@ -52,7 +52,6 @@ def state_changed(stream, state, dir): print "state_changed: connectied" print "WW: stream.signal_native_candidates_prepared()" print "WW: stream.start()" - exit() stream.signal_native_candidates_prepared() stream.start() diff --git a/src/common/jingle.py b/src/common/jingle.py index fbb641f9e..f9de84f64 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -12,24 +12,35 @@ ## ''' Handles the jingle signalling protocol. ''' +# note: if there will be more types of sessions (possibly file transfer, +# video...), split this file + import gajim +import gobject import xmpp -# ugly hack +# ugly hack, fixed in farsight 0.1.24 import sys, dl, gst sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) import farsight sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_LOCAL) +def timeout_add_and_call(timeout, callable, *args, **kwargs): + ''' Call a callback once. If it returns True, add a timeout handler to call it more times. + Helper function. ''' + if callable(*args, **kwargs): + return gobject.timeout_add(timeout, callable, *args, **kwargs) + return -1 # gobject.source_remove will not object + class JingleStates(object): ''' States in which jingle session may exist. ''' ended=0 pending=1 active=2 -class Exception(object): pass -class WrongState(Exception): pass -class NoCommonCodec(Exception): pass +class Error(Exception): pass +class WrongState(Error): pass +class NoSuchSession(Error): pass class JingleSession(object): ''' This represents one jingle session. ''' @@ -54,6 +65,8 @@ class JingleSession(object): 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 @@ -75,6 +88,17 @@ class JingleSession(object): self.p2psession = farsight.farsight_session_factory_make('rtp') self.p2psession.connect('error', self.on_p2psession_error) + ''' Interaction with user ''' + def approveSession(self): + ''' Called when user accepts session in UI (when we aren't the initiator).''' + self.accepted=True + self.acceptSession() + + def declineSession(self): + ''' Called when user declines session in UI (when we aren't the initiator, + or when the user wants to stop session completly. ''' + self.__sessionTerminate() + ''' Middle-level functions to manage contents. Handle local content cache and send change notifications. ''' def addContent(self, name, content, creator='we'): @@ -82,6 +106,8 @@ class JingleSession(object): this will send proper stanza to update session. The protocol prohibits changing that when pending. Creator must be one of ('we', 'peer', 'initiator', 'responder')''' + assert creator in ('we', 'peer', 'initiator', 'responder') + if self.state==JingleStates.pending: raise WrongState @@ -104,6 +130,13 @@ class JingleSession(object): ''' We do not need this now ''' pass + def acceptSession(self): + ''' Check if all contents and user agreed to start session. ''' + if not self.weinitiate and \ + self.accepted and \ + all(c.negotiated for c in self.contents.itervalues()): + self.__sessionAccept() + else: ''' Middle-level function to do stanza exchange. ''' def startSession(self): ''' Start session. ''' @@ -111,7 +144,7 @@ class JingleSession(object): def sendSessionInfo(self): pass - ''' Callbacks. ''' + ''' Session callbacks. ''' def stanzaCB(self, stanza): ''' A callback for ConnectionJingle. It gets stanza, then tries to send it to all internally registered callbacks. @@ -152,12 +185,21 @@ class JingleSession(object): def __sessionInitiateCB(self, stanza, jingle, error, action): ''' We got a jingle session request from other entity, - therefore we are the receiver... Unpack the data. ''' + therefore we are the receiver... Unpack the data, + inform the user. ''' 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 fail = True + contents = [] for element in jingle.iterTags('content'): # checking what kind of session this will be desc_ns = element.getTag('description').getNamespace() @@ -165,10 +207,13 @@ class JingleSession(object): if desc_ns==xmpp.NS_JINGLE_AUDIO and tran_ns==xmpp.NS_JINGLE_ICE_UDP: # we've got voip content self.addContent(element['name'], JingleVoiP(self), 'peer') + contents.append(('VOIP',)) fail = False + # If there's no content we understand... if fail: # TODO: we should send inside too + # TODO: delete this instance self.connection.connection.send( xmpp.Error(stanza, xmpp.NS_STANZAS + 'feature-not-implemented')) self.connection.deleteJingle(self) @@ -176,6 +221,9 @@ class JingleSession(object): self.state = JingleStates.pending + # Send event about starting a session + self.connection.dispatch('JINGLE_INCOMING', (self.initiator, self.sid, contents)) + def __broadcastCB(self, stanza, jingle, error, action): ''' Broadcast the stanza contents to proper content handlers. ''' for content in jingle.iterTags('content'): @@ -198,47 +246,47 @@ class JingleSession(object): 'sid': self.sid}) return stanza, jingle - def __appendContent(self, jingle, content, full=True): + def __appendContent(self, jingle, content): ''' Append element to element, with (full=True) or without (full=False) children. ''' - if full: - jingle.addChild(node=content.toXML()) - else: - jingle.addChild('content', - attrs={'name': content.name, 'creator': content.creator}) + jingle.addChild('content', + attrs={'name': content.name, 'creator': content.creator}) - def __appendContents(self, jingle, full=True): + def __appendContents(self, jingle): ''' Append all elements to .''' # TODO: integrate with __appendContent? # TODO: parameters 'name', 'content'? for content in self.contents.values(): - self.__appendContent(jingle, content, full=full) + self.__appendContent(jingle, content) def __sessionInitiate(self): assert self.state==JingleStates.ended stanza, jingle = self.__makeJingle('session-initiate') self.__appendContents(jingle) + self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent') self.connection.connection.send(stanza) def __sessionAccept(self): assert self.state==JingleStates.pending - stanza, jingle = self.__jingle('session-accept') - self.__appendContents(jingle, False) + stanza, jingle = self.__makeJingle('session-accept') + self.__appendContents(jingle) + self.__broadcastCB(stanza, jingle, None, 'session-accept-sent') self.connection.connection.send(stanza) self.state=JingleStates.active def __sessionInfo(self, payload=None): assert self.state!=JingleStates.ended - stanza, jingle = self.__jingle('session-info') + stanza, jingle = self.__makeJingle('session-info') if payload: jingle.addChild(node=payload) self.connection.connection.send(stanza) def __sessionTerminate(self): assert self.state!=JingleStates.ended - stanza, jingle = self.__jingle('session-terminate') + stanza, jingle = self.__makeJingle('session-terminate') self.connection.connection.send(stanza) + self.__broadcastCB(stanza, jingle, None, 'session-terminate-sent') def __contentAdd(self): assert self.state==JingleStates.active @@ -252,6 +300,12 @@ class JingleSession(object): def __contentRemove(self): assert self.state!=JingleStates.ended + def sendContentAccept(self, content): + assert self.state!=JingleStates.ended + stanza, jingle = self.__makeJingle('content-accept') + jingle.addChild(node=content) + self.connection.connection.send(stanza) + def sendTransportInfo(self, content): assert self.state!=JingleStates.ended stanza, jingle = self.__makeJingle('transport-info') @@ -270,6 +324,7 @@ class JingleContent(object): # (a JingleContent not added to session shouldn't send anything) #self.creator = None #self.name = None + self.negotiated = False # is this content already negotiated? class JingleVoiP(JingleContent): ''' Jingle VoiP sessions consist of audio content transported @@ -288,22 +343,34 @@ class JingleVoiP(JingleContent): def stanzaCB(self, stanza, content, error, action): ''' Called when something related to our content was sent by peer. ''' callbacks = { + # these are called when *we* get stanzas 'content-accept': [self.__getRemoteCodecsCB], 'content-add': [], 'content-modify': [], 'content-remove': [], - 'session-accept': [self.__getRemoteCodecsCB], + 'session-accept': [self.__getRemoteCodecsCB, self.__startMic], 'session-info': [], 'session-initiate': [self.__getRemoteCodecsCB], - 'session-terminate': [], + 'session-terminate': [self.__stop], 'transport-info': [self.__transportInfoCB], 'iq-result': [], 'iq-error': [], + # these are called when *we* sent these stanzas + 'session-initiate-sent': [self.__sessionInitiateSentCB], + 'session-accept-sent': [self.__startMic], + 'session-terminate-sent': [self.__stop], }[action] for callback in callbacks: callback(stanza, content, error, action) + def __sessionInitiateSentCB(self, stanza, content, error, action): + ''' Add our things to session-initiate stanza. ''' + content.setAttr('profile', 'RTP/AVP') + content.addChild(xmpp.NS_JINGLE_AUDIO+' description', payload=self.iterCodecs()) + content.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') + def __getRemoteCodecsCB(self, stanza, content, error, action): + ''' Get peer codecs from what we get from peer. ''' if self.got_codecs: return codecs = [] @@ -362,51 +429,29 @@ class JingleVoiP(JingleContent): attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, payload=payload) - def setupStream(self): - self.p2pstream = self.session.p2psession.create_stream( - farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) - self.p2pstream.set_property('transmitter', 'libjingle') - self.p2pstream.connect('error', self.on_p2pstream_error) - self.p2pstream.connect('new-active-candidate-pair', self.on_p2pstream_new_active_candidate_pair) - self.p2pstream.connect('codec-changed', self.on_p2pstream_codec_changed) - self.p2pstream.connect('native-candidates-prepared', self.on_p2pstream_native_candidates_prepared) - self.p2pstream.connect('state-changed', self.on_p2pstream_state_changed) - self.p2pstream.connect('new-native-candidate', self.on_p2pstream_new_native_candidate) - - self.p2pstream.set_remote_codecs(self.p2pstream.get_local_codecs()) - - self.p2pstream.prepare_transports() - - self.p2pstream.set_active_codec(8) #??? - - sink = gst.element_factory_make('alsasink') - sink.set_property('sync', False) - sink.set_property('latency-time', 20000) - sink.set_property('buffer-time', 80000) - - src = gst.element_factory_make('audiotestsrc') - src.set_property('blocksize', 320) - #src.set_property('latency-time', 20000) - src.set_property('is-live', True) - - self.p2pstream.set_sink(sink) - self.p2pstream.set_source(src) - def on_p2pstream_error(self, *whatever): pass def on_p2pstream_new_active_candidate_pair(self, stream, native, remote): pass def on_p2pstream_codec_changed(self, stream, codecid): pass def on_p2pstream_native_candidates_prepared(self, *whatever): - for candidate in self.p2pstream.get_native_candidate_list(): - self.send_candidate(candidate) + pass + def on_p2pstream_state_changed(self, stream, state, dir): if state==farsight.STREAM_STATE_CONNECTED: stream.signal_native_candidates_prepared() stream.start() + self.pipeline.set_state(gst.STATE_PLAYING) + + self.negotiated = True + if not self.session.weinitiate: + self.session.sendContentAccept(self.__content((xmpp.Node('description', payload=self.iterCodecs()),))) + self.session.acceptSession() + def on_p2pstream_new_native_candidate(self, p2pstream, candidate_id): candidates = p2pstream.get_native_candidate(candidate_id) for candidate in candidates: self.send_candidate(candidate) + def send_candidate(self, candidate): attrs={ 'component': candidate['component'], @@ -442,6 +487,85 @@ class JingleVoiP(JingleContent): else: p = () yield xmpp.Node('payload-type', a, p) + ''' Things to control the gstreamer's pipeline ''' + def setupStream(self): + # the pipeline + self.pipeline = gst.Pipeline() + + # the network part + self.p2pstream = self.session.p2psession.create_stream( + farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) + self.p2pstream.set_pipeline(self.pipeline) + self.p2pstream.set_property('transmitter', 'libjingle') + self.p2pstream.connect('error', self.on_p2pstream_error) + self.p2pstream.connect('new-active-candidate-pair', self.on_p2pstream_new_active_candidate_pair) + self.p2pstream.connect('codec-changed', self.on_p2pstream_codec_changed) + self.p2pstream.connect('native-candidates-prepared', self.on_p2pstream_native_candidates_prepared) + self.p2pstream.connect('state-changed', self.on_p2pstream_state_changed) + self.p2pstream.connect('new-native-candidate', self.on_p2pstream_new_native_candidate) + + self.p2pstream.set_remote_codecs(self.p2pstream.get_local_codecs()) + + self.p2pstream.prepare_transports() + + self.p2pstream.set_active_codec(8) #??? + + # the local parts + # TODO: use gconfaudiosink? + sink = gst.element_factory_make('alsasink') + sink.set_property('sync', False) + sink.set_property('latency-time', 20000) + sink.set_property('buffer-time', 80000) + self.pipeline.add(sink) + + self.src_signal = gst.element_factory_make('audiotestsrc') + self.src_signal.set_property('blocksize', 320) + self.src_signal.set_property('freq', 440) + self.pipeline.add(self.src_signal) + + # TODO: use gconfaudiosrc? + self.src_mic = gst.element_factory_make('alsasrc') + self.src_mic.set_property('blocksize', 320) + self.pipeline.add(self.src_mic) + + self.mic_volume = gst.element_factory_make('volume') + self.mic_volume.set_property('volume', 0) + self.pipeline.add(self.mic_volume) + + self.adder = gst.element_factory_make('adder') + self.pipeline.add(self.adder) + + # link gst elements + self.src_signal.link(self.adder) + self.src_mic.link(self.mic_volume) + self.mic_volume.link(self.adder) + + # this will actually start before the pipeline will be started. + # no worries, though; it's only a ringing sound + def signal(): + while True: + self.src_signal.set_property('volume', 0.5) + yield True # wait 750 ms + yield True # wait 750 ms + self.src_signal.set_property('volume', 0) + yield True # wait 750 ms + self.signal_cb_id = timeout_add_and_call(750, signal().__iter__().next) + + self.p2pstream.set_sink(sink) + self.p2pstream.set_source(self.adder) + + def __startMic(self, *things): + gobject.source_remove(self.signal_cb_id) + self.src_signal.set_property('volume', 0) + self.mic_volume.set_property('volume', 1) + + def __stop(self, *things): + self.pipeline.set_state(gst.STATE_NULL) + gobject.source_remove(self.signal_cb_id) + + def __del__(self): + self.__stop() + class ConnectionJingle(object): ''' This object depends on that it is a part of Connection class. ''' def __init__(self): @@ -484,7 +608,6 @@ class ConnectionJingle(object): # do we need to create a new jingle object if (jid, sid) not in self.__sessions: - # TODO: we should check its type here... newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) self.addJingle(newjingle) @@ -501,3 +624,8 @@ class ConnectionJingle(object): self.addJingle(jingle) jingle.addContent('voice', JingleVoiP(jingle)) jingle.startSession() + def getJingleSession(self, jid, sid): + try: + return self.__sessions[(jid, sid)] + except KeyError: + raise NoSuchSession diff --git a/src/dialogs.py b/src/dialogs.py index a6eb762c7..f21ef8d4a 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -3298,3 +3298,37 @@ class AdvancedNotificationsWindow: def on_close_window(self, widget): self.window.destroy() + +class VoIPCallReceivedDialog(object): + def __init__(self, account, contact_jid, sid): + self.account = account + self.jid = contact_jid + self.sid = sid + + xml = gtkgui_helpers.get_glade('voip_call_received_dialog.glade') + xml.signal_autoconnect(self) + + contact = gajim.contacts.get_first_contact_from_jid(account, contact_jid) + if contact and contact.name: + contact_text = '%s (%s)' % (contact.name, contact_jid) + else: + contact_text = contact_jid + + # do the substitution + dialog = xml.get_widget('voip_call_received_messagedialog') + dialog.set_property('secondary-text', + dialog.get_property('secondary-text') % {'contact': contact_text}) + + dialog.show_all() + + def on_voip_call_received_messagedialog_close(self, dialog): + return self.on_voip_call_received_messagedialog_response(dialog, gtk.RESPONSE_NO) + def on_voip_call_received_messagedialog_response(self, dialog, response): + # we've got response from user, either stop connecting or accept the call + session = gajim.connections[self.account].getJingleSession(self.jid, self.sid) + if response==gtk.RESPONSE_YES: + session.approveSession() + else: # response==gtk.RESPONSE_NO + session.declineSession() + + dialog.destroy() diff --git a/src/gajim.py b/src/gajim.py index bfb3052a4..ee7f8afbe 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1864,6 +1864,41 @@ class Interface: _('You are already connected to this account with the same resource. Please type a new one'), input_str = gajim.connections[account].server_resource, is_modal = False, ok_handler = on_ok) + def handle_event_jingle_incoming(self, account, data): + # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type, data...)) + # TODO: conditional blocking if peer is not in roster + + # unpack data + peerjid, sid, contents = data + content_types = set(c[0] for c in contents) + + # check type of jingle session + if 'VOIP' in content_types: + # a voip session... + # we now handle only voip, so the only thing we will do here is + # not to return from function + pass + else: + # unknown session type... it should be declined in common/jingle.py + return + + if helpers.allow_popup_window(account): + dialogs.VoIPCallReceivedDialog(account, peerjid, sid) + + # TODO: not checked + self.add_event(account, peerjid, 'jingle-session', (sid, contents)) + + if helpers.allow_showing_notification(account): + # TODO: we should use another pixmap ;-) + img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', + 'ft_request.png') + txt = _('%s wants to start a jingle session.') % gajim.get_name_from_jid( + account, jid) + path = gtkgui_helpers.get_path_to_generic_or_avatar(img) + event_type = _('Jingle Session Request') + notify.popup(event_type, jid, account, 'jingle-request', + path_to_image = path, title = event_type, text = txt) + def read_sleepy(self): '''Check idle status and change that status if needed''' if not self.sleeper.poll(): @@ -2192,6 +2227,7 @@ class Interface: 'SEARCH_FORM': self.handle_event_search_form, 'SEARCH_RESULT': self.handle_event_search_result, 'RESOURCE_CONFLICT': self.handle_event_resource_conflict, + 'JINGLE_INCOMING': self.handle_event_jingle_incoming, } gajim.handlers = self.handlers From be38a7a1eb8149e493e8b3968b9d3639acdf15b1 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Fri, 24 Aug 2007 13:25:12 +0000 Subject: [PATCH 24/67] Jingle: declining session --- src/common/jingle.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index f9de84f64..6b6e0cddc 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -78,7 +78,7 @@ class JingleSession(object): 'session-accept': [self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], 'session-info': [self.__defaultCB], 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, self.__defaultCB], - 'session-terminate': [self.__defaultCB], + 'session-terminate': [self.__broadcastAllCB, self.__defaultCB], 'transport-info': [self.__broadcastCB, self.__defaultCB], 'iq-result': [], 'iq-error': [], @@ -136,7 +136,6 @@ class JingleSession(object): self.accepted and \ all(c.negotiated for c in self.contents.itervalues()): self.__sessionAccept() - else: ''' Middle-level function to do stanza exchange. ''' def startSession(self): ''' Start session. ''' @@ -232,6 +231,11 @@ class JingleSession(object): cn = self.contents[(creator, name)] cn.stanzaCB(stanza, content, error, action) + 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) + def on_p2psession_error(self, *anything): pass ''' Methods that make/send proper pieces of XML. They check if the session @@ -285,8 +289,8 @@ class JingleSession(object): def __sessionTerminate(self): assert self.state!=JingleStates.ended stanza, jingle = self.__makeJingle('session-terminate') + self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent') self.connection.connection.send(stanza) - self.__broadcastCB(stanza, jingle, None, 'session-terminate-sent') def __contentAdd(self): assert self.state==JingleStates.active From 2838907e9709b3e68e49dd82bdb23f59c07c4d36 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Fri, 24 Aug 2007 14:36:53 +0000 Subject: [PATCH 25/67] Jingle: systray --- src/common/events.py | 6 ++++-- src/gajim.py | 35 ++++++++++++++++++++--------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/common/events.py b/src/common/events.py index ddb7eef2e..aa5790051 100644 --- a/src/common/events.py +++ b/src/common/events.py @@ -28,7 +28,8 @@ class Event: show_in_systray = True): ''' type_ in chat, normal, file-request, file-error, file-completed, file-request-error, file-send-error, file-stopped, gc_msg, pm, - printed_chat, printed_gc_msg, printed_marked_gc_msg, printed_pm + printed_chat, printed_gc_msg, printed_marked_gc_msg, printed_pm, + jingle-incoming parameters is (per type_): chat, normal: [message, subject, kind, time, encrypted, resource, msg_id] @@ -36,7 +37,8 @@ class Event: file-*: file_props gc_msg: None printed_*: None - messages that are already printed in chat, but not read''' + messages that are already printed in chat, but not read + jingle-*: (fulljid, sessionid) ''' self.type_ = type_ self.time_ = time_ self.parameters = parameters diff --git a/src/gajim.py b/src/gajim.py index ee7f8afbe..7a0e1e949 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1488,33 +1488,33 @@ class Interface: '''add an event to the gajim.events var''' # We add it to the gajim.events queue # Do we have a queue? - jid = gajim.get_jid_without_resource(jid) - no_queue = len(gajim.events.get_events(account, jid)) == 0 + barejid = gajim.get_jid_without_resource(jid) + no_queue = len(gajim.events.get_events(account, barejid)) == 0 event_type = None # type_ can be gc-invitation file-send-error file-error file-request-error - # file-request file-completed file-stopped + # file-request file-completed file-stopped voip-incoming # event_type can be in advancedNotificationWindow.events_list event_types = {'file-request': 'ft_request', 'file-completed': 'ft_finished'} if type_ in event_types: event_type = event_types[type_] - show_in_roster = notify.get_show_in_roster(event_type, account, jid) - show_in_systray = notify.get_show_in_systray(event_type, account, jid) + show_in_roster = notify.get_show_in_roster(event_type, account, barejid) + show_in_systray = notify.get_show_in_systray(event_type, account, barejid) event = gajim.events.create_event(type_, event_args, show_in_roster = show_in_roster, show_in_systray = show_in_systray) - gajim.events.add_event(account, jid, event) + gajim.events.add_event(account, barejid, event) self.roster.show_title() if no_queue: # We didn't have a queue: we change icons - if not gajim.contacts.get_contact_with_highest_priority(account, jid): + if not gajim.contacts.get_contact_with_highest_priority(account, barejid): # add contact to roster ("Not In The Roster") if he is not - self.roster.add_to_not_in_the_roster(account, jid) - self.roster.draw_contact(jid, account) + self.roster.add_to_not_in_the_roster(account, barejid) + self.roster.draw_contact(barejid, account) # Show contact in roster (if he is invisible for example) and select line - path = self.roster.get_path(jid, account) - self.roster.show_and_select_path(path, jid, account) + path = self.roster.get_path(barejid, account) + self.roster.show_and_select_path(path, barejid, account) def remove_first_event(self, account, jid, type_ = None): event = gajim.events.get_first_event(account, jid, type_) @@ -1885,18 +1885,18 @@ class Interface: if helpers.allow_popup_window(account): dialogs.VoIPCallReceivedDialog(account, peerjid, sid) - # TODO: not checked - self.add_event(account, peerjid, 'jingle-session', (sid, contents)) + self.add_event(account, peerjid, 'voip-incoming', (peerjid, sid,)) + # TODO: check this too if helpers.allow_showing_notification(account): # TODO: we should use another pixmap ;-) img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 'ft_request.png') txt = _('%s wants to start a jingle session.') % gajim.get_name_from_jid( - account, jid) + account, peerjid) path = gtkgui_helpers.get_path_to_generic_or_avatar(img) event_type = _('Jingle Session Request') - notify.popup(event_type, jid, account, 'jingle-request', + notify.popup(event_type, peerjid, account, 'jingle-request', path_to_image = path, title = event_type, text = txt) def read_sleepy(self): @@ -2323,6 +2323,11 @@ class Interface: data[1]) gajim.events.remove_events(account, jid, event) self.roster.draw_contact(jid, account) + elif type_ == 'voip-incoming': + event = gajim.events.get_first_event(account, jid, type_) + peerjid, sid = event.parameters + dialogs.VoIPCallReceivedDialog(account, peerjid, sid) + gajim.events.remove_events(account, jid, event) if w: w.set_active_tab(fjid, account) w.window.present() From da072e7b5fce425ebf248561b488b30c093dc5a8 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Fri, 24 Aug 2007 14:45:24 +0000 Subject: [PATCH 26/67] Jingle: notifications --- src/gajim.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/gajim.py b/src/gajim.py index 7a0e1e949..9c6699489 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1887,16 +1887,15 @@ class Interface: self.add_event(account, peerjid, 'voip-incoming', (peerjid, sid,)) - # TODO: check this too if helpers.allow_showing_notification(account): # TODO: we should use another pixmap ;-) img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 'ft_request.png') - txt = _('%s wants to start a jingle session.') % gajim.get_name_from_jid( + txt = _('%s wants to start a voice chat.') % gajim.get_name_from_jid( account, peerjid) path = gtkgui_helpers.get_path_to_generic_or_avatar(img) - event_type = _('Jingle Session Request') - notify.popup(event_type, peerjid, account, 'jingle-request', + event_type = _('Voice Chat Request') + notify.popup(event_type, peerjid, account, 'voip-incoming', path_to_image = path, title = event_type, text = txt) def read_sleepy(self): From d3a4029288c3037ea5d62464294b428ca5e97ae7 Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Fri, 24 Aug 2007 15:44:49 +0000 Subject: [PATCH 27/67] Jingle: backport of py2.5-only function --- src/common/jingle.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/common/jingle.py b/src/common/jingle.py index 6b6e0cddc..ee4c699b1 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -25,6 +25,12 @@ sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) import farsight sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_LOCAL) +def all(iterable): # backport of py2.5 function + for element in iterable: + if not element: + return False + return True + def timeout_add_and_call(timeout, callable, *args, **kwargs): ''' Call a callback once. If it returns True, add a timeout handler to call it more times. Helper function. ''' From 87db674203d559fe1166993e5995269b820dba70 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 22 Jan 2008 22:48:37 +0000 Subject: [PATCH 28/67] adding Makefile.am to compile farsight --- configure.ac | 3 +++ src/common/Makefile.am | 1 + src/common/farsight/Makefile | 24 ------------------- src/common/farsight/Makefile.am | 41 +++++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 24 deletions(-) delete mode 100644 src/common/farsight/Makefile create mode 100644 src/common/farsight/Makefile.am diff --git a/configure.ac b/configure.ac index c4bdb142e..8c8db0427 100644 --- a/configure.ac +++ b/configure.ac @@ -29,6 +29,8 @@ AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE],["$GETTEXT_PACKAGE"], [Gettext package]) AM_GLIB_GNU_GETTEXT AM_NLS +FARSIGHT_CFLAGS=`$PKG_CONFIG --libs farsight-0.1` +AC_SUBST([FARSIGHT_CFLAGS]) dnl **** dnl pygtk and gtk+ @@ -159,6 +161,7 @@ AC_CONFIG_FILES([ data/defs.py src/Makefile src/common/Makefile + src/common/farsight/Makefile scripts/gajim scripts/gajim-remote po/Makefile.in diff --git a/src/common/Makefile.am b/src/common/Makefile.am index ff75958c0..def7566c9 100644 --- a/src/common/Makefile.am +++ b/src/common/Makefile.am @@ -1,3 +1,4 @@ +SUBDIRS = farsight INCLUDES = \ $(PYTHON_INCLUDES) diff --git a/src/common/farsight/Makefile b/src/common/farsight/Makefile deleted file mode 100644 index 2c542b2f1..000000000 --- a/src/common/farsight/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -CFLAGS=-g `pkg-config --cflags farsight-0.1 pygtk-2.0` -I /usr/include/python2.5 -I. -I farsight/ -LDFLAGS=`pkg-config --libs farsight-0.1` - -farsight.so: farsight.o farsightmodule.o - $(CC) $(LDFLAGS) -shared $^ -o $@ - -farsight.c: farsight.defs farsight.override - pygtk-codegen-2.0 \ - --prefix farsight \ - --override farsight.override \ - --register /usr/share/gst-python/0.10/defs/gst-types.defs \ - farsight.defs >$@ - -farsight.defs: - python /usr/share/pygtk/2.0/codegen/h2def.py \ - farsight/farsight-codec.h \ - farsight/farsight-session.h \ - farsight/farsight-stream.h \ - >$@ - -clean: - -rm farsight.c farsightmodule.o farsight.o farsight.so - -.PHONY: clean diff --git a/src/common/farsight/Makefile.am b/src/common/farsight/Makefile.am new file mode 100644 index 000000000..5fa6cc50d --- /dev/null +++ b/src/common/farsight/Makefile.am @@ -0,0 +1,41 @@ +CLEANFILES = \ + farsight.c + +INCLUDES = \ + $(PYTHON_INCLUDES) +farsightlib_LTLIBRARIES = farsight.la +farsightlibdir = $(libdir)/gajim + +farsight_la_LIBADD = $(FARSIGHT_LIBS) + +farsight_la_SOURCES = \ + farsightmodule.c + +nodist_farsight_la_SOURCES = farsight.c + +farsight_la_LDFLAGS = \ + -module -avoid-version + +FARSIGHT_CFLAGS = @FARSIGHT_CFLAGS@ +farsight_la_CFLAGS = $(FARSIGHT_CFLAGS) $(PYGTK_CFLAGS) $(PYTHON_INCLUDES) -I farsight/ +#farsight_la_CFLAGS = -pthread -I/usr/include/farsight-0.1 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/gstreamer-0.10 -I/usr/include/libxml2 $(PYGTK_CFLAGS) $(PYTHON_INCLUDES) -I farsight/ + +farsight.c: farsight.defs farsight.override + pygtk-codegen-2.0 \ + --prefix farsight \ + --override farsight.override \ + --register /usr/share/gst-python/0.10/defs/gst-types.defs \ + farsight.defs >$@ + +farsight.defs: + python /usr/share/pygtk/2.0/codegen/h2def.py \ + farsight/farsight-codec.h \ + farsight/farsight-session.h \ + farsight/farsight-stream.h \ + >$@ + +DISTCLEANFILES = + +EXTRA_DIST = + +MAINTAINERCLEANFILES = Makefile.in From db015d2bfd1bfc8f0ca79eb3c26a34f2747d58fc Mon Sep 17 00:00:00 2001 From: Travis Shirk Date: Tue, 22 Jan 2008 23:03:41 +0000 Subject: [PATCH 29/67] Quote result of pkg-config --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 8c8db0427..fac01231b 100644 --- a/configure.ac +++ b/configure.ac @@ -29,7 +29,7 @@ AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE],["$GETTEXT_PACKAGE"], [Gettext package]) AM_GLIB_GNU_GETTEXT AM_NLS -FARSIGHT_CFLAGS=`$PKG_CONFIG --libs farsight-0.1` +FARSIGHT_CFLAGS="`$PKG_CONFIG --libs farsight-0.1`" AC_SUBST([FARSIGHT_CFLAGS]) dnl **** From 6bae5463fded30fee58ed7a56a11b060e961219b Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 22 Jan 2008 23:23:22 +0000 Subject: [PATCH 30/67] fix farsight compilation. call pkg-config --cflags instead of --libs --- configure.ac | 4 ++-- src/common/farsight/Makefile.am | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/configure.ac b/configure.ac index fac01231b..e1cb369ed 100644 --- a/configure.ac +++ b/configure.ac @@ -29,8 +29,8 @@ AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE],["$GETTEXT_PACKAGE"], [Gettext package]) AM_GLIB_GNU_GETTEXT AM_NLS -FARSIGHT_CFLAGS="`$PKG_CONFIG --libs farsight-0.1`" -AC_SUBST([FARSIGHT_CFLAGS]) +FARSIGHT_CFLAGS=`$PKG_CONFIG --cflags farsight-0.1` +AC_SUBST(FARSIGHT_CFLAGS) dnl **** dnl pygtk and gtk+ diff --git a/src/common/farsight/Makefile.am b/src/common/farsight/Makefile.am index 5fa6cc50d..7b415834d 100644 --- a/src/common/farsight/Makefile.am +++ b/src/common/farsight/Makefile.am @@ -16,9 +16,7 @@ nodist_farsight_la_SOURCES = farsight.c farsight_la_LDFLAGS = \ -module -avoid-version -FARSIGHT_CFLAGS = @FARSIGHT_CFLAGS@ farsight_la_CFLAGS = $(FARSIGHT_CFLAGS) $(PYGTK_CFLAGS) $(PYTHON_INCLUDES) -I farsight/ -#farsight_la_CFLAGS = -pthread -I/usr/include/farsight-0.1 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/gstreamer-0.10 -I/usr/include/libxml2 $(PYGTK_CFLAGS) $(PYTHON_INCLUDES) -I farsight/ farsight.c: farsight.defs farsight.override pygtk-codegen-2.0 \ From daf178144b76e768cb84ee1775f4d05d4c23065d Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 22 Jan 2008 23:37:19 +0000 Subject: [PATCH 31/67] set FARSIGHT_LIBS var --- configure.ac | 2 ++ src/common/contacts.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index e1cb369ed..66ad0e34b 100644 --- a/configure.ac +++ b/configure.ac @@ -29,6 +29,8 @@ AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE],["$GETTEXT_PACKAGE"], [Gettext package]) AM_GLIB_GNU_GETTEXT AM_NLS +FARSIGHT_LIBS=`$PKG_CONFIG --libs farsight-0.1` +AC_SUBST(FARSIGHT_LIBS) FARSIGHT_CFLAGS=`$PKG_CONFIG --cflags farsight-0.1` AC_SUBST(FARSIGHT_CFLAGS) diff --git a/src/common/contacts.py b/src/common/contacts.py index 52cb81959..48f4dd762 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -405,7 +405,7 @@ class Contacts: max_order = 0 order = 0 if data.has_key('order'): - order = data['order'] + order = int(data['order']) if order: family = self.get_metacontacts_family(account, jid) for data_ in family: From 32ad59aa427fe6ea800aa96c991be1a5a0c08846 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sun, 13 Sep 2009 11:02:49 +0200 Subject: [PATCH 32/67] jingle: move from the old farsight to farsight2, better compliance to the last version of XEP 0166, 0167 and 0176 --- src/chat_control.py | 9 +- src/common/gajim.py | 4 +- src/common/jingle.py | 672 +++++++++++++++++++++++------------- src/common/xmpp/protocol.py | 10 +- src/gajim.py | 1 + 5 files changed, 444 insertions(+), 252 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 6ada8fea3..ad41226c4 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -50,6 +50,7 @@ from common.logger import constants from common.pep import MOODS, ACTIVITIES from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION +from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO try: import gtkspell @@ -1634,7 +1635,7 @@ class ChatControl(ChatControlBase): banner_name_tooltip.set_tip(banner_name_label, label_tooltip) def _on_start_voip_menuitem_activate(self, *things): - gajim.connections[self.account].startVoiP(self.contact.jid+'/'+self.contact.resource) + gajim.connections[self.account].startVoIP(self.contact.jid+'/'+self.contact.resource) def _toggle_gpg(self): if not self.gpg_is_active and not self.contact.keyID: @@ -2168,6 +2169,12 @@ class ChatControl(ChatControlBase): else: send_file_menuitem.set_sensitive(False) + # check if it's possible to start jingle sessions + if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_AUDIO): + start_voip_menuitem.set_sensitive(True) + else: + start_voip_menuitem.set_sensitive(False) + # check if it's possible to convert to groupchat if gajim.capscache.is_supported(contact, NS_MUC): convert_to_gc_menuitem.set_sensitive(True) diff --git a/src/common/gajim.py b/src/common/gajim.py index 9a2ff9528..245aef3a0 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -196,7 +196,9 @@ gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI, xmpp.NS_FILE, 'jabber:iq:gateway', xmpp.NS_LAST, xmpp.NS_PRIVACY, xmpp.NS_PRIVATE, xmpp.NS_REGISTER, xmpp.NS_VERSION, xmpp.NS_DATA, xmpp.NS_ENCRYPTED, 'msglog', 'sslc2s', 'stringprep', xmpp.NS_PING, xmpp.NS_TIME_REVISED, xmpp.NS_SSN, - xmpp.NS_MOOD, xmpp.NS_ACTIVITY, xmpp.NS_NICK, xmpp.NS_ROSTERX] + xmpp.NS_MOOD, xmpp.NS_ACTIVITY, xmpp.NS_NICK, xmpp.NS_ROSTERX, + xmpp.NS_JINGLE, xmpp.NS_JINGLE_RTP, xmpp.NS_JINGLE_RTP_AUDIO, + xmpp.NS_JINGLE_ICE_UDP] # Optional features gajim supports per account gajim_optional_features = {} diff --git a/src/common/jingle.py b/src/common/jingle.py index ee4c699b1..85e2cfa19 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -12,42 +12,69 @@ ## ''' Handles the jingle signalling protocol. ''' -# note: if there will be more types of sessions (possibly file transfer, -# video...), split this file +#TODO: +# * things in XEP 0166, includign: +# - 'senders' attribute of 'content' element +# - security preconditions +# * actions: +# - content-accept, content-reject, content-add, content-modify +# - description-info, session-info +# - security-info +# - transport-accept, transport-reject +# * sid/content related: +# - tiebreaking +# - if there already is a session, use it +# * UI: +# - hang up button! +# - make state and codec informations available to the user +# * config: +# - codecs +# - STUN +# * figure out why it doesn't work with pidgin, and why it doesn't work well with psi +# * destroy sessions when user is unavailable, see handle_event_notify? +# * timeout +# * video +# * security (see XEP 0166) + +# * split this file in several modules +# For example, a file dedicated for XEP0166, one for XEP0176, +# and one for each media of XEP0167 + +# * handle different kinds of sink and src elements import gajim import gobject import xmpp -# ugly hack, fixed in farsight 0.1.24 -import sys, dl, gst -sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) -import farsight -sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_LOCAL) +import farsight, gst -def all(iterable): # backport of py2.5 function - for element in iterable: - if not element: - return False - return True - -def timeout_add_and_call(timeout, callable, *args, **kwargs): - ''' Call a callback once. If it returns True, add a timeout handler to call it more times. - Helper function. ''' - if callable(*args, **kwargs): - return gobject.timeout_add(timeout, callable, *args, **kwargs) - return -1 # gobject.source_remove will not object +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 Error(Exception): pass class WrongState(Error): pass class NoSuchSession(Error): pass +class OutOfOrder(Exception): + ''' Exception that should be raised when an action is received when in the wrong state. ''' + class JingleSession(object): ''' This represents one jingle session. ''' def __init__(self, con, weinitiate, jid, sid=None): @@ -72,28 +99,31 @@ class JingleSession(object): self.sid=sid # sessionid self.accepted=True # is this session accepted by user + self.candidates_ready = False # True when local candidates are prepared # 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.__defaultCB], - 'content-modify': [self.__defaultCB], - 'content-remove': [self.__defaultCB], - 'session-accept': [self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], - 'session-info': [self.__defaultCB], + 'content-accept': [self.__contentAcceptCB, self.__defaultCB], + 'content-add': [self.__defaultCB], #TODO + 'content-modify': [self.__defaultCB], #TODO + 'content-reject': [self.__defaultCB], #TODO + 'content-remove': [self.__defaultCB, self.__contentRemoveCB], + 'description-info': [self.__defaultCB], #TODO + 'security-info': [self.__defaultCB], #TODO + 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], + 'session-info': [self.__sessionInfoCB, self.__broadcastCB], 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, self.__defaultCB], - 'session-terminate': [self.__broadcastAllCB, 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': [], } - # for making streams using farsight - self.p2psession = farsight.farsight_session_factory_make('rtp') - self.p2psession.connect('error', self.on_p2psession_error) - ''' Interaction with user ''' def approveSession(self): ''' Called when user accepts session in UI (when we aren't the initiator).''' @@ -101,9 +131,10 @@ class JingleSession(object): self.acceptSession() def declineSession(self): - ''' Called when user declines session in UI (when we aren't the initiator, - or when the user wants to stop session completly. ''' - self.__sessionTerminate() + ''' Called when user declines session in UI (when we aren't the initiator)''' + reason = xmpp.Node('reason') + reason.addChild('decline') + self.__sessionTerminate(reason) ''' Middle-level functions to manage contents. Handle local content cache and send change notifications. ''' @@ -138,17 +169,29 @@ class JingleSession(object): def acceptSession(self): ''' Check if all contents and user agreed to start session. ''' - if not self.weinitiate and \ - self.accepted and \ - all(c.negotiated for c in self.contents.itervalues()): + if not self.weinitiate and self.accepted and self.candidates_ready: self.__sessionAccept() + ''' Middle-level function to do stanza exchange. ''' def startSession(self): ''' Start session. ''' - self.__sessionInitiate() + if self.weinitiate and self.candidates_ready: + self.__sessionInitiate() def sendSessionInfo(self): pass + def sendContentAccept(self, content): + assert self.state!=JingleStates.ended + stanza, jingle = self.__makeJingle('content-accept') + jingle.addChild(node=content) + self.connection.connection.send(stanza) + + def sendTransportInfo(self, content): + assert self.state!=JingleStates.ended + stanza, jingle = self.__makeJingle('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 @@ -162,6 +205,17 @@ class JingleSession(object): elif jingle: # it's a jingle action action = jingle.getAttr('action') + if action not in self.callbacks: + err = xmpp.Error(stanza, xmpp.NS_STANZAS + ' bad_request') + self.connection.connection.send(err) + return + #FIXME: If we aren't initiated and it's not a session-initiate... + if action != 'session-initiate' and self.state == JingleStates.ended: + err = xmpp.Error(stanza, xmpp.NS_STANZAS + ' item-not-found') + err.setTag('unknown-session', namespace=xmpp.NS_JINGLE_ERRORS) + self.connection.connection.send(err) + self.connection.deleteJingle(self) + return else: # it's an iq-result (ack) stanza action = 'iq-result' @@ -173,6 +227,10 @@ class JingleSession(object): callable(stanza=stanza, jingle=jingle, error=error, action=action) except xmpp.NodeProcessed: pass + except OutOfOrder: + err = xmpp.Error(stanza, xmpp.NS_STANZAS + ' unexpected-request') + err.setTag('out-of-order', namespace=xmpp.NS_JINGLE_ERRORS) + self.connection.connection.send(err) def __defaultCB(self, stanza, jingle, error, action): ''' Default callback for action stanzas -- simple ack @@ -180,18 +238,72 @@ class JingleSession(object): response = stanza.buildReply('result') self.connection.connection.send(response) + 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.__makeJingle('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 + else: + #FIXME: This ressource is unknown to us, what should we do? + #For now, reject the transport + stanza, jingle = self.__makeJingle('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): + payload = jingle.getPayload() + if len(payload) > 0: + err = xmpp.Error(stanza, xmpp.NS_STANZAS + ' feature-not-implemented') + err.setTag('unsupported-info', namespace=xmpp.NS_JINGLE_ERRORS) + self.connection.connection.send(err) + 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: + del self.contents[(creator, name)] + if len(self.contents) == 0: + reason = xmpp.Node('reason') + reason.setTag('success') #FIXME: Is it the good one? + self.__sessionTerminate(reason) + + def __sessionAcceptCB(self, stanza, jingle, error, action): + if self.state != JingleStates.pending: #FIXME + raise OutOfOrder + 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'] + name = content['name']#TODO... 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: #FIXME + raise OutOfOrder + self.initiator = jingle['initiator'] self.responder = self.ourjid self.peerjid = self.initiator @@ -203,25 +315,37 @@ class JingleSession(object): # error. # Lets check what kind of jingle session does the peer want - fail = True contents = [] + contents_ok = False + transports_ok = False for element in jingle.iterTags('content'): # checking what kind of session this will be desc_ns = element.getTag('description').getNamespace() + media = element.getTag('description')['media'] tran_ns = element.getTag('transport').getNamespace() - if desc_ns==xmpp.NS_JINGLE_AUDIO and tran_ns==xmpp.NS_JINGLE_ICE_UDP: - # we've got voip content - self.addContent(element['name'], JingleVoiP(self), 'peer') - contents.append(('VOIP',)) - fail = False + if desc_ns == xmpp.NS_JINGLE_RTP and media == 'audio': + contents_ok = True + if tran_ns == xmpp.NS_JINGLE_ICE_UDP: + # we've got voip content + self.addContent(element['name'], JingleVoIP(self), 'peer') + contents.append(('VOIP',)) + transports_ok = True # If there's no content we understand... - if fail: - # TODO: we should send inside too - # TODO: delete this instance - self.connection.connection.send( - xmpp.Error(stanza, xmpp.NS_STANZAS + 'feature-not-implemented')) - self.connection.deleteJingle(self) + 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.__sessionTerminate(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.__sessionTerminate(reason) raise xmpp.NodeProcessed self.state = JingleStates.pending @@ -237,23 +361,25 @@ class JingleSession(object): cn = self.contents[(creator, name)] cn.stanzaCB(stanza, content, error, action) + def __sessionTerminateCB(self, stanza, jingle, error, action): + self.connection.deleteJingle(self) + 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) - def on_p2psession_error(self, *anything): pass - ''' Methods that make/send proper pieces of XML. They check if the session is in appropriate state. ''' def __makeJingle(self, action): stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) - jingle = stanza.addChild('jingle', attrs={ - 'xmlns': 'http://www.xmpp.org/extensions/xep-0166.html#ns', - 'action': action, - 'initiator': self.initiator, - 'responder': self.responder, - 'sid': self.sid}) + 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 __appendContent(self, jingle, content): @@ -276,6 +402,7 @@ class JingleSession(object): self.__appendContents(jingle) self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent') self.connection.connection.send(stanza) + self.state = JingleStates.pending def __sessionAccept(self): assert self.state==JingleStates.pending @@ -292,11 +419,14 @@ class JingleSession(object): jingle.addChild(node=payload) self.connection.connection.send(stanza) - def __sessionTerminate(self): + def __sessionTerminate(self, reason=None): assert self.state!=JingleStates.ended stanza, jingle = self.__makeJingle('session-terminate') + if reason is not None: + jingle.addChild(node=reason) self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent') self.connection.connection.send(stanza) + self.connection.deleteJingle(self) def __contentAdd(self): assert self.state==JingleStates.active @@ -310,20 +440,13 @@ class JingleSession(object): def __contentRemove(self): assert self.state!=JingleStates.ended - def sendContentAccept(self, content): - assert self.state!=JingleStates.ended - stanza, jingle = self.__makeJingle('content-accept') - jingle.addChild(node=content) - self.connection.connection.send(stanza) - def sendTransportInfo(self, content): - assert self.state!=JingleStates.ended - stanza, jingle = self.__makeJingle('transport-info') - jingle.addChild(node=content) - self.connection.connection.send(stanza) +class JingleTransport(object): + ''' An abstraction of a transport in Jingle sessions. ''' + #TODO: Complete + def __init__(self): + pass#TODO: Complete - '''Callbacks''' - def sessionTerminateCB(self, stanza): pass class JingleContent(object): ''' An abstraction of content in Jingle sessions. ''' @@ -335,49 +458,179 @@ class JingleContent(object): #self.creator = None #self.name = None self.negotiated = False # is this content already negotiated? + self.candidates = [] # Local transport candidates -class JingleVoiP(JingleContent): - ''' Jingle VoiP sessions consist of audio content transported - over an ICE UDP protocol. ''' - def __init__(self, session, node=None): - JingleContent.__init__(self, session, node) - self.got_codecs = False + self.allow_sending = True # Used for stream direction, attribute 'senders' - #if node is None: - # self.audio = JingleAudioSession(self) - #else: - # self.audio = JingleAudioSession(self, node.getTag('content')) - #self.transport = JingleICEUDPSession(self) - self.setupStream() - - def stanzaCB(self, stanza, content, error, action): - ''' Called when something related to our content was sent by peer. ''' - callbacks = { + self.callbacks = { # these are called when *we* get stanzas - 'content-accept': [self.__getRemoteCodecsCB], + 'content-accept': [], 'content-add': [], 'content-modify': [], 'content-remove': [], - 'session-accept': [self.__getRemoteCodecsCB, self.__startMic], + 'session-accept': [self.__transportInfoCB], 'session-info': [], - 'session-initiate': [self.__getRemoteCodecsCB], - 'session-terminate': [self.__stop], + 'session-initiate': [self.__transportInfoCB], + 'session-terminate': [], 'transport-info': [self.__transportInfoCB], 'iq-result': [], 'iq-error': [], # these are called when *we* sent these stanzas - 'session-initiate-sent': [self.__sessionInitiateSentCB], - 'session-accept-sent': [self.__startMic], - 'session-terminate-sent': [self.__stop], - }[action] - for callback in callbacks: - callback(stanza, content, error, action) + 'session-initiate-sent': [self.__fillJingleStanza], + 'session-accept-sent': [self.__fillJingleStanza], + 'session-terminate-sent': [], + } - def __sessionInitiateSentCB(self, stanza, content, error, action): + 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']] + candidates.append(cand) + #FIXME: connectivity should not be etablished yet + # Instead, it should be etablished after session-accept! + if len(candidates) > 0: + self.p2pstream.set_remote_candidates(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: 'srlfx', + farsight.CANDIDATE_TYPE_PRFLX: 'prlfx', + 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 iterCandidates(self): + for candidate in self.candidates: + yield self.__candidate(candidate) + + def send_candidate(self, candidate): + c=self.__content() + t=c.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') + + if candidate.username: t['ufrag']=candidate.username + if candidate.password: t['pwd']=candidate.password + + t.addChild(node=self.__candidate(candidate)) + self.session.sendTransportInfo(c) + + def __fillJingleStanza(self, stanza, content, error, action): ''' Add our things to session-initiate stanza. ''' - content.setAttr('profile', 'RTP/AVP') - content.addChild(xmpp.NS_JINGLE_AUDIO+' description', payload=self.iterCodecs()) - content.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') + self._fillContent(content) + + 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.iterCandidates()) + + +class JingleRTPContent(JingleContent): + def __init__(self, session, node=None): + JingleContent.__init__(self, session, node) + self.got_codecs = False + + self.callbacks['content-accept'] += [self.__getRemoteCodecsCB] + self.callbacks['session-accept'] += [self.__getRemoteCodecsCB] + self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB] + + def setupStream(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 + + def _on_gst_message(self, bus, message): + if message.type == gst.MESSAGE_ELEMENT: + name = message.structure.get_name() + #print name + if name == 'farsight-new-active-candidate-pair': + pass + elif name == 'farsight-recv-codecs-changed': + pass + elif name == 'farsight-local-candidates-prepared': + self.session.candidates_ready = True + self.session.acceptSession() + self.session.startSession() + elif name == 'farsight-new-local-candidate': + candidate = message.structure['candidate'] + self.candidates.append(candidate) + if self.session.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_READY: + self.negotiated = True + self.pipeline.set_state(gst.STATE_PLAYING) + #if not self.session.weinitiate: #FIXME: one more FIXME... + # self.session.sendContentAccept(self.__content((xmpp.Node('description', payload=self.iterCodecs()),))) + 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'] def __getRemoteCodecsCB(self, stanza, content, error, action): ''' Get peer codecs from what we get from peer. ''' @@ -385,154 +638,86 @@ class JingleVoiP(JingleContent): codecs = [] for codec in content.getTag('description').iterTags('payload-type'): - c = {'id': int(codec['id']), - 'encoding_name': codec['name'], - 'media_type': farsight.MEDIA_TYPE_AUDIO, - 'channels': 1, - 'params': dict((p['name'], p['value']) for p in codec.iterTags('parameter'))} - if 'channels' in codec: c['channels']=codec['channels'] + c = farsight.Codec(int(codec['id']), codec['name'], + farsight.MEDIA_TYPE_AUDIO, 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: return + #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 __transportInfoCB(self, stanza, content, error, action): - ''' Got a new transport candidate. ''' - candidates = [] - for candidate in content.getTag('transport').iterTags('candidate'): - cand={ - 'candidate_id': self.session.connection.connection.getAnID(), - 'component': int(candidate['component']), - 'ip': candidate['ip'], - 'port': int(candidate['port']), - 'proto_subtype':'RTP', - 'proto_profile':'AVP', - 'preference': float(candidate['priority'])/100000, - 'type': farsight.CANDIDATE_TYPE_LOCAL, - } - 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 - if 'ufrag' in candidate: - cand['username']=candidate['ufrag'] - if 'pwd' in candidate: - cand['password']=candidate['pwd'] - - candidates.append(cand) - self.p2pstream.add_remote_candidate(candidates) - - def toXML(self): - ''' Return proper XML for element. ''' - return xmpp.Node('content', - attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, - payload=[ - xmpp.Node(xmpp.NS_JINGLE_AUDIO+' description', payload=self.iterCodecs()), - xmpp.Node(xmpp.NS_JINGLE_ICE_UDP+' transport') - ]) - - def __content(self, payload=[]): - ''' Build a XML content-wrapper for our data. ''' - return xmpp.Node('content', - attrs={'name': self.name, 'creator': self.creator, 'profile': 'RTP/AVP'}, - payload=payload) - - def on_p2pstream_error(self, *whatever): pass - def on_p2pstream_new_active_candidate_pair(self, stream, native, remote): pass - def on_p2pstream_codec_changed(self, stream, codecid): pass - def on_p2pstream_native_candidates_prepared(self, *whatever): - pass - - def on_p2pstream_state_changed(self, stream, state, dir): - if state==farsight.STREAM_STATE_CONNECTED: - stream.signal_native_candidates_prepared() - stream.start() - self.pipeline.set_state(gst.STATE_PLAYING) - - self.negotiated = True - if not self.session.weinitiate: - self.session.sendContentAccept(self.__content((xmpp.Node('description', payload=self.iterCodecs()),))) - self.session.acceptSession() - - def on_p2pstream_new_native_candidate(self, p2pstream, candidate_id): - candidates = p2pstream.get_native_candidate(candidate_id) - - for candidate in candidates: - self.send_candidate(candidate) - - def send_candidate(self, candidate): - attrs={ - 'component': candidate['component'], - 'foundation': '1', # hack - 'generation': '0', - 'ip': candidate['ip'], - 'network': '0', - 'port': candidate['port'], - 'priority': int(100000*candidate['preference']), # hack - } - 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' - if 'username' in candidate: attrs['ufrag']=candidate['username'] - if 'password' in candidate: attrs['pwd']=candidate['password'] - c=self.__content() - t=c.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') - t.addChild('candidate', attrs=attrs) - self.session.sendTransportInfo(c) - def iterCodecs(self): - codecs=self.p2pstream.get_local_codecs() + codecs=self.p2psession.get_property('codecs') for codec in codecs: - a = {'name': codec['encoding_name'], - 'id': codec['id'], - 'channels': 1} - if 'clock_rate' in codec: a['clockrate']=codec['clock_rate'] - if 'optional_params' in codec: + a = {'name': codec.encoding_name, + 'id': codec.id, + 'channels': codec.channels} + if codec.clock_rate: a['clockrate']=codec.clock_rate + if codec.optional_params: p = (xmpp.Node('parameter', {'name': name, 'value': value}) - for name, value in codec['optional_params'].iteritems()) + for name, value in codec.optional_params) else: p = () yield xmpp.Node('payload-type', a, p) + +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, node) + self.got_codecs = False + + self.callbacks['session-accept'] += [self.__startMic] + self.callbacks['session-terminate'] += [self.__stop] + self.callbacks['session-accept-sent'] += [self.__startMic] + self.callbacks['session-terminate-sent'] += [self.__stop] + + self.setupStream() + + def _fillContent(self, content): + content.addChild(xmpp.NS_JINGLE_RTP+' description', attrs={'media': 'audio'}, + payload=self.iterCodecs()) + ''' Things to control the gstreamer's pipeline ''' def setupStream(self): - # the pipeline - self.pipeline = gst.Pipeline() + JingleRTPContent.setupStream(self) # the network part - self.p2pstream = self.session.p2psession.create_stream( - farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) - self.p2pstream.set_pipeline(self.pipeline) - self.p2pstream.set_property('transmitter', 'libjingle') - self.p2pstream.connect('error', self.on_p2pstream_error) - self.p2pstream.connect('new-active-candidate-pair', self.on_p2pstream_new_active_candidate_pair) - self.p2pstream.connect('codec-changed', self.on_p2pstream_codec_changed) - self.p2pstream.connect('native-candidates-prepared', self.on_p2pstream_native_candidates_prepared) - self.p2pstream.connect('state-changed', self.on_p2pstream_state_changed) - self.p2pstream.connect('new-native-candidate', self.on_p2pstream_new_native_candidate) + participant = self.conference.new_participant(self.session.peerjid) + params = {'controlling-mode': self.session.weinitiate,# 'debug': False} + 'stun-ip': '69.0.208.27', 'debug': False} - self.p2pstream.set_remote_codecs(self.p2pstream.get_local_codecs()) + self.p2psession = self.conference.new_session(farsight.MEDIA_TYPE_AUDIO) - self.p2pstream.prepare_transports() + # Configure SPEEX + #FIXME: codec ID is an important thing for psi (and pidgin?) + # So, if it doesn't work with pidgin or psi, LOOK AT THIS + codecs = [farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', + farsight.MEDIA_TYPE_AUDIO, 8000), + farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', + farsight.MEDIA_TYPE_AUDIO, 16000)] + self.p2psession.set_codec_preferences(codecs) - self.p2pstream.set_active_codec(8) #??? + #TODO: farsight.DIRECTION_BOTH only if senders='both' + self.p2pstream = self.p2psession.new_stream(participant, farsight.DIRECTION_BOTH, + 'nice', params) # the local parts # TODO: use gconfaudiosink? + # sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink']) sink = gst.element_factory_make('alsasink') sink.set_property('sync', False) - sink.set_property('latency-time', 20000) - sink.set_property('buffer-time', 80000) + #sink.set_property('latency-time', 20000) + #sink.set_property('buffer-time', 80000) self.pipeline.add(sink) - self.src_signal = gst.element_factory_make('audiotestsrc') - self.src_signal.set_property('blocksize', 320) - self.src_signal.set_property('freq', 440) - self.pipeline.add(self.src_signal) - # TODO: use gconfaudiosrc? self.src_mic = gst.element_factory_make('alsasrc') self.src_mic.set_property('blocksize', 320) @@ -542,40 +727,35 @@ class JingleVoiP(JingleContent): self.mic_volume.set_property('volume', 0) self.pipeline.add(self.mic_volume) - self.adder = gst.element_factory_make('adder') - self.pipeline.add(self.adder) - # link gst elements - self.src_signal.link(self.adder) self.src_mic.link(self.mic_volume) - self.mic_volume.link(self.adder) - # this will actually start before the pipeline will be started. - # no worries, though; it's only a ringing sound - def signal(): - while True: - self.src_signal.set_property('volume', 0.5) - yield True # wait 750 ms - yield True # wait 750 ms - self.src_signal.set_property('volume', 0) - yield True # wait 750 ms - self.signal_cb_id = timeout_add_and_call(750, signal().__iter__().next) + def src_pad_added (stream, pad, codec): + if not self.funnel: + self.funnel = gst.element_factory_make('fsfunnel') + self.pipeline.add(self.funnel) + self.funnel.set_state (gst.STATE_PLAYING) + sink.set_state (gst.STATE_PLAYING) + self.funnel.link(sink) + pad.link(self.funnel.get_pad('sink%d')) - self.p2pstream.set_sink(sink) - self.p2pstream.set_source(self.adder) + self.mic_volume.get_pad('src').link(self.p2psession.get_property('sink-pad')) + self.p2pstream.connect('src-pad-added', src_pad_added) + + # The following is needed for farsight to process ICE requests: + self.conference.set_state(gst.STATE_PLAYING) def __startMic(self, *things): - gobject.source_remove(self.signal_cb_id) - self.src_signal.set_property('volume', 0) self.mic_volume.set_property('volume', 1) def __stop(self, *things): + self.conference.set_state(gst.STATE_NULL) self.pipeline.set_state(gst.STATE_NULL) - gobject.source_remove(self.signal_cb_id) def __del__(self): self.__stop() + class ConnectionJingle(object): ''' This object depends on that it is a part of Connection class. ''' def __init__(self): @@ -594,7 +774,7 @@ class ConnectionJingle(object): def deleteJingle(self, jingle): ''' Remove a jingle session from a jingle stanza dispatcher ''' - del self.__session[(jingle.peerjid, jingle.sid)] + del self.__sessions[(jingle.peerjid, jingle.sid)] def _JingleCB(self, con, stanza): ''' The jingle stanza dispatcher. @@ -629,10 +809,10 @@ class ConnectionJingle(object): def addJingleIqCallback(self, jid, id, jingle): self.__iq_responses[(jid, id)]=jingle - def startVoiP(self, jid): + def startVoIP(self, jid): jingle = JingleSession(self, weinitiate=True, jid=jid) self.addJingle(jingle) - jingle.addContent('voice', JingleVoiP(jingle)) + jingle.addContent('voice', JingleVoIP(jingle)) jingle.startSession() def getJingleSession(self, jid, sid): try: diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 2ab3c2bfe..0bdd737a3 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -63,10 +63,12 @@ NS_HTTP_BIND ='http://jabber.org/protocol/httpbind' # XEP-0124 NS_IBB ='http://jabber.org/protocol/ibb' NS_INVISIBLE ='presence-invisible' # Jabberd2 NS_IQ ='iq' # Jabberd2 -NS_JINGLE ='http://www.xmpp.org/extensions/xep-0166.html#ns' # XEP-0166 -NS_JINGLE_AUDIO ='http://www.xmpp.org/extensions/xep-0167.html#ns' # XEP-0167 -NS_JINGLE_RAW_UDP='http://www.xmpp.org/extensions/xep-0177.html#ns' # XEP-0177 -NS_JINGLE_ICE_UDP='http://www.xmpp.org/extensions/xep-0176.html#ns-udp' # XEP-0176 +NS_JINGLE ='urn:xmpp:jingle:1' # XEP-0166 +NS_JINGLE_ERRORS='urn:xmpp:jingle:errors:1' # XEP-0166 +NS_JINGLE_RTP ='urn:xmpp:jingle:apps:rtp:1' # XEP-0167 +NS_JINGLE_RTP_AUDIO='urn:xmpp:jingle:apps:rtp:audio' # XEP-0167 +NS_JINGLE_RAW_UDP='urn:xmpp:jingle:transports:raw-udp:1' # XEP-0177 +NS_JINGLE_ICE_UDP='urn:xmpp:jingle:transports:ice-udp:1' # XEP-0176 NS_LAST ='jabber:iq:last' NS_MESSAGE ='message' # Jabberd2 NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107 diff --git a/src/gajim.py b/src/gajim.py index 9f7cbcb30..c7352826e 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -2098,6 +2098,7 @@ class Interface: if helpers.allow_popup_window(account): dialogs.VoIPCallReceivedDialog(account, peerjid, sid) + return self.add_event(account, peerjid, 'voip-incoming', (peerjid, sid,)) From af44ee0840479e1861726f4d014a3b3460cd8f2d Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Wed, 16 Sep 2009 13:55:54 +0200 Subject: [PATCH 33/67] Moved things to JingleRTPContent, start pipeline earlier Some methods of JingleVoIPContent have been moved to JingleRTPContent, in prevision of a video content class. The pipeline now starts in setupStream, and the stream's direction is changed when the stream is ready. --- src/common/jingle.py | 61 ++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 85e2cfa19..c2eb54f62 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -13,7 +13,7 @@ ''' Handles the jingle signalling protocol. ''' #TODO: -# * things in XEP 0166, includign: +# * things in XEP 0166, including: # - 'senders' attribute of 'content' element # - security preconditions # * actions: @@ -24,13 +24,19 @@ # * 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 +# * XEP 0177 (raw udp) + # * UI: # - hang up button! # - make state and codec informations available to the user # * config: # - codecs # - STUN -# * figure out why it doesn't work with pidgin, and why it doesn't work well with psi +# * 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 # * destroy sessions when user is unavailable, see handle_event_notify? # * timeout # * video @@ -460,6 +466,7 @@ class JingleContent(object): self.negotiated = False # is this content already negotiated? self.candidates = [] # Local transport candidates + self.senders = 'both' #FIXME self.allow_sending = True # Used for stream direction, attribute 'senders' self.callbacks = { @@ -581,13 +588,16 @@ class JingleContent(object): class JingleRTPContent(JingleContent): - def __init__(self, session, node=None): + def __init__(self, session, media, node=None): JingleContent.__init__(self, session, node) + self.media = media self.got_codecs = False self.callbacks['content-accept'] += [self.__getRemoteCodecsCB] self.callbacks['session-accept'] += [self.__getRemoteCodecsCB] self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB] + self.callbacks['session-terminate'] += [self.__stop] + self.callbacks['session-terminate-sent'] += [self.__stop] def setupStream(self): # pipeline and bus @@ -602,6 +612,10 @@ class JingleRTPContent(JingleContent): self.pipeline.add(self.conference) self.funnel = None + def _fillContent(self, content): + content.addChild(xmpp.NS_JINGLE_RTP+' description', attrs={'media': self.media}, + payload=self.iterCodecs()) + def _on_gst_message(self, bus, message): if message.type == gst.MESSAGE_ELEMENT: name = message.structure.get_name() @@ -622,15 +636,18 @@ class JingleRTPContent(JingleContent): elif name == 'farsight-component-state-changed': state = message.structure['state'] print message.structure['component'], state - if state==farsight.STREAM_STATE_READY: + if state==farsight.STREAM_STATE_CONNECTED: self.negotiated = True - self.pipeline.set_state(gst.STATE_PLAYING) + #TODO: farsight.DIRECTION_BOTH only if senders='both' + self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) #if not self.session.weinitiate: #FIXME: one more FIXME... # self.session.sendContentAccept(self.__content((xmpp.Node('description', payload=self.iterCodecs()),))) 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 __getRemoteCodecsCB(self, stanza, content, error, action): ''' Get peer codecs from what we get from peer. ''' @@ -666,24 +683,23 @@ class JingleRTPContent(JingleContent): else: p = () yield xmpp.Node('payload-type', a, p) + def __stop(self, *things): + self.pipeline.set_state(gst.STATE_NULL) + + def __del__(self): + self.__stop() + 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, node) - self.got_codecs = False + JingleRTPContent.__init__(self, session, 'audio', node) - self.callbacks['session-accept'] += [self.__startMic] - self.callbacks['session-terminate'] += [self.__stop] - self.callbacks['session-accept-sent'] += [self.__startMic] - self.callbacks['session-terminate-sent'] += [self.__stop] + self.got_codecs = False self.setupStream() - def _fillContent(self, content): - content.addChild(xmpp.NS_JINGLE_RTP+' description', attrs={'media': 'audio'}, - payload=self.iterCodecs()) ''' Things to control the gstreamer's pipeline ''' def setupStream(self): @@ -705,8 +721,7 @@ class JingleVoIP(JingleRTPContent): farsight.MEDIA_TYPE_AUDIO, 16000)] self.p2psession.set_codec_preferences(codecs) - #TODO: farsight.DIRECTION_BOTH only if senders='both' - self.p2pstream = self.p2psession.new_stream(participant, farsight.DIRECTION_BOTH, + self.p2pstream = self.p2psession.new_stream(participant, farsight.DIRECTION_NONE, 'nice', params) # the local parts @@ -724,7 +739,7 @@ class JingleVoIP(JingleRTPContent): self.pipeline.add(self.src_mic) self.mic_volume = gst.element_factory_make('volume') - self.mic_volume.set_property('volume', 0) + self.mic_volume.set_property('volume', 1) self.pipeline.add(self.mic_volume) # link gst elements @@ -743,17 +758,7 @@ class JingleVoIP(JingleRTPContent): self.p2pstream.connect('src-pad-added', src_pad_added) # The following is needed for farsight to process ICE requests: - self.conference.set_state(gst.STATE_PLAYING) - - def __startMic(self, *things): - self.mic_volume.set_property('volume', 1) - - def __stop(self, *things): - self.conference.set_state(gst.STATE_NULL) - self.pipeline.set_state(gst.STATE_NULL) - - def __del__(self): - self.__stop() + self.pipeline.set_state(gst.STATE_PLAYING) class ConnectionJingle(object): From 11c7de6c3463d73e0ebbcba10e6dba452febb27b Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 16 Sep 2009 20:41:12 +0200 Subject: [PATCH 34/67] coding standards --- src/common/jingle.py | 173 +++++++++++++++++++++++-------------------- 1 file changed, 94 insertions(+), 79 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index c2eb54f62..3098bbda5 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -85,26 +85,27 @@ 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 + 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 + 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 + self.initiator = weinitiate and self.ourjid or self.peerjid # jid we use as the responder - self.responder=weinitiate and self.peerjid or self.ourjid + self.responder = weinitiate and self.peerjid or self.ourjid # are we an initiator? - self.weinitiate=weinitiate + self.weinitiate = weinitiate # what state is session in? (one from JingleStates) - self.state=JingleStates.ended + self.state = JingleStates.ended if not sid: - sid=con.connection.getAnID() - self.sid=sid # sessionid + sid = con.connection.getAnID() + self.sid = sid # sessionid - self.accepted=True # is this session accepted by user + self.accepted = True # is this session accepted by user self.candidates_ready = False # True when local candidates are prepared # callbacks to call on proper contents @@ -118,10 +119,13 @@ class JingleSession(object): 'content-remove': [self.__defaultCB, self.__contentRemoveCB], 'description-info': [self.__defaultCB], #TODO 'security-info': [self.__defaultCB], #TODO - 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], + 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, + self.__broadcastCB, self.__defaultCB], 'session-info': [self.__sessionInfoCB, self.__broadcastCB], - 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, self.__defaultCB], - 'session-terminate': [self.__sessionTerminateCB, self.__broadcastAllCB, 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 @@ -132,12 +136,14 @@ class JingleSession(object): ''' Interaction with user ''' def approveSession(self): - ''' Called when user accepts session in UI (when we aren't the initiator).''' - self.accepted=True + ''' Called when user accepts session in UI (when we aren't the initiator). + ''' + self.accepted = True self.acceptSession() def declineSession(self): - ''' Called when user declines session in UI (when we aren't the initiator)''' + ''' Called when user declines session in UI (when we aren't the initiator) + ''' reason = xmpp.Node('reason') reason.addChild('decline') self.__sessionTerminate(reason) @@ -151,18 +157,20 @@ class JingleSession(object): Creator must be one of ('we', 'peer', 'initiator', 'responder')''' assert creator in ('we', 'peer', 'initiator', 'responder') - if self.state==JingleStates.pending: + if self.state == JingleStates.pending: raise WrongState - 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' + 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 + self.contents[(creator,name)] = content - if self.state==JingleStates.active: + if self.state == JingleStates.active: pass # TODO: send proper stanza, shouldn't be needed now def removeContent(self, creator, name): @@ -187,7 +195,7 @@ class JingleSession(object): def sendSessionInfo(self): pass def sendContentAccept(self, content): - assert self.state!=JingleStates.ended + assert self.state != JingleStates.ended stanza, jingle = self.__makeJingle('content-accept') jingle.addChild(node=content) self.connection.connection.send(stanza) @@ -258,7 +266,7 @@ class JingleSession(object): else: stanza, jingle = self.__makeJingle('transport-reject') c = jingle.setTag('content', attrs={'creator': creator, - 'name': name}) + 'name': name}) c.setTag('transport', namespace=transport_ns) self.connection.connection.send(stanza) raise xmpp.NodeProcessed @@ -267,7 +275,7 @@ class JingleSession(object): #For now, reject the transport stanza, jingle = self.__makeJingle('transport-reject') c = jingle.setTag('content', attrs={'creator': creator, - 'name': name}) + 'name': name}) c.setTag('transport', namespace=transport_ns) self.connection.connection.send(stanza) raise xmpp.NodeProcessed @@ -357,7 +365,8 @@ class JingleSession(object): self.state = JingleStates.pending # Send event about starting a session - self.connection.dispatch('JINGLE_INCOMING', (self.initiator, self.sid, contents)) + self.connection.dispatch('JINGLE_INCOMING', (self.initiator, self.sid, + contents)) def __broadcastCB(self, stanza, jingle, error, action): ''' Broadcast the stanza contents to proper content handlers. ''' @@ -419,14 +428,14 @@ class JingleSession(object): self.state=JingleStates.active def __sessionInfo(self, payload=None): - assert self.state!=JingleStates.ended + assert self.state != JingleStates.ended stanza, jingle = self.__makeJingle('session-info') if payload: jingle.addChild(node=payload) self.connection.connection.send(stanza) def __sessionTerminate(self, reason=None): - assert self.state!=JingleStates.ended + assert self.state != JingleStates.ended stanza, jingle = self.__makeJingle('session-terminate') if reason is not None: jingle.addChild(node=reason) @@ -435,16 +444,16 @@ class JingleSession(object): self.connection.deleteJingle(self) def __contentAdd(self): - assert self.state==JingleStates.active + assert self.state == JingleStates.active def __contentAccept(self): - assert self.state!=JingleStates.ended + assert self.state != JingleStates.ended def __contentModify(self): - assert self.state!=JingleStates.ended + assert self.state != JingleStates.ended def __contentRemove(self): - assert self.state!=JingleStates.ended + assert self.state != JingleStates.ended class JingleTransport(object): @@ -469,7 +478,7 @@ class JingleContent(object): self.senders = 'both' #FIXME self.allow_sending = True # Used for stream direction, attribute 'senders' - self.callbacks = { + self.callbacks = { # these are called when *we* get stanzas 'content-accept': [], 'content-add': [], @@ -507,21 +516,21 @@ class JingleContent(object): #cand.type = farsight.CANDIDATE_TYPE_LOCAL cand.priority = int(candidate['priority']) - if candidate['protocol']=='udp': + 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.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} + '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']] candidates.append(cand) @@ -538,10 +547,10 @@ class JingleContent(object): def __candidate(self, candidate): types = {farsight.CANDIDATE_TYPE_HOST: 'host', - farsight.CANDIDATE_TYPE_SRFLX: 'srlfx', - farsight.CANDIDATE_TYPE_PRFLX: 'prlfx', - farsight.CANDIDATE_TYPE_RELAY: 'relay', - farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} + farsight.CANDIDATE_TYPE_SRFLX: 'srlfx', + farsight.CANDIDATE_TYPE_PRFLX: 'prlfx', + farsight.CANDIDATE_TYPE_RELAY: 'relay', + farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} attrs={ 'component': candidate.component_id, 'foundation': '1', # hack @@ -554,10 +563,10 @@ class JingleContent(object): if candidate.type in types: attrs['type'] = types[candidate.type] if candidate.proto==farsight.NETWORK_PROTOCOL_UDP: - attrs['protocol']='udp' + attrs['protocol'] = 'udp' else: # we actually don't handle properly different tcp options in jingle - attrs['protocol']='tcp' + attrs['protocol'] = 'tcp' return xmpp.Node('candidate', attrs=attrs) def iterCandidates(self): @@ -565,11 +574,11 @@ class JingleContent(object): yield self.__candidate(candidate) def send_candidate(self, candidate): - c=self.__content() - t=c.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') + c = self.__content() + t = c.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport') - if candidate.username: t['ufrag']=candidate.username - if candidate.password: t['pwd']=candidate.password + if candidate.username: t['ufrag'] = candidate.username + if candidate.password: t['pwd'] = candidate.password t.addChild(node=self.__candidate(candidate)) self.session.sendTransportInfo(c) @@ -578,14 +587,14 @@ class JingleContent(object): ''' Add our things to session-initiate stanza. ''' self._fillContent(content) - if self.candidates and self.candidates[0].username and self.candidates[0].password: + if self.candidates and self.candidates[0].username and \ + self.candidates[0].password: attrs = {'ufrag': self.candidates[0].username, - 'pwd': self.candidates[0].password} + 'pwd': self.candidates[0].password} else: attrs = {} - content.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport', attrs=attrs, - payload=self.iterCandidates()) - + content.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport', attrs=attrs, + payload=self.iterCandidates()) class JingleRTPContent(JingleContent): def __init__(self, session, media, node=None): @@ -613,8 +622,8 @@ class JingleRTPContent(JingleContent): self.funnel = None def _fillContent(self, content): - content.addChild(xmpp.NS_JINGLE_RTP+' description', attrs={'media': self.media}, - payload=self.iterCodecs()) + content.addChild(xmpp.NS_JINGLE_RTP + ' description', + attrs={'media': self.media}, payload=self.iterCodecs()) def _on_gst_message(self, bus, message): if message.type == gst.MESSAGE_ELEMENT: @@ -631,7 +640,8 @@ class JingleRTPContent(JingleContent): elif name == 'farsight-new-local-candidate': candidate = message.structure['candidate'] self.candidates.append(candidate) - if self.session.candidates_ready: #FIXME: Is this case even possible? + if self.session.candidates_ready: + #FIXME: Is this case even possible? self.send_candidate(candidate) elif name == 'farsight-component-state-changed': state = message.structure['state'] @@ -641,7 +651,8 @@ class JingleRTPContent(JingleContent): #TODO: farsight.DIRECTION_BOTH only if senders='both' self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) #if not self.session.weinitiate: #FIXME: one more FIXME... - # self.session.sendContentAccept(self.__content((xmpp.Node('description', payload=self.iterCodecs()),))) + # self.session.sendContentAccept(self.__content((xmpp.Node( + # 'description', payload=self.iterCodecs()),))) elif name == 'farsight-error': print 'Farsight error #%d!' % message.structure['error-no'] print 'Message: %s' % message.structure['error-msg'] @@ -656,30 +667,32 @@ class JingleRTPContent(JingleContent): codecs = [] for codec in content.getTag('description').iterTags('payload-type'): c = farsight.Codec(int(codec['id']), codec['name'], - farsight.MEDIA_TYPE_AUDIO, int(codec['clockrate'])) + farsight.MEDIA_TYPE_AUDIO, 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')] + c.optional_params = [(str(p['name']), str(p['value'])) for p in \ + codec.iterTags('parameter')] codecs.append(c) - if len(codecs)==0: return + if len(codecs) == 0: return #FIXME: Handle this case: - # glib.GError: There was no intersection between the remote codecs and the local ones + # glib.GError: There was no intersection between the remote codecs and + # the local ones self.p2pstream.set_remote_codecs(codecs) - self.got_codecs=True + self.got_codecs = True def iterCodecs(self): codecs=self.p2psession.get_property('codecs') for codec in codecs: a = {'name': codec.encoding_name, - 'id': codec.id, - 'channels': codec.channels} + 'id': codec.id, + 'channels': codec.channels} if codec.clock_rate: a['clockrate']=codec.clock_rate if codec.optional_params: p = (xmpp.Node('parameter', {'name': name, 'value': value}) - for name, value in codec.optional_params) + for name, value in codec.optional_params) else: p = () yield xmpp.Node('payload-type', a, p) @@ -708,7 +721,7 @@ class JingleVoIP(JingleRTPContent): # the network part participant = self.conference.new_participant(self.session.peerjid) params = {'controlling-mode': self.session.weinitiate,# 'debug': False} - 'stun-ip': '69.0.208.27', 'debug': False} + 'stun-ip': '69.0.208.27', 'debug': False} self.p2psession = self.conference.new_session(farsight.MEDIA_TYPE_AUDIO) @@ -716,13 +729,13 @@ class JingleVoIP(JingleRTPContent): #FIXME: codec ID is an important thing for psi (and pidgin?) # So, if it doesn't work with pidgin or psi, LOOK AT THIS codecs = [farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', - farsight.MEDIA_TYPE_AUDIO, 8000), - farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', - farsight.MEDIA_TYPE_AUDIO, 16000)] + farsight.MEDIA_TYPE_AUDIO, 8000), + farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', + farsight.MEDIA_TYPE_AUDIO, 16000)] self.p2psession.set_codec_preferences(codecs) - self.p2pstream = self.p2psession.new_stream(participant, farsight.DIRECTION_NONE, - 'nice', params) + self.p2pstream = self.p2psession.new_stream(participant, + farsight.DIRECTION_NONE, 'nice', params) # the local parts # TODO: use gconfaudiosink? @@ -754,7 +767,8 @@ class JingleVoIP(JingleRTPContent): self.funnel.link(sink) pad.link(self.funnel.get_pad('sink%d')) - self.mic_volume.get_pad('src').link(self.p2psession.get_property('sink-pad')) + self.mic_volume.get_pad('src').link(self.p2psession.get_property( + 'sink-pad')) self.p2pstream.connect('src-pad-added', src_pad_added) # The following is needed for farsight to process ICE requests: @@ -775,7 +789,7 @@ class ConnectionJingle(object): ''' Add a jingle session to a jingle stanza dispatcher jingle - a JingleSession object. ''' - self.__sessions[(jingle.peerjid, jingle.sid)]=jingle + self.__sessions[(jingle.peerjid, jingle.sid)] = jingle def deleteJingle(self, jingle): ''' Remove a jingle session from a jingle stanza dispatcher ''' @@ -812,13 +826,14 @@ class ConnectionJingle(object): raise xmpp.NodeProcessed def addJingleIqCallback(self, jid, id, jingle): - self.__iq_responses[(jid, id)]=jingle + self.__iq_responses[(jid, id)] = jingle def startVoIP(self, jid): jingle = JingleSession(self, weinitiate=True, jid=jid) self.addJingle(jingle) jingle.addContent('voice', JingleVoIP(jingle)) jingle.startSession() + def getJingleSession(self, jid, sid): try: return self.__sessions[(jid, sid)] From 8b4cd1b368ae3212b538674b23d9c81e1d8e3e02 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Wed, 16 Sep 2009 21:21:08 +0200 Subject: [PATCH 35/67] Fixed two more or less fatal mistakes from my last commit --- src/common/jingle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 3098bbda5..520007234 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -735,7 +735,7 @@ class JingleVoIP(JingleRTPContent): self.p2psession.set_codec_preferences(codecs) self.p2pstream = self.p2psession.new_stream(participant, - farsight.DIRECTION_NONE, 'nice', params) + farsight.DIRECTION_RECV, 'nice', params) # the local parts # TODO: use gconfaudiosink? From 163b01e113c0c5936125ab87440ce47d29e5bb81 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Wed, 16 Sep 2009 22:35:11 +0200 Subject: [PATCH 36/67] New class for video sessions, added an end_session method, and minor changes --- src/common/gajim.py | 2 +- src/common/jingle.py | 91 +++++++++++++++++++++++++++---------- src/common/xmpp/protocol.py | 1 + src/gajim.py | 2 +- 4 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/common/gajim.py b/src/common/gajim.py index 245aef3a0..6ffe8d050 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -198,7 +198,7 @@ gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI, xmpp.NS_FILE, 'sslc2s', 'stringprep', xmpp.NS_PING, xmpp.NS_TIME_REVISED, xmpp.NS_SSN, xmpp.NS_MOOD, xmpp.NS_ACTIVITY, xmpp.NS_NICK, xmpp.NS_ROSTERX, xmpp.NS_JINGLE, xmpp.NS_JINGLE_RTP, xmpp.NS_JINGLE_RTP_AUDIO, - xmpp.NS_JINGLE_ICE_UDP] + xmpp.NS_JINGLE_RTP_VIDEO, xmpp.NS_JINGLE_ICE_UDP] # Optional features gajim supports per account gajim_optional_features = {} diff --git a/src/common/jingle.py b/src/common/jingle.py index 520007234..a71a20d1f 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -148,6 +148,14 @@ class JingleSession(object): reason.addChild('decline') self.__sessionTerminate(reason) + def end_session(self): + reason = xmpp.Node('reason') + if self.state == JingleStates.active: + reason.addChild('success') + else: + reason.addChild('abort') + self.__sessionTerminate(reason) + ''' Middle-level functions to manage contents. Handle local content cache and send change notifications. ''' def addContent(self, name, content, creator='we'): @@ -337,12 +345,15 @@ class JingleSession(object): desc_ns = element.getTag('description').getNamespace() media = element.getTag('description')['media'] tran_ns = element.getTag('transport').getNamespace() - if desc_ns == xmpp.NS_JINGLE_RTP and media == 'audio': + if desc_ns == xmpp.NS_JINGLE_RTP and media in ('audio', 'video'): contents_ok = True if tran_ns == xmpp.NS_JINGLE_ICE_UDP: # we've got voip content - self.addContent(element['name'], JingleVoIP(self), 'peer') - contents.append(('VOIP',)) + if media == 'audio': + self.addContent(element['name'], JingleVoIP(self), 'peer') + else: + self.addContent(element['name'], JingleVideo(self), 'peer') + contents.append((media,)) transports_ok = True # If there's no content we understand... @@ -600,6 +611,8 @@ class JingleRTPContent(JingleContent): def __init__(self, session, media, node=None): JingleContent.__init__(self, session, node) self.media = media + self.farsight_media = {'audio': farsight.MEDIA_TYPE_AUDIO, + 'video': farsight.MEDIA_TYPE_VIDEO}[media] self.got_codecs = False self.callbacks['content-accept'] += [self.__getRemoteCodecsCB] @@ -621,6 +634,15 @@ class JingleRTPContent(JingleContent): 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) + 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 _fillContent(self, content): content.addChild(xmpp.NS_JINGLE_RTP + ' description', attrs={'media': self.media}, payload=self.iterCodecs()) @@ -646,7 +668,7 @@ class JingleRTPContent(JingleContent): elif name == 'farsight-component-state-changed': state = message.structure['state'] print message.structure['component'], state - if state==farsight.STREAM_STATE_CONNECTED: + if state==farsight.STREAM_STATE_READY: self.negotiated = True #TODO: farsight.DIRECTION_BOTH only if senders='both' self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) @@ -667,7 +689,7 @@ class JingleRTPContent(JingleContent): codecs = [] for codec in content.getTag('description').iterTags('payload-type'): c = farsight.Codec(int(codec['id']), codec['name'], - farsight.MEDIA_TYPE_AUDIO, int(codec['clockrate'])) + self.farsight_media, int(codec['clockrate'])) if 'channels' in codec: c.channels = int(codec['channels']) else: @@ -709,8 +731,6 @@ class JingleVoIP(JingleRTPContent): def __init__(self, session, node=None): JingleRTPContent.__init__(self, session, 'audio', node) - self.got_codecs = False - self.setupStream() @@ -718,13 +738,6 @@ class JingleVoIP(JingleRTPContent): def setupStream(self): JingleRTPContent.setupStream(self) - # the network part - participant = self.conference.new_participant(self.session.peerjid) - params = {'controlling-mode': self.session.weinitiate,# 'debug': False} - 'stun-ip': '69.0.208.27', 'debug': False} - - self.p2psession = self.conference.new_session(farsight.MEDIA_TYPE_AUDIO) - # Configure SPEEX #FIXME: codec ID is an important thing for psi (and pidgin?) # So, if it doesn't work with pidgin or psi, LOOK AT THIS @@ -734,9 +747,6 @@ class JingleVoIP(JingleRTPContent): farsight.MEDIA_TYPE_AUDIO, 16000)] self.p2psession.set_codec_preferences(codecs) - self.p2pstream = self.p2psession.new_stream(participant, - farsight.DIRECTION_RECV, 'nice', params) - # the local parts # TODO: use gconfaudiosink? # sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink']) @@ -744,19 +754,17 @@ class JingleVoIP(JingleRTPContent): sink.set_property('sync', False) #sink.set_property('latency-time', 20000) #sink.set_property('buffer-time', 80000) - self.pipeline.add(sink) # TODO: use gconfaudiosrc? - self.src_mic = gst.element_factory_make('alsasrc') - self.src_mic.set_property('blocksize', 320) - self.pipeline.add(self.src_mic) + 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) - self.pipeline.add(self.mic_volume) # link gst elements - self.src_mic.link(self.mic_volume) + self.pipeline.add(sink, src_mic, self.mic_volume) + src_mic.link(self.mic_volume) def src_pad_added (stream, pad, codec): if not self.funnel: @@ -774,6 +782,43 @@ class JingleVoIP(JingleRTPContent): # 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.setupStream() + + ''' Things to control the gstreamer's pipeline ''' + def setupStream(self): + #TODO: Everything is not working properly: + # sometimes, one window won't show up, + # sometimes it'll freeze... + JingleRTPContent.setupStream(self) + # the local parts + src_vid = gst.element_factory_make('videotestsrc') + 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) + + def src_pad_added (stream, pad, codec): + if not self.funnel: + self.funnel = gst.element_factory_make('fsfunnel') + self.pipeline.add(self.funnel) + videosink = gst.element_factory_make('xvimagesink') + self.pipeline.add(videosink) + self.funnel.set_state (gst.STATE_PLAYING) + videosink.set_state(gst.STATE_PLAYING) + self.funnel.link(videosink) + pad.link(self.funnel.get_pad('sink%d')) + + colorspace.get_pad('src').link(self.p2psession.get_property('sink-pad')) + self.p2pstream.connect('src-pad-added', src_pad_added) + + # The following is needed for farsight to process ICE requests: + self.pipeline.set_state(gst.STATE_PLAYING) class ConnectionJingle(object): ''' This object depends on that it is a part of Connection class. ''' diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 0bdd737a3..619771409 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -67,6 +67,7 @@ NS_JINGLE ='urn:xmpp:jingle:1' # XEP-01 NS_JINGLE_ERRORS='urn:xmpp:jingle:errors:1' # XEP-0166 NS_JINGLE_RTP ='urn:xmpp:jingle:apps:rtp:1' # XEP-0167 NS_JINGLE_RTP_AUDIO='urn:xmpp:jingle:apps:rtp:audio' # XEP-0167 +NS_JINGLE_RTP_VIDEO='urn:xmpp:jingle:apps:rtp:video' # XEP-0167 NS_JINGLE_RAW_UDP='urn:xmpp:jingle:transports:raw-udp:1' # XEP-0177 NS_JINGLE_ICE_UDP='urn:xmpp:jingle:transports:ice-udp:1' # XEP-0176 NS_LAST ='jabber:iq:last' diff --git a/src/gajim.py b/src/gajim.py index c7352826e..1f94856d1 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -2087,7 +2087,7 @@ class Interface: content_types = set(c[0] for c in contents) # check type of jingle session - if 'VOIP' in content_types: + if 'audio' in content_types or 'video' in content_types: # a voip session... # we now handle only voip, so the only thing we will do here is # not to return from function From 12baddbdc34a286e1214d6aaea444561a3d6b20b Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 17 Sep 2009 14:48:15 +0200 Subject: [PATCH 37/67] start of GUI stuff for VOIP --- data/glade/chat_control_popup_menu.glade | 63 +++--- data/glade/message_window.glade | 222 +++++++++++++-------- data/glade/voip_call_received_dialog.glade | 24 +-- src/chat_control.py | 79 +++++++- src/common/jingle.py | 11 +- src/dialogs.py | 30 ++- src/gajim.py | 45 +++++ 7 files changed, 340 insertions(+), 134 deletions(-) diff --git a/data/glade/chat_control_popup_menu.glade b/data/glade/chat_control_popup_menu.glade index b9df90bca..a794732b7 100644 --- a/data/glade/chat_control_popup_menu.glade +++ b/data/glade/chat_control_popup_menu.glade @@ -1,49 +1,52 @@ - - - + + + + _Add to Roster True - _Add to Roster True + False - + True gtk-add - 1 + 1 + Send _File True - Send _File True + False - + True gtk-save - 1 + 1 + Invite _Contacts GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - Invite _Contacts True + False - + True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-missing-image + None + 1 @@ -63,15 +66,22 @@ - - True - Start _Voice chat - True - - - - - + + True + Start _Voice chat + True + + + + + + True + Stop _Voice chat + True + + + + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -88,22 +98,23 @@ - True gtk-info + True True True + _History True - _History True + False - + True gtk-justify-fill - 1 + 1 diff --git a/data/glade/message_window.glade b/data/glade/message_window.glade index b3e2c3cc3..b99e7d1fe 100644 --- a/data/glade/message_window.glade +++ b/data/glade/message_window.glade @@ -1,7 +1,7 @@ - - - + + + 480 440 @@ -40,6 +40,7 @@ False False 5 + 0 @@ -54,6 +55,9 @@ <span weight="heavy" size="large">Contact name</span> True + + 0 + @@ -71,14 +75,17 @@ True None - 1 + 1 + + 0 + True None - 1 + 1 1 @@ -87,13 +94,23 @@ True - ../emoticons/static/music.png - 1 + ../emoticons/static/music.png + 1 2 + + + True + None + 1 + + + 3 + + 11 @@ -103,7 +120,7 @@ - 3 + 4 @@ -138,6 +155,7 @@ False False + 0 @@ -148,35 +166,40 @@ 60 True 3 - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN + automatic + automatic + in + + 0 + - + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True - GTK_RELIEF_NONE + none False - 0 GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-dialog-authentication - 1 + 1 False + 0 @@ -184,9 +207,9 @@ True True 3 - GTK_POLICY_NEVER - GTK_POLICY_NEVER - GTK_SHADOW_IN + never + never + in @@ -212,37 +235,40 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Show a list of emoticons (Alt+M) - GTK_RELIEF_NONE + none False - 0 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-missing-image - 1 + 1 False + 0 True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Show a list of formattings - GTK_RELIEF_NONE + none False - 0 True gtk-bold - 1 + 1 @@ -268,14 +294,13 @@ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True Add this contact to roster (Ctrl+D) - GTK_RELIEF_NONE - 0 + none True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-add - 1 + 1 @@ -288,16 +313,17 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Send a file (Ctrl+F) - GTK_RELIEF_NONE + none False - 0 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 1 + 1 @@ -309,17 +335,18 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Invite contacts to the conversation (Ctrl+G) - GTK_RELIEF_NONE + none False - 0 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-missing-image - 1 + 1 @@ -331,17 +358,18 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Show the contact's profile (Ctrl+I) - GTK_RELIEF_NONE + none False - 0 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-info - 2 + 2 @@ -353,17 +381,18 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Browse the chat history (Ctrl+H) - GTK_RELIEF_NONE + none False - 0 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-justify-fill - 1 + 1 @@ -385,17 +414,18 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Show a menu of advanced functions (Alt+A) - GTK_RELIEF_NONE + none False - 0 True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-execute - 1 + 1 @@ -420,7 +450,7 @@ True True - 0 + False True @@ -438,6 +468,7 @@ False False + 0 @@ -486,6 +517,7 @@ False False + 0 @@ -493,7 +525,7 @@ True 0 True - PANGO_ELLIPSIZE_END + end 1 @@ -505,14 +537,14 @@ 20 True True - GTK_RELIEF_NONE - 0 + False + none True 6 gtk-close - 1 + 1 @@ -526,8 +558,8 @@ - tab False + tab @@ -555,6 +587,7 @@ False False 5 + 0 @@ -569,6 +602,9 @@ <span weight="heavy" size="large">room jid</span> True + + 0 + @@ -586,6 +622,7 @@ False False + 0 @@ -609,21 +646,24 @@ 60 True True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN + automatic + automatic + in + + 0 + True True - GTK_POLICY_NEVER - GTK_POLICY_NEVER - GTK_SHADOW_IN + never + never + in @@ -634,6 +674,9 @@ + + 0 + @@ -646,9 +689,9 @@ 100 True False - GTK_POLICY_NEVER - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN + never + automatic + in True @@ -677,41 +720,45 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Show a list of emoticons (Alt+M) - GTK_RELIEF_NONE - 0 + none True gtk-missing-image - 1 + 1 False False + 0 False + 0 True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Show a list of formattings - GTK_RELIEF_NONE + none False - 0 True gtk-bold - 1 + 1 @@ -733,16 +780,17 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Change your nickname (Ctrl+N) - GTK_RELIEF_NONE - 0 + none True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-edit - 1 + 1 @@ -755,16 +803,17 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Change the room's subject (Alt+T) - GTK_RELIEF_NONE - 0 + none True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-properties - 1 + 1 @@ -777,17 +826,18 @@ True - True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True Bookmark this room (Ctrl+B) - GTK_RELIEF_NONE - 0 + none True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-add - 1 + 1 @@ -800,16 +850,17 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Browse the chat history (Ctrl+H) - GTK_RELIEF_NONE - 0 + none True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-justify-fill - 1 + 1 @@ -832,11 +883,12 @@ True + False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Show a menu of advanced functions (Alt+A) - GTK_RELIEF_NONE + none False - 0 True @@ -846,7 +898,7 @@ True gtk-execute - 1 + 1 @@ -874,7 +926,7 @@ True True - 0 + False True @@ -892,6 +944,7 @@ False False + 0 @@ -944,6 +997,7 @@ False False + 0 @@ -963,14 +1017,14 @@ 20 True True - GTK_RELIEF_NONE - 0 + False + none True 6 gtk-close - 1 + 1 @@ -984,9 +1038,9 @@ - tab 1 False + tab diff --git a/data/glade/voip_call_received_dialog.glade b/data/glade/voip_call_received_dialog.glade index 6b869f0f6..d2afca0a9 100644 --- a/data/glade/voip_call_received_dialog.glade +++ b/data/glade/voip_call_received_dialog.glade @@ -1,36 +1,36 @@ - - - + + + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 False - GTK_WIN_POS_CENTER_ON_PARENT - GDK_WINDOW_TYPE_HINT_DIALOG + center-on-parent + dialog True - False - GTK_MESSAGE_QUESTION - GTK_BUTTONS_YES_NO + question + yes-no <b><big>Incoming call</big></b> True %(contact)s wants to start a voice chat with you. Do you want to answer the call? - + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 - + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - GTK_BUTTONBOX_END + end False - GTK_PACK_END + end + 0 diff --git a/src/chat_control.py b/src/chat_control.py index ad41226c4..fca91d185 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1128,6 +1128,15 @@ class ChatControlBase(MessageControl): ################################################################################ class ChatControl(ChatControlBase): '''A control for standard 1-1 chat''' + ( + AUDIO_STATE_NOT_AVAILABLE, + AUDIO_STATE_AVAILABLE, + AUDIO_STATE_CONNECTING, + AUDIO_STATE_CONNECTION_RECEIVED, + AUDIO_STATE_CONNECTED, + AUDIO_STATE_ERROR + ) = range(6) + TYPE_ID = message_control.TYPE_CHAT old_msg_kind = None # last kind of the printed message CHAT_CMDS = ['clear', 'compact', 'help', 'me', 'ping', 'say'] @@ -1199,10 +1208,17 @@ class ChatControl(ChatControlBase): self._mood_image = self.xml.get_widget('mood_image') self._activity_image = self.xml.get_widget('activity_image') self._tune_image = self.xml.get_widget('tune_image') + self._audio_image = self.xml.get_widget('audio_image') self.update_mood() self.update_activity() self.update_tune() + if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_AUDIO): + self.set_audio_state('available') + else: + self.set_audio_state('not_available') + self.audio_sid = None + # keep timeout id and window obj for possible big avatar # it is on enter-notify and leave-notify so no need to be @@ -1434,6 +1450,45 @@ class ChatControl(ChatControlBase): else: self._tune_image.hide() + def update_audio(self): + if self.audio_state == self.AUDIO_STATE_NOT_AVAILABLE: + self._audio_image.hide() + return + elif self.audio_state == self.AUDIO_STATE_AVAILABLE: + self._audio_image.set_from_stock(gtk.STOCK_APPLY, 1) + elif self.audio_state == self.AUDIO_STATE_CONNECTING: + self._audio_image.set_from_stock(gtk.STOCK_CONVERT, 1) + elif self.audio_state == self.AUDIO_STATE_CONNECTION_RECEIVED: + self._audio_image.set_from_stock(gtk.STOCK_NETWORK, 1) + elif self.audio_state == self.AUDIO_STATE_CONNECTED: + self._audio_image.set_from_stock(gtk.STOCK_CONNECT, 1) + elif self.audio_state == self.AUDIO_STATE_ERROR: + self._audio_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 1) + self._audio_image.show() + + def set_audio_state(self, state, sid=None, reason=None): + str = 'Audio state : %s' % state + if reason: + str += ', reason: %s' % reason + self.print_conversation(str, 'info') + if state == 'not_available': + self.audio_state = self.AUDIO_STATE_NOT_AVAILABLE + self.audio_sid = None + elif state == 'available': + self.audio_state = self.AUDIO_STATE_AVAILABLE + self.audio_sid = None + elif state == 'connecting': + self.audio_state = self.AUDIO_STATE_CONNECTING + self.audio_sid = sid + elif state == 'connection_received': + self.audio_state = self.AUDIO_STATE_CONNECTION_RECEIVED + self.audio_sid = sid + elif state == 'connected': + self.audio_state = self.AUDIO_STATE_CONNECTED + elif state == 'error': + self.audio_state = self.AUDIO_STATE_ERROR + self.update_audio() + def on_avatar_eventbox_enter_notify_event(self, widget, event): ''' we enter the eventbox area so we under conditions add a timeout @@ -1635,7 +1690,16 @@ class ChatControl(ChatControlBase): banner_name_tooltip.set_tip(banner_name_label, label_tooltip) def _on_start_voip_menuitem_activate(self, *things): - gajim.connections[self.account].startVoIP(self.contact.jid+'/'+self.contact.resource) + sid = gajim.connections[self.account].startVoIP(self.contact.get_full_jid( + )) + self.set_audio_state('connecting', sid) + + def _on_stop_voip_menuitem_activate(self, *things): + session = gajim.connections[self.account].getJingleSession( + self.contact.get_full_jid(), self.audio_sid) + if session: + session.end_session() + self.set_audio_state('available') def _toggle_gpg(self): if not self.gpg_is_active and not self.contact.keyID: @@ -2100,6 +2164,7 @@ class ChatControl(ChatControlBase): history_menuitem = xml.get_widget('history_menuitem') toggle_gpg_menuitem = xml.get_widget('toggle_gpg_menuitem') start_voip_menuitem = xml.get_widget('start_voip_menuitem') + stop_voip_menuitem = xml.get_widget('stop_voip_menuitem') toggle_e2e_menuitem = xml.get_widget('toggle_e2e_menuitem') send_file_menuitem = xml.get_widget('send_file_menuitem') information_menuitem = xml.get_widget('information_menuitem') @@ -2170,10 +2235,15 @@ class ChatControl(ChatControlBase): send_file_menuitem.set_sensitive(False) # check if it's possible to start jingle sessions - if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_AUDIO): + if self.audio_state == self.AUDIO_STATE_NOT_AVAILABLE: + start_voip_menuitem.show() + start_voip_menuitem.set_sensitive(False) + elif self.audio_state in (self.AUDIO_STATE_AVAILABLE, + self.AUDIO_STATE_ERROR): + start_voip_menuitem.show() start_voip_menuitem.set_sensitive(True) else: - start_voip_menuitem.set_sensitive(False) + stop_voip_menuitem.show() # check if it's possible to convert to groupchat if gajim.capscache.is_supported(contact, NS_MUC): @@ -2197,6 +2267,9 @@ class ChatControl(ChatControlBase): id = start_voip_menuitem.connect('activate', self._on_start_voip_menuitem_activate) self.handlers[id] = start_voip_menuitem + id = stop_voip_menuitem.connect('activate', + self._on_stop_voip_menuitem_activate) + self.handlers[id] = stop_voip_menuitem id_ = toggle_e2e_menuitem.connect('activate', self._on_toggle_e2e_menuitem_activate) self.handlers[id_] = toggle_e2e_menuitem diff --git a/src/common/jingle.py b/src/common/jingle.py index a71a20d1f..9c53477cc 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -76,7 +76,6 @@ class TransportType(object): class Error(Exception): pass class WrongState(Error): pass -class NoSuchSession(Error): pass class OutOfOrder(Exception): ''' Exception that should be raised when an action is received when in the wrong state. ''' @@ -389,6 +388,7 @@ class JingleSession(object): def __sessionTerminateCB(self, stanza, jingle, error, action): self.connection.deleteJingle(self) + self.connection.dispatch('JINGLE_DISCONNECTED', (self.peerjid, self.sid)) def __broadcastAllCB(self, stanza, jingle, error, action): ''' Broadcast the stanza to all content handlers. ''' @@ -466,6 +466,9 @@ class JingleSession(object): def __contentRemove(self): assert self.state != JingleStates.ended + def content_negociated(self, media): + self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid, + media)) class JingleTransport(object): ''' An abstraction of a transport in Jingle sessions. ''' @@ -668,10 +671,11 @@ class JingleRTPContent(JingleContent): elif name == 'farsight-component-state-changed': state = message.structure['state'] print message.structure['component'], state - if state==farsight.STREAM_STATE_READY: + if state == farsight.STREAM_STATE_READY: self.negotiated = True #TODO: farsight.DIRECTION_BOTH only if senders='both' self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) + self.session.content_negociated(self.media) #if not self.session.weinitiate: #FIXME: one more FIXME... # self.session.sendContentAccept(self.__content((xmpp.Node( # 'description', payload=self.iterCodecs()),))) @@ -878,9 +882,10 @@ class ConnectionJingle(object): self.addJingle(jingle) jingle.addContent('voice', JingleVoIP(jingle)) jingle.startSession() + return jingle.sid def getJingleSession(self, jid, sid): try: return self.__sessions[(jid, sid)] except KeyError: - raise NoSuchSession + return None diff --git a/src/dialogs.py b/src/dialogs.py index 6db6c60df..26fb0da10 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -4443,15 +4443,16 @@ class GPGInfoWindow: class VoIPCallReceivedDialog(object): def __init__(self, account, contact_jid, sid): self.account = account - self.jid = contact_jid + self.fjid = contact_jid self.sid = sid xml = gtkgui_helpers.get_glade('voip_call_received_dialog.glade') xml.signal_autoconnect(self) - - contact = gajim.contacts.get_first_contact_from_jid(account, contact_jid) + + jid = gajim.get_jid_without_resource(self.fjid) + contact = gajim.contacts.get_first_contact_from_jid(account, jid) if contact and contact.name: - contact_text = '%s (%s)' % (contact.name, contact_jid) + contact_text = '%s (%s)' % (contact.name, jid) else: contact_text = contact_jid @@ -4463,12 +4464,29 @@ class VoIPCallReceivedDialog(object): dialog.show_all() def on_voip_call_received_messagedialog_close(self, dialog): - return self.on_voip_call_received_messagedialog_response(dialog, gtk.RESPONSE_NO) + return self.on_voip_call_received_messagedialog_response(dialog, + gtk.RESPONSE_NO) + def on_voip_call_received_messagedialog_response(self, dialog, response): # we've got response from user, either stop connecting or accept the call - session = gajim.connections[self.account].getJingleSession(self.jid, self.sid) + session = gajim.connections[self.account].getJingleSession(self.fjid, + self.sid) if response==gtk.RESPONSE_YES: session.approveSession() + jid = gajim.get_jid_without_resource(self.fjid) + resource = gajim.get_resource_from_jid(self.fjid) + ctrl = gajim.interface.msg_win_mgr.get_control(self.fjid, self.account) + if not ctrl: + ctrl = gajim.interface.msg_win_mgr.get_control(jid, self.account) + if not ctrl: + # open chat control + contact = gajim.contacts.get_contact(self.account, jid, resource) + if not contact: + contact = gajim.contacts.get_contact(self.account, jid) + if not contact: + return + ctrl = gajim.interface.new_chat(contact, self.account) + ctrl.set_audio_state('connecting', self.sid) else: # response==gtk.RESPONSE_NO session.declineSession() diff --git a/src/gajim.py b/src/gajim.py index 1f94856d1..149131951 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -2096,6 +2096,14 @@ class Interface: # unknown session type... it should be declined in common/jingle.py return + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.set_audio_state('connection_received', sid) + if helpers.allow_popup_window(account): dialogs.VoIPCallReceivedDialog(account, peerjid, sid) return @@ -2113,6 +2121,40 @@ class Interface: notify.popup(event_type, peerjid, account, 'voip-incoming', path_to_image = path, title = event_type, text = txt) + def handle_event_jingle_connected(self, account, data): + # ('JINGLE_CONNECTED', account, (peerjid, sid, media)) + peerjid, sid, media = data + if media == 'audio': + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.set_audio_state('connected', sid) + + def handle_event_jingle_disconnected(self, account, data): + # ('JINGLE_DISCONNECTED', account, (peerjid, sid)) + peerjid, sid = data + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.set_audio_state('available', sid) + + def handle_event_jingle_error(self, account, data): + # ('JINGLE_ERROR', account, (peerjid, sid, reason)) + peerjid, sid, reason = data + jid = gajim.get_jid_without_resource(peerjid) + resource = gajim.get_resource_from_jid(peerjid) + ctrl = self.msg_win_mgr.get_control(peerjid, account) + if not ctrl: + ctrl = self.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.set_audio_state('error', reason=reason) + def handle_event_pep_config(self, account, data): # ('PEP_CONFIG', account, (node, form)) if 'pep_services' in self.instances[account]: @@ -2371,6 +2413,9 @@ class Interface: 'PUBSUB_NODE_REMOVED': self.handle_event_pubsub_node_removed, 'PUBSUB_NODE_NOT_REMOVED': self.handle_event_pubsub_node_not_removed, 'JINGLE_INCOMING': self.handle_event_jingle_incoming, + 'JINGLE_CONNECTED': self.handle_event_jingle_connected, + 'JINGLE_DISCONNECTED': self.handle_event_jingle_disconnected, + 'JINGLE_ERROR': self.handle_event_jingle_error, } gajim.handlers = self.handlers From f5b1c2dca78163bc773875125513b22d25a3b68a Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 17 Sep 2009 22:21:06 +0200 Subject: [PATCH 38/67] improve GUI for jnigle --- data/glade/message_window.glade | 56 +++++++++++++++++++--- src/chat_control.py | 84 ++++++++++++++++++--------------- src/gajim.py | 9 ++-- 3 files changed, 100 insertions(+), 49 deletions(-) diff --git a/data/glade/message_window.glade b/data/glade/message_window.glade index b99e7d1fe..3220f4812 100644 --- a/data/glade/message_window.glade +++ b/data/glade/message_window.glade @@ -332,6 +332,48 @@ 4 + + + True + True + True + Start audio session + none + + + + True + gtk-missing-image + 1 + + + + + False + 5 + + + + + True + True + True + Stop audio session + none + + + + True + gtk-missing-image + 1 + + + + + False + 6 + + True @@ -352,7 +394,7 @@ False - 5 + 7 @@ -375,7 +417,7 @@ False - 6 + 8 @@ -398,7 +440,7 @@ False - 7 + 9 @@ -408,7 +450,7 @@ False - 8 + 10 @@ -431,7 +473,7 @@ False - 9 + 11 @@ -443,7 +485,7 @@ - 10 + 12 @@ -490,7 +532,7 @@ False - 11 + 13 diff --git a/src/chat_control.py b/src/chat_control.py index fca91d185..efc6c234b 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1161,6 +1161,16 @@ class ChatControl(ChatControlBase): self._on_add_to_roster_menuitem_activate) self.handlers[id_] = self._add_to_roster_button + self._start_audio_button = self.xml.get_widget('start_audio_button') + id_ = self._start_audio_button.connect('clicked', + self.on_start_audio_button_activate) + self.handlers[id_] = self._start_audio_button + + self._stop_audio_button = self.xml.get_widget('stop_audio_button') + id_ = self._stop_audio_button.connect('clicked', + self.on_stop_audio_button_activate) + self.handlers[id_] = self._stop_audio_button + self._send_file_button = self.xml.get_widget('send_file_button') # add a special img for send file button path_to_upload_img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'upload.png') @@ -1203,22 +1213,22 @@ class ChatControl(ChatControlBase): img.set_from_pixbuf(gtkgui_helpers.load_icon( 'muc_active').get_pixbuf()) - self.update_toolbar() - - self._mood_image = self.xml.get_widget('mood_image') - self._activity_image = self.xml.get_widget('activity_image') - self._tune_image = self.xml.get_widget('tune_image') self._audio_image = self.xml.get_widget('audio_image') - - self.update_mood() - self.update_activity() - self.update_tune() if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_AUDIO): self.set_audio_state('available') else: self.set_audio_state('not_available') self.audio_sid = None + self.update_toolbar() + + self._mood_image = self.xml.get_widget('mood_image') + self._activity_image = self.xml.get_widget('activity_image') + self._tune_image = self.xml.get_widget('tune_image') + + self.update_mood() + self.update_activity() + self.update_tune() # keep timeout id and window obj for possible big avatar # it is on enter-notify and leave-notify so no need to be @@ -1318,6 +1328,20 @@ class ChatControl(ChatControlBase): else: self._add_to_roster_button.hide() + # Audio buttons + if self.audio_state == self.AUDIO_STATE_NOT_AVAILABLE: + self._start_audio_button.show() + self._start_audio_button.set_sensitive(False) + self._stop_audio_button.hide() + elif self.audio_state in (self.AUDIO_STATE_AVAILABLE, + self.AUDIO_STATE_ERROR): + self._start_audio_button.show() + self._start_audio_button.set_sensitive(True) + self._stop_audio_button.hide() + else: + self._start_audio_button.hide() + self._stop_audio_button.show() + # Send file if gajim.capscache.is_supported(self.contact, NS_FILE) and \ self.contact.resource: @@ -1451,11 +1475,10 @@ class ChatControl(ChatControlBase): self._tune_image.hide() def update_audio(self): - if self.audio_state == self.AUDIO_STATE_NOT_AVAILABLE: + if self.audio_state in (self.AUDIO_STATE_NOT_AVAILABLE, + self.AUDIO_STATE_AVAILABLE): self._audio_image.hide() return - elif self.audio_state == self.AUDIO_STATE_AVAILABLE: - self._audio_image.set_from_stock(gtk.STOCK_APPLY, 1) elif self.audio_state == self.AUDIO_STATE_CONNECTING: self._audio_image.set_from_stock(gtk.STOCK_CONVERT, 1) elif self.audio_state == self.AUDIO_STATE_CONNECTION_RECEIVED: @@ -1465,12 +1488,14 @@ class ChatControl(ChatControlBase): elif self.audio_state == self.AUDIO_STATE_ERROR: self._audio_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 1) self._audio_image.show() + self.update_toolbar() def set_audio_state(self, state, sid=None, reason=None): - str = 'Audio state : %s' % state - if reason: - str += ', reason: %s' % reason - self.print_conversation(str, 'info') + if state in ('connecting', 'connected', 'stop'): + str = _('Audio state : %s') % state + if reason: + str += ', ' + _('reason: %s') % reason + self.print_conversation(str, 'info') if state == 'not_available': self.audio_state = self.AUDIO_STATE_NOT_AVAILABLE self.audio_sid = None @@ -1485,6 +1510,9 @@ class ChatControl(ChatControlBase): self.audio_sid = sid elif state == 'connected': self.audio_state = self.AUDIO_STATE_CONNECTED + elif state == 'stop': + self.audio_state = self.AUDIO_STATE_AVAILABLE + self.audio_sid = None elif state == 'error': self.audio_state = self.AUDIO_STATE_ERROR self.update_audio() @@ -1689,17 +1717,16 @@ class ChatControl(ChatControlBase): banner_name_label.set_markup(label_text) banner_name_tooltip.set_tip(banner_name_label, label_tooltip) - def _on_start_voip_menuitem_activate(self, *things): + def on_start_audio_button_activate(self, *things): sid = gajim.connections[self.account].startVoIP(self.contact.get_full_jid( )) self.set_audio_state('connecting', sid) - def _on_stop_voip_menuitem_activate(self, *things): + def on_stop_audio_button_activate(self, *things): session = gajim.connections[self.account].getJingleSession( self.contact.get_full_jid(), self.audio_sid) if session: session.end_session() - self.set_audio_state('available') def _toggle_gpg(self): if not self.gpg_is_active and not self.contact.keyID: @@ -2163,8 +2190,6 @@ class ChatControl(ChatControlBase): add_to_roster_menuitem = xml.get_widget('add_to_roster_menuitem') history_menuitem = xml.get_widget('history_menuitem') toggle_gpg_menuitem = xml.get_widget('toggle_gpg_menuitem') - start_voip_menuitem = xml.get_widget('start_voip_menuitem') - stop_voip_menuitem = xml.get_widget('stop_voip_menuitem') toggle_e2e_menuitem = xml.get_widget('toggle_e2e_menuitem') send_file_menuitem = xml.get_widget('send_file_menuitem') information_menuitem = xml.get_widget('information_menuitem') @@ -2234,17 +2259,6 @@ class ChatControl(ChatControlBase): else: send_file_menuitem.set_sensitive(False) - # check if it's possible to start jingle sessions - if self.audio_state == self.AUDIO_STATE_NOT_AVAILABLE: - start_voip_menuitem.show() - start_voip_menuitem.set_sensitive(False) - elif self.audio_state in (self.AUDIO_STATE_AVAILABLE, - self.AUDIO_STATE_ERROR): - start_voip_menuitem.show() - start_voip_menuitem.set_sensitive(True) - else: - stop_voip_menuitem.show() - # check if it's possible to convert to groupchat if gajim.capscache.is_supported(contact, NS_MUC): convert_to_gc_menuitem.set_sensitive(True) @@ -2264,12 +2278,6 @@ class ChatControl(ChatControlBase): id_ = toggle_gpg_menuitem.connect('activate', self._on_toggle_gpg_menuitem_activate) self.handlers[id_] = toggle_gpg_menuitem - id = start_voip_menuitem.connect('activate', - self._on_start_voip_menuitem_activate) - self.handlers[id] = start_voip_menuitem - id = stop_voip_menuitem.connect('activate', - self._on_stop_voip_menuitem_activate) - self.handlers[id] = stop_voip_menuitem id_ = toggle_e2e_menuitem.connect('activate', self._on_toggle_e2e_menuitem_activate) self.handlers[id_] = toggle_e2e_menuitem diff --git a/src/gajim.py b/src/gajim.py index 149131951..c922f4df2 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -2079,7 +2079,8 @@ class Interface: is_modal = False, ok_handler = on_ok) def handle_event_jingle_incoming(self, account, data): - # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type, data...)) + # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type, + # data...)) # TODO: conditional blocking if peer is not in roster # unpack data @@ -2134,15 +2135,15 @@ class Interface: ctrl.set_audio_state('connected', sid) def handle_event_jingle_disconnected(self, account, data): - # ('JINGLE_DISCONNECTED', account, (peerjid, sid)) - peerjid, sid = data + # ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason)) + peerjid, sid, reason = data jid = gajim.get_jid_without_resource(peerjid) resource = gajim.get_resource_from_jid(peerjid) ctrl = self.msg_win_mgr.get_control(peerjid, account) if not ctrl: ctrl = self.msg_win_mgr.get_control(jid, account) if ctrl: - ctrl.set_audio_state('available', sid) + ctrl.set_audio_state('stop', sid=sid, reason=reason) def handle_event_jingle_error(self, account, data): # ('JINGLE_ERROR', account, (peerjid, sid, reason)) From bd9d793ad8b1b06682d1ab038928f34dd1af256c Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Thu, 17 Sep 2009 23:36:26 +0200 Subject: [PATCH 39/67] send messages to the gui, wait for codecs, and other things JingleSession now sends messages about errors or session terminating to the GUI. Another thing is that it'll wait for all transports candidates and all codecs to be ready before starting or accepting a session. This is required by video, which is only missing a GUI. :) --- src/chat_control.py | 1 - src/common/jingle.py | 110 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 23 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index efc6c234b..d1b5c9dfc 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1478,7 +1478,6 @@ class ChatControl(ChatControlBase): if self.audio_state in (self.AUDIO_STATE_NOT_AVAILABLE, self.AUDIO_STATE_AVAILABLE): self._audio_image.hide() - return elif self.audio_state == self.AUDIO_STATE_CONNECTING: self._audio_image.set_from_stock(gtk.STOCK_CONVERT, 1) elif self.audio_state == self.AUDIO_STATE_CONNECTION_RECEIVED: diff --git a/src/common/jingle.py b/src/common/jingle.py index 9c53477cc..e433b5970 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -105,7 +105,6 @@ class JingleSession(object): self.sid = sid # sessionid self.accepted = True # is this session accepted by user - self.candidates_ready = False # True when local candidates are prepared # callbacks to call on proper contents # use .prepend() to add new callbacks, especially when you're going @@ -130,7 +129,7 @@ class JingleSession(object): 'transport-accept': [self.__defaultCB], #TODO 'transport-reject': [self.__defaultCB], #TODO 'iq-result': [], - 'iq-error': [], + 'iq-error': [self.__errorCB], } ''' Interaction with user ''' @@ -152,7 +151,7 @@ class JingleSession(object): if self.state == JingleStates.active: reason.addChild('success') else: - reason.addChild('abort') + reason.addChild('cancel') self.__sessionTerminate(reason) ''' Middle-level functions to manage contents. Handle local content @@ -190,13 +189,17 @@ class JingleSession(object): def acceptSession(self): ''' Check if all contents and user agreed to start session. ''' - if not self.weinitiate and self.accepted and self.candidates_ready: + if not self.weinitiate and self.accepted and \ + all((i.candidates_ready for i in self.contents.itervalues())) and \ + all((i.p2psession.get_property('codecs-ready') for i in self.contents.itervalues())): self.__sessionAccept() ''' Middle-level function to do stanza exchange. ''' def startSession(self): ''' Start session. ''' - if self.weinitiate and self.candidates_ready: + if self.weinitiate and \ + all((i.candidates_ready for i in self.contents.itervalues())) and \ + all((i.p2psession.get_property('codecs-ready') for i in self.contents.itervalues())): self.__sessionInitiate() def sendSessionInfo(self): pass @@ -227,15 +230,11 @@ class JingleSession(object): # it's a jingle action action = jingle.getAttr('action') if action not in self.callbacks: - err = xmpp.Error(stanza, xmpp.NS_STANZAS + ' bad_request') - self.connection.connection.send(err) + self.__send_error('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: - err = xmpp.Error(stanza, xmpp.NS_STANZAS + ' item-not-found') - err.setTag('unknown-session', namespace=xmpp.NS_JINGLE_ERRORS) - self.connection.connection.send(err) - self.connection.deleteJingle(self) + self.__send_error('item-not-found', 'unknown-session') return else: # it's an iq-result (ack) stanza @@ -249,9 +248,7 @@ class JingleSession(object): except xmpp.NodeProcessed: pass except OutOfOrder: - err = xmpp.Error(stanza, xmpp.NS_STANZAS + ' unexpected-request') - err.setTag('out-of-order', namespace=xmpp.NS_JINGLE_ERRORS) - self.connection.connection.send(err) + self.__send_error('unexpected-request', 'out-of-order')#FIXME def __defaultCB(self, stanza, jingle, error, action): ''' Default callback for action stanzas -- simple ack @@ -259,6 +256,20 @@ class JingleSession(object): 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 if we would want to do that... not yet... + #self.connection.deleteJingle(self) + def __transportReplaceCB(self, stanza, jingle, error, action): for content in jingle.iterTags('content'): creator = content['creator'] @@ -290,9 +301,7 @@ class JingleSession(object): def __sessionInfoCB(self, stanza, jingle, error, action): payload = jingle.getPayload() if len(payload) > 0: - err = xmpp.Error(stanza, xmpp.NS_STANZAS + ' feature-not-implemented') - err.setTag('unsupported-info', namespace=xmpp.NS_JINGLE_ERRORS) - self.connection.connection.send(err) + self.__send_error('feature-not-implemented', 'unsupported-info') raise xmpp.NodeProcessed def __contentRemoveCB(self, stanza, jingle, error, action): @@ -309,6 +318,7 @@ class JingleSession(object): 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 @@ -388,13 +398,55 @@ class JingleSession(object): def __sessionTerminateCB(self, stanza, jingle, error, action): self.connection.deleteJingle(self) - self.connection.dispatch('JINGLE_DISCONNECTED', (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, text)) + + 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 __dispatch_error(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 __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) + 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 __makeJingle(self, action): @@ -436,7 +488,7 @@ class JingleSession(object): self.__appendContents(jingle) self.__broadcastCB(stanza, jingle, None, 'session-accept-sent') self.connection.connection.send(stanza) - self.state=JingleStates.active + self.state = JingleStates.active def __sessionInfo(self, payload=None): assert self.state != JingleStates.ended @@ -452,6 +504,14 @@ class JingleSession(object): 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.dispatch('JINGLE_DISCONNECTED', (self.peerjid, self.sid, text)) self.connection.deleteJingle(self) def __contentAdd(self): @@ -618,6 +678,8 @@ class JingleRTPContent(JingleContent): 'video': farsight.MEDIA_TYPE_VIDEO}[media] self.got_codecs = False + self.candidates_ready = False # True when local candidates are prepared + self.callbacks['content-accept'] += [self.__getRemoteCodecsCB] self.callbacks['session-accept'] += [self.__getRemoteCodecsCB] self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB] @@ -658,14 +720,17 @@ class JingleRTPContent(JingleContent): pass elif name == 'farsight-recv-codecs-changed': pass + elif name == 'farsight-codecs-changed': + self.session.acceptSession() + self.session.startSession() elif name == 'farsight-local-candidates-prepared': - self.session.candidates_ready = True + self.candidates_ready = True self.session.acceptSession() self.session.startSession() elif name == 'farsight-new-local-candidate': candidate = message.structure['candidate'] self.candidates.append(candidate) - if self.session.candidates_ready: + if self.candidates_ready: #FIXME: Is this case even possible? self.send_candidate(candidate) elif name == 'farsight-component-state-changed': @@ -734,7 +799,6 @@ class JingleVoIP(JingleRTPContent): over an ICE UDP protocol. ''' def __init__(self, session, node=None): JingleRTPContent.__init__(self, session, 'audio', node) - self.setupStream() @@ -786,6 +850,7 @@ class JingleVoIP(JingleRTPContent): # 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) @@ -824,6 +889,7 @@ class JingleVideo(JingleRTPContent): # The following is needed for farsight to process ICE requests: self.pipeline.set_state(gst.STATE_PLAYING) + class ConnectionJingle(object): ''' This object depends on that it is a part of Connection class. ''' def __init__(self): From 5503c80e2afa68e54ebe9f7302a3f9910165d9a8 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Fri, 18 Sep 2009 20:17:35 +0200 Subject: [PATCH 40/67] fix some erros from my last commit, fix conditions for session initating and acceptance --- src/chat_control.py | 5 +++-- src/common/jingle.py | 34 +++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index d1b5c9dfc..bafe57be5 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1478,7 +1478,9 @@ class ChatControl(ChatControlBase): if self.audio_state in (self.AUDIO_STATE_NOT_AVAILABLE, self.AUDIO_STATE_AVAILABLE): self._audio_image.hide() - elif self.audio_state == self.AUDIO_STATE_CONNECTING: + else: + self._audio_image.show() + if self.audio_state == self.AUDIO_STATE_CONNECTING: self._audio_image.set_from_stock(gtk.STOCK_CONVERT, 1) elif self.audio_state == self.AUDIO_STATE_CONNECTION_RECEIVED: self._audio_image.set_from_stock(gtk.STOCK_NETWORK, 1) @@ -1486,7 +1488,6 @@ class ChatControl(ChatControlBase): self._audio_image.set_from_stock(gtk.STOCK_CONNECT, 1) elif self.audio_state == self.AUDIO_STATE_ERROR: self._audio_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 1) - self._audio_image.show() self.update_toolbar() def set_audio_state(self, state, sid=None, reason=None): diff --git a/src/common/jingle.py b/src/common/jingle.py index e433b5970..05319f51b 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -30,16 +30,17 @@ # * XEP 0177 (raw udp) # * UI: -# - hang up button! # - make state and codec informations available to the user +# - video integration # * config: # - 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 + # * destroy sessions when user is unavailable, see handle_event_notify? # * timeout -# * video # * security (see XEP 0166) # * split this file in several modules @@ -190,16 +191,18 @@ class JingleSession(object): def acceptSession(self): ''' Check if all contents and user agreed to start session. ''' if not self.weinitiate and self.accepted and \ - all((i.candidates_ready for i in self.contents.itervalues())) and \ - all((i.p2psession.get_property('codecs-ready') for i in self.contents.itervalues())): + self.state == JingleStates.pending and self.is_ready(): self.__sessionAccept() + def is_ready(self): + return all((c.candidates_ready and c.p2psession.get_property('codecs-ready') + for c in self.contents.itervalues())) + ''' Middle-level function to do stanza exchange. ''' def startSession(self): ''' Start session. ''' - if self.weinitiate and \ - all((i.candidates_ready for i in self.contents.itervalues())) and \ - all((i.p2psession.get_property('codecs-ready') for i in self.contents.itervalues())): + #FIXME: Start only once + if self.weinitiate and self.state == JingleStates.ended and self.is_ready(): self.__sessionInitiate() def sendSessionInfo(self): pass @@ -230,11 +233,11 @@ class JingleSession(object): # it's a jingle action action = jingle.getAttr('action') if action not in self.callbacks: - self.__send_error('bad_request') + 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('item-not-found', 'unknown-session') + self.__send_error(stanza, 'item-not-found', 'unknown-session') return else: # it's an iq-result (ack) stanza @@ -248,7 +251,7 @@ class JingleSession(object): except xmpp.NodeProcessed: pass except OutOfOrder: - self.__send_error('unexpected-request', 'out-of-order')#FIXME + self.__send_error(stanza, 'unexpected-request', 'out-of-order')#FIXME def __defaultCB(self, stanza, jingle, error, action): ''' Default callback for action stanzas -- simple ack @@ -267,8 +270,9 @@ class JingleSession(object): elif child.getNamespace() == xmpp.NS_STANZAS: xmpp_error = child.getName() self.__dispatch_error(xmpp_error, jingle_error, text) - #FIXME: Not sure if we would want to do that... not yet... - #self.connection.deleteJingle(self) + #FIXME: Not sure when we would want to do that... + if xmpp_error == 'item-not-found': + self.connection.deleteJingle(self) def __transportReplaceCB(self, stanza, jingle, error, action): for content in jingle.iterTags('content'): @@ -301,7 +305,7 @@ class JingleSession(object): def __sessionInfoCB(self, stanza, jingle, error, action): payload = jingle.getPayload() if len(payload) > 0: - self.__send_error('feature-not-implemented', 'unsupported-info') + self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info') raise xmpp.NodeProcessed def __contentRemoveCB(self, stanza, jingle, error, action): @@ -417,7 +421,7 @@ class JingleSession(object): self.connection.connection.send(err) self.__dispatch_error(error, jingle_error, text) - def __dispatch_error(error, jingle_error=None, text=None): + def __dispatch_error(self, error, jingle_error=None, text=None): if jingle_error: error = jingle_error if text: @@ -863,7 +867,7 @@ class JingleVideo(JingleRTPContent): # sometimes it'll freeze... JingleRTPContent.setupStream(self) # the local parts - src_vid = gst.element_factory_make('videotestsrc') + src_vid = gst.element_factory_make('v4l2src') 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')) From c162780a587618a1199b5267251e9b008d46280f Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sat, 19 Sep 2009 17:24:59 +0200 Subject: [PATCH 41/67] coding standards, and other few things --- src/chat_control.py | 2 +- src/common/jingle.py | 239 +++++++++++++++++++++---------------------- src/dialogs.py | 6 +- 3 files changed, 122 insertions(+), 125 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 2ff30f878..e5713fa68 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1775,7 +1775,7 @@ class ChatControl(ChatControlBase, ChatCommands): self.set_audio_state('connecting', sid) def on_stop_audio_button_activate(self, *things): - session = gajim.connections[self.account].getJingleSession( + session = gajim.connections[self.account].get_jingle_session( self.contact.get_full_jid(), self.audio_sid) if session: session.end_session() diff --git a/src/common/jingle.py b/src/common/jingle.py index 05319f51b..c777e60d7 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -50,7 +50,6 @@ # * handle different kinds of sink and src elements import gajim -import gobject import xmpp import farsight, gst @@ -65,9 +64,9 @@ def get_first_gst_element(elements): #FIXME: Move it to JingleSession.States? class JingleStates(object): ''' States in which jingle session may exist. ''' - ended=0 - pending=1 - active=2 + ended = 0 + pending = 1 + active = 2 #FIXME: Move it to JingleTransport.Type? class TransportType(object): @@ -75,9 +74,6 @@ class TransportType(object): datagram = 1 streaming = 2 -class Error(Exception): pass -class WrongState(Error): pass - class OutOfOrder(Exception): ''' Exception that should be raised when an action is received when in the wrong state. ''' @@ -110,7 +106,7 @@ class JingleSession(object): # 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={ + self.callbacks = { 'content-accept': [self.__contentAcceptCB, self.__defaultCB], 'content-add': [self.__defaultCB], #TODO 'content-modify': [self.__defaultCB], #TODO @@ -134,39 +130,37 @@ class JingleSession(object): } ''' Interaction with user ''' - def approveSession(self): + def approve_session(self): ''' Called when user accepts session in UI (when we aren't the initiator). ''' self.accepted = True - self.acceptSession() + self.accept_session() - def declineSession(self): + 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.__sessionTerminate(reason) + self.__session_terminate(reason) 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.__sessionTerminate(reason) + self.__session_terminate(reason) ''' Middle-level functions to manage contents. Handle local content cache and send change notifications. ''' - def addContent(self, name, content, creator='we'): + 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. The protocol prohibits changing that when pending. Creator must be one of ('we', 'peer', 'initiator', 'responder')''' assert creator in ('we', 'peer', 'initiator', 'responder') - if self.state == JingleStates.pending: - raise WrongState - if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \ not self.weinitiate): creator = 'initiator' @@ -175,47 +169,50 @@ class JingleSession(object): creator = 'responder' content.creator = creator content.name = name - self.contents[(creator,name)] = content + self.contents[(creator, name)] = content if self.state == JingleStates.active: pass # TODO: send proper stanza, shouldn't be needed now - def removeContent(self, creator, name): + def remove_content(self, creator, name): ''' We do not need this now ''' pass - def modifyContent(self, creator, name, *someother): + def modify_content(self, creator, name, *someother): ''' We do not need this now ''' pass - def acceptSession(self): + def accept_session(self): ''' Check if all contents and user agreed to start session. ''' if not self.weinitiate and self.accepted and \ self.state == JingleStates.pending and self.is_ready(): - self.__sessionAccept() + self.__session_accept() def is_ready(self): + ''' Returns True when all codecs and candidates are ready + (for all contents). ''' return all((c.candidates_ready and c.p2psession.get_property('codecs-ready') for c in self.contents.itervalues())) ''' Middle-level function to do stanza exchange. ''' - def startSession(self): + def start_session(self): ''' Start session. ''' #FIXME: Start only once if self.weinitiate and self.state == JingleStates.ended and self.is_ready(): - self.__sessionInitiate() + self.__session_initiate() - def sendSessionInfo(self): pass + def send_session_info(self): + pass - def sendContentAccept(self, content): + def send_content_accept(self, content): assert self.state != JingleStates.ended - stanza, jingle = self.__makeJingle('content-accept') + stanza, jingle = self.__make_jingle('content-accept') jingle.addChild(node=content) self.connection.connection.send(stanza) - def sendTransportInfo(self, content): - assert self.state!=JingleStates.ended - stanza, jingle = self.__makeJingle('transport-info') + 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) @@ -272,7 +269,7 @@ class JingleSession(object): 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.deleteJingle(self) + self.connection.delete_jingle(self) def __transportReplaceCB(self, stanza, jingle, error, action): for content in jingle.iterTags('content'): @@ -286,16 +283,16 @@ class JingleSession(object): #Anyway, content's transport is not modifiable yet pass else: - stanza, jingle = self.__makeJingle('transport-reject') - c = jingle.setTag('content', attrs={'creator': creator, + stanza, jingle = self.__make_jingle('transport-reject') + content = jingle.setTag('content', attrs={'creator': creator, 'name': name}) - c.setTag('transport', namespace=transport_ns) + 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.__makeJingle('transport-reject') + stanza, jingle = self.__make_jingle('transport-reject') c = jingle.setTag('content', attrs={'creator': creator, 'name': name}) c.setTag('transport', namespace=transport_ns) @@ -317,7 +314,7 @@ class JingleSession(object): if len(self.contents) == 0: reason = xmpp.Node('reason') reason.setTag('success') #FIXME: Is it the good one? - self.__sessionTerminate(reason) + self.__session_terminate(reason) def __sessionAcceptCB(self, stanza, jingle, error, action): if self.state != JingleStates.pending: #FIXME @@ -363,9 +360,9 @@ class JingleSession(object): if tran_ns == xmpp.NS_JINGLE_ICE_UDP: # we've got voip content if media == 'audio': - self.addContent(element['name'], JingleVoIP(self), 'peer') + self.add_content(element['name'], JingleVoIP(self), 'peer') else: - self.addContent(element['name'], JingleVideo(self), 'peer') + self.add_content(element['name'], JingleVideo(self), 'peer') contents.append((media,)) transports_ok = True @@ -375,7 +372,7 @@ class JingleSession(object): reason = xmpp.Node('reason') reason.setTag('unsupported-applications') self.__defaultCB(stanza, jingle, error, action) - self.__sessionTerminate(reason) + self.__session_terminate(reason) raise xmpp.NodeProcessed if not transports_ok: @@ -383,7 +380,7 @@ class JingleSession(object): reason = xmpp.Node('reason') reason.setTag('unsupported-transports') self.__defaultCB(stanza, jingle, error, action) - self.__sessionTerminate(reason) + self.__session_terminate(reason) raise xmpp.NodeProcessed self.state = JingleStates.pending @@ -401,7 +398,7 @@ class JingleSession(object): cn.stanzaCB(stanza, content, error, action) def __sessionTerminateCB(self, stanza, jingle, error, action): - self.connection.deleteJingle(self) + self.connection.delete_jingle(self) reason, text = self.__reason_from_stanza(jingle) if reason not in ('success', 'cancel', 'decline'): self.__dispatch_error(reason, reason, text) @@ -411,6 +408,11 @@ class JingleSession(object): text = reason#TODO self.connection.dispatch('JINGLE_DISCONNECTED', (self.peerjid, self.sid, 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) + def __send_error(self, stanza, error, jingle_error=None, text=None): err = xmpp.Error(stanza, error) err.setNamespace(xmpp.NS_STANZAS) @@ -430,11 +432,6 @@ class JingleSession(object): text = error self.connection.dispatch('JINGLE_ERROR', (self.peerjid, self.sid, 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) - def __reason_from_stanza(self, stanza): reason = 'success' reasons = ['success', 'busy', 'cancel', 'connectivity-error', @@ -453,7 +450,7 @@ class JingleSession(object): ''' Methods that make/send proper pieces of XML. They check if the session is in appropriate state. ''' - def __makeJingle(self, action): + def __make_jingle(self, action): stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) attrs = {'action': action, 'sid': self.sid} @@ -464,46 +461,46 @@ class JingleSession(object): jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE) return stanza, jingle - def __appendContent(self, jingle, content): + 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 __appendContents(self, jingle): + def __append_contents(self, jingle): ''' Append all elements to .''' # TODO: integrate with __appendContent? # TODO: parameters 'name', 'content'? for content in self.contents.values(): - self.__appendContent(jingle, content) + self.__append_content(jingle, content) - def __sessionInitiate(self): - assert self.state==JingleStates.ended - stanza, jingle = self.__makeJingle('session-initiate') - self.__appendContents(jingle) + 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 __sessionAccept(self): - assert self.state==JingleStates.pending - stanza, jingle = self.__makeJingle('session-accept') - self.__appendContents(jingle) + 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 __sessionInfo(self, payload=None): + def __session_info(self, payload=None): assert self.state != JingleStates.ended - stanza, jingle = self.__makeJingle('session-info') + stanza, jingle = self.__make_jingle('session-info') if payload: jingle.addChild(node=payload) self.connection.connection.send(stanza) - def __sessionTerminate(self, reason=None): + def __session_terminate(self, reason=None): assert self.state != JingleStates.ended - stanza, jingle = self.__makeJingle('session-terminate') + stanza, jingle = self.__make_jingle('session-terminate') if reason is not None: jingle.addChild(node=reason) self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent') @@ -516,29 +513,29 @@ class JingleSession(object): else: text = reason self.connection.dispatch('JINGLE_DISCONNECTED', (self.peerjid, self.sid, text)) - self.connection.deleteJingle(self) + self.connection.delete_jingle(self) - def __contentAdd(self): + def __content_add(self): assert self.state == JingleStates.active - def __contentAccept(self): + def __content_accept(self): assert self.state != JingleStates.ended - def __contentModify(self): + def __content_modify(self): assert self.state != JingleStates.ended - def __contentRemove(self): + def __content_remove(self): assert self.state != JingleStates.ended def content_negociated(self, media): self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid, media)) -class JingleTransport(object): - ''' An abstraction of a transport in Jingle sessions. ''' - #TODO: Complete - def __init__(self): - pass#TODO: Complete +#TODO: +#class JingleTransport(object): +# ''' An abstraction of a transport in Jingle sessions. ''' +# def __init__(self): +# pass class JingleContent(object): @@ -595,7 +592,7 @@ class JingleContent(object): cand.priority = int(candidate['priority']) if candidate['protocol'] == 'udp': - cand.proto=farsight.NETWORK_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 @@ -629,7 +626,7 @@ class JingleContent(object): farsight.CANDIDATE_TYPE_PRFLX: 'prlfx', farsight.CANDIDATE_TYPE_RELAY: 'relay', farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} - attrs={ + attrs = { 'component': candidate.component_id, 'foundation': '1', # hack 'generation': '0', @@ -640,26 +637,27 @@ class JingleContent(object): } if candidate.type in types: attrs['type'] = types[candidate.type] - if candidate.proto==farsight.NETWORK_PROTOCOL_UDP: + 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 iterCandidates(self): + def iter_candidates(self): for candidate in self.candidates: yield self.__candidate(candidate) def send_candidate(self, candidate): - c = self.__content() - t = c.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport') + content = self.__content() + transport = content.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport') - if candidate.username: t['ufrag'] = candidate.username - if candidate.password: t['pwd'] = candidate.password + if candidate.username and candidate.password: + transport['ufrag'] = candidate.username + transport['pwd'] = candidate.password - t.addChild(node=self.__candidate(candidate)) - self.session.sendTransportInfo(c) + 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. ''' @@ -672,7 +670,7 @@ class JingleContent(object): else: attrs = {} content.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport', attrs=attrs, - payload=self.iterCandidates()) + payload=self.iter_candidates()) class JingleRTPContent(JingleContent): def __init__(self, session, media, node=None): @@ -690,7 +688,7 @@ class JingleRTPContent(JingleContent): self.callbacks['session-terminate'] += [self.__stop] self.callbacks['session-terminate-sent'] += [self.__stop] - def setupStream(self): + def setup_stream(self): # pipeline and bus self.pipeline = gst.Pipeline() bus = self.pipeline.get_bus() @@ -714,7 +712,7 @@ class JingleRTPContent(JingleContent): def _fillContent(self, content): content.addChild(xmpp.NS_JINGLE_RTP + ' description', - attrs={'media': self.media}, payload=self.iterCodecs()) + attrs={'media': self.media}, payload=self.iter_codecs()) def _on_gst_message(self, bus, message): if message.type == gst.MESSAGE_ELEMENT: @@ -725,12 +723,12 @@ class JingleRTPContent(JingleContent): elif name == 'farsight-recv-codecs-changed': pass elif name == 'farsight-codecs-changed': - self.session.acceptSession() - self.session.startSession() + self.session.accept_session() + self.session.start_session() elif name == 'farsight-local-candidates-prepared': self.candidates_ready = True - self.session.acceptSession() - self.session.startSession() + self.session.accept_session() + self.session.start_session() elif name == 'farsight-new-local-candidate': candidate = message.structure['candidate'] self.candidates.append(candidate) @@ -746,8 +744,8 @@ class JingleRTPContent(JingleContent): self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) self.session.content_negociated(self.media) #if not self.session.weinitiate: #FIXME: one more FIXME... - # self.session.sendContentAccept(self.__content((xmpp.Node( - # 'description', payload=self.iterCodecs()),))) + # self.session.send_content_accept(self.__content((xmpp.Node( + # 'description', payload=self.iter_codecs()),))) elif name == 'farsight-error': print 'Farsight error #%d!' % message.structure['error-no'] print 'Message: %s' % message.structure['error-msg'] @@ -757,7 +755,8 @@ class JingleRTPContent(JingleContent): def __getRemoteCodecsCB(self, stanza, content, error, action): ''' Get peer codecs from what we get from peer. ''' - if self.got_codecs: return + if self.got_codecs: + return codecs = [] for codec in content.getTag('description').iterTags('payload-type'): @@ -770,26 +769,27 @@ class JingleRTPContent(JingleContent): c.optional_params = [(str(p['name']), str(p['value'])) for p in \ codec.iterTags('parameter')] codecs.append(c) - if len(codecs) == 0: return - #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 + 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 iterCodecs(self): - codecs=self.p2psession.get_property('codecs') + def iter_codecs(self): + codecs = self.p2psession.get_property('codecs') for codec in codecs: - a = {'name': codec.encoding_name, + attrs = {'name': codec.encoding_name, 'id': codec.id, 'channels': codec.channels} - if codec.clock_rate: a['clockrate']=codec.clock_rate + if codec.clock_rate: + attrs['clockrate'] = codec.clock_rate if codec.optional_params: - p = (xmpp.Node('parameter', {'name': name, 'value': value}) + payload = (xmpp.Node('parameter', {'name': name, 'value': value}) for name, value in codec.optional_params) - else: p = () - yield xmpp.Node('payload-type', a, p) + else: payload = () + yield xmpp.Node('payload-type', attrs, payload) def __stop(self, *things): self.pipeline.set_state(gst.STATE_NULL) @@ -803,12 +803,12 @@ class JingleVoIP(JingleRTPContent): over an ICE UDP protocol. ''' def __init__(self, session, node=None): JingleRTPContent.__init__(self, session, 'audio', node) - self.setupStream() + self.setup_stream() ''' Things to control the gstreamer's pipeline ''' - def setupStream(self): - JingleRTPContent.setupStream(self) + def setup_stream(self): + JingleRTPContent.setup_stream(self) # Configure SPEEX #FIXME: codec ID is an important thing for psi (and pidgin?) @@ -858,14 +858,14 @@ class JingleVoIP(JingleRTPContent): class JingleVideo(JingleRTPContent): def __init__(self, session, node=None): JingleRTPContent.__init__(self, session, 'video', node) - self.setupStream() + self.setup_stream() ''' Things to control the gstreamer's pipeline ''' - def setupStream(self): + def setup_stream(self): #TODO: Everything is not working properly: # sometimes, one window won't show up, # sometimes it'll freeze... - JingleRTPContent.setupStream(self) + JingleRTPContent.setup_stream(self) # the local parts src_vid = gst.element_factory_make('v4l2src') videoscale = gst.element_factory_make('videoscale') @@ -904,13 +904,13 @@ class ConnectionJingle(object): # one time callbacks self.__iq_responses = {} - def addJingle(self, jingle): + def add_jingle(self, jingle): ''' Add a jingle session to a jingle stanza dispatcher jingle - a JingleSession object. ''' self.__sessions[(jingle.peerjid, jingle.sid)] = jingle - def deleteJingle(self, jingle): + def delete_jingle(self, jingle): ''' Remove a jingle session from a jingle stanza dispatcher ''' del self.__sessions[(jingle.peerjid, jingle.sid)] @@ -937,24 +937,21 @@ class ConnectionJingle(object): # do we need to create a new jingle object if (jid, sid) not in self.__sessions: newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) - self.addJingle(newjingle) + self.add_jingle(newjingle) # we already have such session in dispatcher... self.__sessions[(jid, sid)].stanzaCB(stanza) raise xmpp.NodeProcessed - def addJingleIqCallback(self, jid, id, jingle): - self.__iq_responses[(jid, id)] = jingle - def startVoIP(self, jid): jingle = JingleSession(self, weinitiate=True, jid=jid) - self.addJingle(jingle) - jingle.addContent('voice', JingleVoIP(jingle)) - jingle.startSession() + self.add_jingle(jingle) + jingle.add_content('voice', JingleVoIP(jingle)) + jingle.start_session() return jingle.sid - def getJingleSession(self, jid, sid): + def get_jingle_session(self, jid, sid): try: return self.__sessions[(jid, sid)] except KeyError: diff --git a/src/dialogs.py b/src/dialogs.py index 9b1abfec8..ea2c86cfe 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -4470,10 +4470,10 @@ class VoIPCallReceivedDialog(object): def on_voip_call_received_messagedialog_response(self, dialog, response): # we've got response from user, either stop connecting or accept the call - session = gajim.connections[self.account].getJingleSession(self.fjid, + session = gajim.connections[self.account].get_jingle_session(self.fjid, self.sid) if response==gtk.RESPONSE_YES: - session.approveSession() + session.approve_session() jid = gajim.get_jid_without_resource(self.fjid) resource = gajim.get_resource_from_jid(self.fjid) ctrl = gajim.interface.msg_win_mgr.get_control(self.fjid, self.account) @@ -4489,7 +4489,7 @@ class VoIPCallReceivedDialog(object): ctrl = gajim.interface.new_chat(contact, self.account) ctrl.set_audio_state('connecting', self.sid) else: # response==gtk.RESPONSE_NO - session.declineSession() + session.decline_session() dialog.destroy() From 40199e359c605023982dae9feefa812822ace064 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sun, 20 Sep 2009 20:46:03 +0200 Subject: [PATCH 42/67] farsighr module is now optional. Fixes #5278 --- src/chat_control.py | 3 ++- src/common/connection_handlers.py | 9 ++++++++- src/common/gajim.py | 9 ++++++--- src/common/helpers.py | 6 ++++++ src/features_window.py | 7 +++++++ 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index e5713fa68..8e04e0a32 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1253,7 +1253,8 @@ class ChatControl(ChatControlBase, ChatCommands): 'muc_active').get_pixbuf()) self._audio_image = self.xml.get_widget('audio_image') - if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_AUDIO): + if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_AUDIO) and \ + gajim.HAVE_FARSIGHT: self.set_audio_state('available') else: self.set_audio_state('not_available') diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 85afe8947..b03915809 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -51,7 +51,14 @@ from common import exceptions from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub from common.caps import ConnectionCaps -from common.jingle import ConnectionJingle +if gajim.HAVE_FARSIGHT: + from common.jingle import ConnectionJingle +else: + class ConnectionJingle(): + def __init__(self): + pass + def _JingleCB(self, con, stanza): + pass from common import dbus_support if dbus_support.supported: diff --git a/src/common/gajim.py b/src/common/gajim.py index 6ffe8d050..3ab854ff0 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -189,6 +189,11 @@ try: except ImportError: HAVE_INDICATOR = False +HAVE_FARSIGHT = True +try: + import farsight, gst +except ImportError: + HAVE_FARSIGHT = False gajim_identity = {'type': 'pc', 'category': 'client', 'name': 'Gajim'} gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI, xmpp.NS_FILE, xmpp.NS_MUC, xmpp.NS_MUC_USER, xmpp.NS_MUC_ADMIN, xmpp.NS_MUC_OWNER, @@ -196,9 +201,7 @@ gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI, xmpp.NS_FILE, 'jabber:iq:gateway', xmpp.NS_LAST, xmpp.NS_PRIVACY, xmpp.NS_PRIVATE, xmpp.NS_REGISTER, xmpp.NS_VERSION, xmpp.NS_DATA, xmpp.NS_ENCRYPTED, 'msglog', 'sslc2s', 'stringprep', xmpp.NS_PING, xmpp.NS_TIME_REVISED, xmpp.NS_SSN, - xmpp.NS_MOOD, xmpp.NS_ACTIVITY, xmpp.NS_NICK, xmpp.NS_ROSTERX, - xmpp.NS_JINGLE, xmpp.NS_JINGLE_RTP, xmpp.NS_JINGLE_RTP_AUDIO, - xmpp.NS_JINGLE_RTP_VIDEO, xmpp.NS_JINGLE_ICE_UDP] + xmpp.NS_MOOD, xmpp.NS_ACTIVITY, xmpp.NS_NICK, xmpp.NS_ROSTERX] # Optional features gajim supports per account gajim_optional_features = {} diff --git a/src/common/helpers.py b/src/common/helpers.py index 0be1163f6..3cb264e52 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -1352,6 +1352,12 @@ def update_optional_features(account = None): gajim.gajim_optional_features[a].append(xmpp.NS_ESESSION) if gajim.config.get_per('accounts', a, 'answer_receipts'): gajim.gajim_optional_features[a].append(xmpp.NS_RECEIPTS) + if gajim.HAVE_FARSIGHT: + gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE) + gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP) + gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_AUDIO) + gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_VIDEO) + gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_ICE_UDP) gajim.caps_hash[a] = compute_caps_hash([gajim.gajim_identity], gajim.gajim_common_features + gajim.gajim_optional_features[a]) # re-send presence with new hash diff --git a/src/features_window.py b/src/features_window.py index ebe001558..609959e65 100644 --- a/src/features_window.py +++ b/src/features_window.py @@ -107,6 +107,10 @@ class FeaturesWindow: _('Ability to have clickable URLs in chat and groupchat window banners.'), _('Requires python-sexy.'), _('Requires python-sexy.')), + _('Audio / Video'): (self.farsight_available, + _('Ability to start audio and video chat.'), + _('Requires python-farsight.'), + _('Feature not available under Windows.')), } # name, supported @@ -265,4 +269,7 @@ class FeaturesWindow: def pysexy_available(self): return gajim.HAVE_PYSEXY + def farsight_available(self): + return gajim.HAVE_FARSIGHT + # vim: se ts=3: From a4e019ee7e471e7e5171911db89a3117753babc5 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sun, 20 Sep 2009 22:41:17 +0200 Subject: [PATCH 43/67] Last time, I removed something I shouldn't have... --- src/common/jingle.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index c777e60d7..aabc34a56 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -191,8 +191,7 @@ class JingleSession(object): def is_ready(self): ''' Returns True when all codecs and candidates are ready (for all contents). ''' - return all((c.candidates_ready and c.p2psession.get_property('codecs-ready') - for c in self.contents.itervalues())) + return all((content.is_ready() for content in self.contents.itervalues())) ''' Middle-level function to do stanza exchange. ''' def start_session(self): @@ -572,6 +571,11 @@ class JingleContent(object): 'session-terminate-sent': [], } + def is_ready(self): + #print '[%s] %s, %s' % (self.media, self.candidates_ready, + # self.p2psession.get_property('codecs-ready')) + return self.candidates_ready 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: @@ -944,6 +948,9 @@ class ConnectionJingle(object): raise xmpp.NodeProcessed + def addJingleIqCallback(self, jid, id, jingle): + self.__iq_responses[(jid, id)] = jingle + def startVoIP(self, jid): jingle = JingleSession(self, weinitiate=True, jid=jid) self.add_jingle(jingle) From d7560ed764780d344173f62df217a8f221629e69 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Tue, 22 Sep 2009 22:33:00 +0200 Subject: [PATCH 44/67] My last commit was useless... now terminate the session if the connection fails --- src/common/jingle.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index aabc34a56..c15067235 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -141,7 +141,7 @@ class JingleSession(object): ''' reason = xmpp.Node('reason') reason.addChild('decline') - self.__session_terminate(reason) + self._session_terminate(reason) def end_session(self): ''' Called when user stops or cancel session in UI. ''' @@ -150,7 +150,7 @@ class JingleSession(object): reason.addChild('success') else: reason.addChild('cancel') - self.__session_terminate(reason) + self._session_terminate(reason) ''' Middle-level functions to manage contents. Handle local content cache and send change notifications. ''' @@ -313,7 +313,7 @@ class JingleSession(object): if len(self.contents) == 0: reason = xmpp.Node('reason') reason.setTag('success') #FIXME: Is it the good one? - self.__session_terminate(reason) + self._session_terminate(reason) def __sessionAcceptCB(self, stanza, jingle, error, action): if self.state != JingleStates.pending: #FIXME @@ -371,7 +371,7 @@ class JingleSession(object): reason = xmpp.Node('reason') reason.setTag('unsupported-applications') self.__defaultCB(stanza, jingle, error, action) - self.__session_terminate(reason) + self._session_terminate(reason) raise xmpp.NodeProcessed if not transports_ok: @@ -379,7 +379,7 @@ class JingleSession(object): reason = xmpp.Node('reason') reason.setTag('unsupported-transports') self.__defaultCB(stanza, jingle, error, action) - self.__session_terminate(reason) + self._session_terminate(reason) raise xmpp.NodeProcessed self.state = JingleStates.pending @@ -497,7 +497,7 @@ class JingleSession(object): jingle.addChild(node=payload) self.connection.connection.send(stanza) - def __session_terminate(self, reason=None): + def _session_terminate(self, reason=None): assert self.state != JingleStates.ended stanza, jingle = self.__make_jingle('session-terminate') if reason is not None: @@ -742,7 +742,11 @@ class JingleRTPContent(JingleContent): elif name == 'farsight-component-state-changed': state = message.structure['state'] print message.structure['component'], state - if state == farsight.STREAM_STATE_READY: + if state == farsight.STREAM_STATE_FAILED: + reason = xmpp.Node('reason') + reason.setTag('failed-transport') + self.session._session_terminate(reason) + elif state == farsight.STREAM_STATE_READY: self.negotiated = True #TODO: farsight.DIRECTION_BOTH only if senders='both' self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) @@ -948,9 +952,6 @@ class ConnectionJingle(object): raise xmpp.NodeProcessed - def addJingleIqCallback(self, jid, id, jingle): - self.__iq_responses[(jid, id)] = jingle - def startVoIP(self, jid): jingle = JingleSession(self, weinitiate=True, jid=jid) self.add_jingle(jingle) From 32965a948e119eec8ccfd139bdbc7a42188298d8 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Fri, 25 Sep 2009 13:52:44 +0200 Subject: [PATCH 45/67] new GUI to start audio and video sessions. TODO: ability to add/remove audio/video content to an existing session --- data/glade/message_window.glade | 18 ++-- src/chat_control.py | 167 +++++++++++++++++++++----------- src/common/jingle.py | 46 +++++++-- 3 files changed, 157 insertions(+), 74 deletions(-) diff --git a/data/glade/message_window.glade b/data/glade/message_window.glade index 3220f4812..ad3263915 100644 --- a/data/glade/message_window.glade +++ b/data/glade/message_window.glade @@ -102,7 +102,7 @@ - + True None 1 @@ -333,15 +333,13 @@ - + True True - True - Start audio session + Toggle audio session none - - + True gtk-missing-image 1 @@ -354,15 +352,13 @@ - + True True - True - Stop audio session + Toggle video session none - - + True gtk-missing-image 1 diff --git a/src/chat_control.py b/src/chat_control.py index 8e04e0a32..4c980b544 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1167,12 +1167,12 @@ class ChatControlBase(MessageControl, CommonCommands): class ChatControl(ChatControlBase, ChatCommands): '''A control for standard 1-1 chat''' ( - AUDIO_STATE_NOT_AVAILABLE, - AUDIO_STATE_AVAILABLE, - AUDIO_STATE_CONNECTING, - AUDIO_STATE_CONNECTION_RECEIVED, - AUDIO_STATE_CONNECTED, - AUDIO_STATE_ERROR + JINGLE_STATE_NOT_AVAILABLE, + JINGLE_STATE_AVAILABLE, + JINGLE_STATE_CONNECTING, + JINGLE_STATE_CONNECTION_RECEIVED, + JINGLE_STATE_CONNECTED, + JINGLE_STATE_ERROR ) = range(6) TYPE_ID = message_control.TYPE_CHAT @@ -1200,15 +1200,13 @@ class ChatControl(ChatControlBase, ChatCommands): self._on_add_to_roster_menuitem_activate) self.handlers[id_] = self._add_to_roster_button - self._start_audio_button = self.xml.get_widget('start_audio_button') - id_ = self._start_audio_button.connect('clicked', - self.on_start_audio_button_activate) - self.handlers[id_] = self._start_audio_button + self._audio_button = self.xml.get_widget('audio_togglebutton') + id_ = self._audio_button.connect('toggled', self.on_audio_button_toggled) + self.handlers[id_] = self._audio_button - self._stop_audio_button = self.xml.get_widget('stop_audio_button') - id_ = self._stop_audio_button.connect('clicked', - self.on_stop_audio_button_activate) - self.handlers[id_] = self._stop_audio_button + self._video_button = self.xml.get_widget('video_togglebutton') + id_ = self._video_button.connect('toggled', self.on_video_button_toggled) + self.handlers[id_] = self._video_button self._send_file_button = self.xml.get_widget('send_file_button') # add a special img for send file button @@ -1252,13 +1250,20 @@ class ChatControl(ChatControlBase, ChatCommands): img.set_from_pixbuf(gtkgui_helpers.load_icon( 'muc_active').get_pixbuf()) - self._audio_image = self.xml.get_widget('audio_image') + self._audio_banner_image = self.xml.get_widget('audio_banner_image') + self._video_banner_image = self.xml.get_widget('video_banner_image') if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_AUDIO) and \ gajim.HAVE_FARSIGHT: self.set_audio_state('available') else: self.set_audio_state('not_available') + if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_VIDEO) and \ + gajim.HAVE_FARSIGHT: + self.set_video_state('available') + else: + self.set_video_state('not_available') self.audio_sid = None + self.video_sid = None self.update_toolbar() @@ -1369,18 +1374,16 @@ class ChatControl(ChatControlBase, ChatCommands): self._add_to_roster_button.hide() # Audio buttons - if self.audio_state == self.AUDIO_STATE_NOT_AVAILABLE: - self._start_audio_button.show() - self._start_audio_button.set_sensitive(False) - self._stop_audio_button.hide() - elif self.audio_state in (self.AUDIO_STATE_AVAILABLE, - self.AUDIO_STATE_ERROR): - self._start_audio_button.show() - self._start_audio_button.set_sensitive(True) - self._stop_audio_button.hide() + if self.audio_state == self.JINGLE_STATE_NOT_AVAILABLE: + self._audio_button.set_sensitive(False) else: - self._start_audio_button.hide() - self._stop_audio_button.show() + self._audio_button.set_sensitive(True) + + # Video buttons + if self.video_state == self.JINGLE_STATE_NOT_AVAILABLE: + self._video_button.set_sensitive(False) + else: + self._video_button.set_sensitive(True) # Send file if gajim.capscache.is_supported(self.contact, NS_FILE) and \ @@ -1515,19 +1518,35 @@ class ChatControl(ChatControlBase, ChatCommands): self._tune_image.hide() def update_audio(self): - if self.audio_state in (self.AUDIO_STATE_NOT_AVAILABLE, - self.AUDIO_STATE_AVAILABLE): - self._audio_image.hide() + if self.audio_state in (self.JINGLE_STATE_NOT_AVAILABLE, + self.JINGLE_STATE_AVAILABLE): + self._audio_banner_image.hide() else: - self._audio_image.show() - if self.audio_state == self.AUDIO_STATE_CONNECTING: - self._audio_image.set_from_stock(gtk.STOCK_CONVERT, 1) - elif self.audio_state == self.AUDIO_STATE_CONNECTION_RECEIVED: - self._audio_image.set_from_stock(gtk.STOCK_NETWORK, 1) - elif self.audio_state == self.AUDIO_STATE_CONNECTED: - self._audio_image.set_from_stock(gtk.STOCK_CONNECT, 1) - elif self.audio_state == self.AUDIO_STATE_ERROR: - self._audio_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 1) + self._audio_banner_image.show() + if self.audio_state == self.JINGLE_STATE_CONNECTING: + self._audio_banner_image.set_from_stock(gtk.STOCK_CONVERT, 1) + elif self.audio_state == self.JINGLE_STATE_CONNECTION_RECEIVED: + self._audio_banner_image.set_from_stock(gtk.STOCK_NETWORK, 1) + elif self.audio_state == self.JINGLE_STATE_CONNECTED: + self._audio_banner_image.set_from_stock(gtk.STOCK_CONNECT, 1) + elif self.audio_state == self.JINGLE_STATE_ERROR: + self._audio_banner_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 1) + self.update_toolbar() + + def update_video(self): + if self.video_state in (self.JINGLE_STATE_NOT_AVAILABLE, + self.JINGLE_STATE_AVAILABLE): + self._video_banner_image.hide() + else: + self._video_banner_image.show() + if self.video_state == self.JINGLE_STATE_CONNECTING: + self._video_banner_image.set_from_stock(gtk.STOCK_CONVERT, 1) + elif self.video_state == self.JINGLE_STATE_CONNECTION_RECEIVED: + self._video_banner_image.set_from_stock(gtk.STOCK_NETWORK, 1) + elif self.video_state == self.JINGLE_STATE_CONNECTED: + self._video_banner_image.set_from_stock(gtk.STOCK_CONNECT, 1) + elif self.video_state == self.JINGLE_STATE_ERROR: + self._video_banner_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 1) self.update_toolbar() def change_resource(self, resource): @@ -1550,26 +1569,53 @@ class ChatControl(ChatControlBase, ChatCommands): str += ', ' + _('reason: %s') % reason self.print_conversation(str, 'info') if state == 'not_available': - self.audio_state = self.AUDIO_STATE_NOT_AVAILABLE + self.audio_state = self.JINGLE_STATE_NOT_AVAILABLE self.audio_sid = None elif state == 'available': - self.audio_state = self.AUDIO_STATE_AVAILABLE + self.audio_state = self.JINGLE_STATE_AVAILABLE self.audio_sid = None elif state == 'connecting': - self.audio_state = self.AUDIO_STATE_CONNECTING + self.audio_state = self.JINGLE_STATE_CONNECTING self.audio_sid = sid elif state == 'connection_received': - self.audio_state = self.AUDIO_STATE_CONNECTION_RECEIVED + self.audio_state = self.JINGLE_STATE_CONNECTION_RECEIVED self.audio_sid = sid elif state == 'connected': - self.audio_state = self.AUDIO_STATE_CONNECTED + self.audio_state = self.JINGLE_STATE_CONNECTED elif state == 'stop': - self.audio_state = self.AUDIO_STATE_AVAILABLE + self.audio_state = self.JINGLE_STATE_AVAILABLE self.audio_sid = None elif state == 'error': - self.audio_state = self.AUDIO_STATE_ERROR + self.audio_state = self.JINGLE_STATE_ERROR self.update_audio() + def set_video_state(self, state, sid=None, reason=None): + if state in ('connecting', 'connected', 'stop'): + str = _('Video state : %s') % state + if reason: + str += ', ' + _('reason: %s') % reason + self.print_conversation(str, 'info') + if state == 'not_available': + self.video_state = self.JINGLE_STATE_NOT_AVAILABLE + self.video_sid = None + elif state == 'available': + self.video_state = self.JINGLE_STATE_AVAILABLE + self.video_sid = None + elif state == 'connecting': + self.video_state = self.JINGLE_STATE_CONNECTING + self.video_sid = sid + elif state == 'connection_received': + self.video_state = self.JINGLE_STATE_CONNECTION_RECEIVED + self.video_sid = sid + elif state == 'connected': + self.video_state = self.JINGLE_STATE_CONNECTED + elif state == 'stop': + self.video_state = self.JINGLE_STATE_AVAILABLE + self.video_sid = None + elif state == 'error': + self.video_state = self.JINGLE_STATE_ERROR + self.update_video() + def on_avatar_eventbox_enter_notify_event(self, widget, event): ''' we enter the eventbox area so we under conditions add a timeout @@ -1770,16 +1816,29 @@ class ChatControl(ChatControlBase, ChatCommands): banner_name_label.set_markup(label_text) banner_name_tooltip.set_tip(banner_name_label, label_tooltip) - def on_start_audio_button_activate(self, *things): - sid = gajim.connections[self.account].startVoIP(self.contact.get_full_jid( - )) - self.set_audio_state('connecting', sid) + def on_audio_button_toggled(self, widget): + if widget.get_active(): + sid = gajim.connections[self.account].startVoIP( + self.contact.get_full_jid()) + self.set_audio_state('connecting', sid) + else: + session = gajim.connections[self.account].get_jingle_session( + self.contact.get_full_jid(), self.audio_sid) + if session: + # TODO: end only audio + session.end_session() - def on_stop_audio_button_activate(self, *things): - session = gajim.connections[self.account].get_jingle_session( - self.contact.get_full_jid(), self.audio_sid) - if session: - session.end_session() + def on_video_button_toggled(self, widget): + if widget.get_active(): + sid = gajim.connections[self.account].startVideoIP( + self.contact.get_full_jid()) + self.set_video_state('connecting', sid) + else: + session = gajim.connections[self.account].get_jingle_session( + self.contact.get_full_jid(), self.video_sid) + if session: + # TODO: end only video + session.end_session() def _toggle_gpg(self): if not self.gpg_is_active and not self.contact.keyID: diff --git a/src/common/jingle.py b/src/common/jingle.py index c15067235..9232ae02c 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -953,14 +953,42 @@ class ConnectionJingle(object): raise xmpp.NodeProcessed def startVoIP(self, jid): - jingle = JingleSession(self, weinitiate=True, jid=jid) - self.add_jingle(jingle) - jingle.add_content('voice', JingleVoIP(jingle)) - jingle.start_session() + jingle = self.get_jingle_session(jid, media='video') + if jingle: + jingle.add_content('voice', JingleVoIP(jingle)) + else: + jingle = JingleSession(self, weinitiate=True, jid=jid) + self.add_jingle(jingle) + jingle.add_content('voice', JingleVoIP(jingle)) + jingle.start_session() return jingle.sid - def get_jingle_session(self, jid, sid): - try: - return self.__sessions[(jid, sid)] - except KeyError: - return None + def startVideoIP(self, jid): + jingle = self.get_jingle_session(jid, media='audio') + if jingle: + jingle.add_content('video', JingleVideo(jingle)) + else: + jingle = JingleSession(self, weinitiate=True, jid=jid) + self.add_jingle(jingle) + jingle.add_content('video', JingleVideo(jingle)) + jingle.start_session() + return jingle.sid + + def get_jingle_session(self, jid, sid=None, media=None): + if sid: + if (jid, sid) in self.__sessions: + return self.__sessions[(jid, sid)] + else: + return None + elif media: + if media == 'audio': + cls = JingleVoIP + elif media == 'video': + cls = JingleVideo + else: + return None + for session in self.__sessions.values(): + for content in session.contents.values(): + if isinstance(content, cls): + return session + return None From a57448d083a70d58cf71a2ef21c930b24590254b Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Fri, 25 Sep 2009 15:29:22 +0200 Subject: [PATCH 46/67] fix some missing things in video GUI --- data/glade/message_window.glade | 12 +++++++++++- src/chat_control.py | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/data/glade/message_window.glade b/data/glade/message_window.glade index ad3263915..bafc686ce 100644 --- a/data/glade/message_window.glade +++ b/data/glade/message_window.glade @@ -111,6 +111,16 @@ 3 + + + True + None + 1 + + + 4 + + 11 @@ -120,7 +130,7 @@ - 4 + 5 diff --git a/src/chat_control.py b/src/chat_control.py index 4c980b544..f650d90ef 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1263,7 +1263,9 @@ class ChatControl(ChatControlBase, ChatCommands): else: self.set_video_state('not_available') self.audio_sid = None + self.audio_state = self.JINGLE_STATE_NOT_AVAILABLE self.video_sid = None + self.video_state = self.JINGLE_STATE_NOT_AVAILABLE self.update_toolbar() From a051d1ec952dcf699e60e115bd44083b1e1eb62e Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Fri, 25 Sep 2009 15:47:43 +0200 Subject: [PATCH 47/67] initialize var before they are used --- src/chat_control.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index f650d90ef..97682f2cd 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -50,7 +50,7 @@ from common.logger import constants from common.pep import MOODS, ACTIVITIES from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION -from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO +from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO from commands.implementation import CommonCommands, ChatCommands @@ -1252,6 +1252,10 @@ class ChatControl(ChatControlBase, ChatCommands): self._audio_banner_image = self.xml.get_widget('audio_banner_image') self._video_banner_image = self.xml.get_widget('video_banner_image') + self.audio_sid = None + self.audio_state = self.JINGLE_STATE_NOT_AVAILABLE + self.video_sid = None + self.video_state = self.JINGLE_STATE_NOT_AVAILABLE if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_AUDIO) and \ gajim.HAVE_FARSIGHT: self.set_audio_state('available') @@ -1262,10 +1266,6 @@ class ChatControl(ChatControlBase, ChatCommands): self.set_video_state('available') else: self.set_video_state('not_available') - self.audio_sid = None - self.audio_state = self.JINGLE_STATE_NOT_AVAILABLE - self.video_sid = None - self.video_state = self.JINGLE_STATE_NOT_AVAILABLE self.update_toolbar() From 77541f3e7f78076eeae9de4267d6bd44fa279b25 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Fri, 25 Sep 2009 19:32:13 +0200 Subject: [PATCH 48/67] support for content-{add,reject,accept}, new helpers, and other few things --- src/chat_control.py | 18 +++- src/common/jingle.py | 225 +++++++++++++++++++++++++++++-------------- src/dialogs.py | 18 +++- src/gajim.py | 16 ++- 4 files changed, 196 insertions(+), 81 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 97682f2cd..1f4ded938 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1579,14 +1579,17 @@ class ChatControl(ChatControlBase, ChatCommands): elif state == 'connecting': self.audio_state = self.JINGLE_STATE_CONNECTING self.audio_sid = sid + self._audio_button.set_active(True) elif state == 'connection_received': self.audio_state = self.JINGLE_STATE_CONNECTION_RECEIVED self.audio_sid = sid + self._audio_button.set_active(True) elif state == 'connected': self.audio_state = self.JINGLE_STATE_CONNECTED elif state == 'stop': self.audio_state = self.JINGLE_STATE_AVAILABLE self.audio_sid = None + self._audio_button.set_active(False) elif state == 'error': self.audio_state = self.JINGLE_STATE_ERROR self.update_audio() @@ -1605,15 +1608,18 @@ class ChatControl(ChatControlBase, ChatCommands): self.video_sid = None elif state == 'connecting': self.video_state = self.JINGLE_STATE_CONNECTING + self._video_button.set_active(True) self.video_sid = sid elif state == 'connection_received': self.video_state = self.JINGLE_STATE_CONNECTION_RECEIVED + self._video_button.set_active(True) self.video_sid = sid elif state == 'connected': self.video_state = self.JINGLE_STATE_CONNECTED elif state == 'stop': self.video_state = self.JINGLE_STATE_AVAILABLE self.video_sid = None + self._video_button.set_active(False) elif state == 'error': self.video_state = self.JINGLE_STATE_ERROR self.update_video() @@ -1825,10 +1831,11 @@ class ChatControl(ChatControlBase, ChatCommands): self.set_audio_state('connecting', sid) else: session = gajim.connections[self.account].get_jingle_session( - self.contact.get_full_jid(), self.audio_sid) + self.contact.get_full_jid(), self.video_sid) if session: - # TODO: end only audio - session.end_session() + content = session.get_content('audio') + if content: + session.remove_content(content.creator, content.name) def on_video_button_toggled(self, widget): if widget.get_active(): @@ -1839,8 +1846,9 @@ class ChatControl(ChatControlBase, ChatCommands): session = gajim.connections[self.account].get_jingle_session( self.contact.get_full_jid(), self.video_sid) if session: - # TODO: end only video - session.end_session() + content = session.get_content('video') + if content: + session.remove_content(content.creator, content.name) def _toggle_gpg(self): if not self.gpg_is_active and not self.contact.keyID: diff --git a/src/common/jingle.py b/src/common/jingle.py index 9232ae02c..8e22832e7 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -17,7 +17,10 @@ # - 'senders' attribute of 'content' element # - security preconditions # * actions: -# - content-accept, content-reject, content-add, content-modify +# - content-accept: see content-add +# - content-reject: sending it ; receiving is ok +# - content-add: handling ; sending is ok +# - content-modify: both # - description-info, session-info # - security-info # - transport-accept, transport-reject @@ -88,7 +91,7 @@ class JingleSession(object): # our full jid self.ourjid = gajim.get_jid_from_account(self.connection.name) + '/' + \ con.server_resource - self.peerjid = jid # jid we connect to + self.peerjid = str(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 @@ -107,10 +110,12 @@ class JingleSession(object): # use .prepend() to add new callbacks, especially when you're going # to send error instead of ack self.callbacks = { - 'content-accept': [self.__contentAcceptCB, self.__defaultCB], - 'content-add': [self.__defaultCB], #TODO + '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], #TODO + 'content-reject': [self.__defaultCB, self.__contentRemoveCB], #TODO 'content-remove': [self.__defaultCB, self.__contentRemoveCB], 'description-info': [self.__defaultCB], #TODO 'security-info': [self.__defaultCB], #TODO @@ -133,7 +138,6 @@ class JingleSession(object): def approve_session(self): ''' Called when user accepts session in UI (when we aren't the initiator). ''' - self.accepted = True self.accept_session() def decline_session(self): @@ -154,10 +158,23 @@ class JingleSession(object): ''' 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. - The protocol prohibits changing that when pending. Creator must be one of ('we', 'peer', 'initiator', 'responder')''' assert creator in ('we', 'peer', 'initiator', 'responder') @@ -171,34 +188,53 @@ class JingleSession(object): content.name = name self.contents[(creator, name)] = content - if self.state == JingleStates.active: - pass # TODO: send proper stanza, shouldn't be needed now - def remove_content(self, creator, name): ''' We do not need this now ''' - pass + #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 accept_session(self): - ''' Check if all contents and user agreed to start session. ''' - if not self.weinitiate and self.accepted and \ - self.state == JingleStates.pending and self.is_ready(): - self.__session_accept() + def on_session_state_changed(self, content=None): + if self.state == JingleStates.active and self.accepted: + 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) + elif self.is_ready(): + if not self.weinitiate and self.state == JingleStates.pending: + self.__session_accept() + elif self.weinitiate and self.state == JingleStates.ended: + self.__session_initiate() 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())) + 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): - ''' Start session. ''' - #FIXME: Start only once - if self.weinitiate and self.state == JingleStates.ended and self.is_ready(): - self.__session_initiate() + ''' Mark the session as ready to be started. ''' + self.accepted = True + self.on_session_state_changed() def send_session_info(self): pass @@ -309,10 +345,10 @@ class JingleSession(object): creator = content['creator'] name = content['name'] if (creator, name) in self.contents: - del self.contents[(creator, name)] + self.contents[(creator, name)].destroy() if len(self.contents) == 0: reason = xmpp.Node('reason') - reason.setTag('success') #FIXME: Is it the good one? + reason.setTag('success') self._session_terminate(reason) def __sessionAcceptCB(self, stanza, jingle, error, action): @@ -328,6 +364,27 @@ class JingleSession(object): creator = content['creator'] name = content['name']#TODO... + def __contentAddCB(self, stanza, jingle, error, action): + #TODO: Needs to be rewritten + if self.state == JingleStates.ended: + raise OutOfOrder + for element in jingle.iterTags('content'): + # checking what kind of session this will be + desc_ns = element.getTag('description').getNamespace() + media = element.getTag('description')['media'] + tran_ns = element.getTag('transport').getNamespace() + if desc_ns == xmpp.NS_JINGLE_RTP and media in ('audio', 'video') \ + and tran_ns == xmpp.NS_JINGLE_ICE_UDP: + if media == 'audio': + self.add_content(element['name'], JingleVoIP(self), 'peer') + else: + self.add_content(element['name'], JingleVideo(self), 'peer') + else: + content = JingleContent() + self.add_content(element['name'], content, 'peer') + self.__content_reject(content) + self.contents[(content.creator, content.name)].destroy() + def __sessionInitiateCB(self, stanza, jingle, error, action): ''' We got a jingle session request from other entity, therefore we are the receiver... Unpack the data, @@ -511,20 +568,40 @@ class JingleSession(object): text = '%s (%s)' % (reason, text) else: text = reason - self.connection.dispatch('JINGLE_DISCONNECTED', (self.peerjid, self.sid, text)) self.connection.delete_jingle(self) + self.connection.dispatch('JINGLE_DISCONNECTED', (self.peerjid, self.sid, text)) - def __content_add(self): - assert self.state == JingleStates.active - - def __content_accept(self): + 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) def __content_modify(self): assert self.state != JingleStates.ended - def __content_remove(self): + 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: dispatch something? def content_negociated(self, media): self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid, @@ -554,8 +631,8 @@ class JingleContent(object): self.callbacks = { # these are called when *we* get stanzas - 'content-accept': [], - 'content-add': [], + 'content-accept': [self.__transportInfoCB], + 'content-add': [self.__transportInfoCB], 'content-modify': [], 'content-remove': [], 'session-accept': [self.__transportInfoCB], @@ -566,6 +643,8 @@ class JingleContent(object): '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': [], @@ -676,6 +755,11 @@ class JingleContent(object): 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) @@ -687,6 +771,7 @@ class JingleRTPContent(JingleContent): self.candidates_ready = False # True when local candidates are prepared self.callbacks['content-accept'] += [self.__getRemoteCodecsCB] + self.callbacks['content-add'] += [self.__getRemoteCodecsCB] self.callbacks['session-accept'] += [self.__getRemoteCodecsCB] self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB] self.callbacks['session-terminate'] += [self.__stop] @@ -718,21 +803,32 @@ class JingleRTPContent(JingleContent): 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() - #print name if name == 'farsight-new-active-candidate-pair': pass elif name == 'farsight-recv-codecs-changed': pass elif name == 'farsight-codecs-changed': - self.session.accept_session() - self.session.start_session() + if self.is_ready(): + self.session.on_session_state_changed(self) elif name == 'farsight-local-candidates-prepared': self.candidates_ready = True - self.session.accept_session() - self.session.start_session() + 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) @@ -751,9 +847,6 @@ class JingleRTPContent(JingleContent): #TODO: farsight.DIRECTION_BOTH only if senders='both' self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) self.session.content_negociated(self.media) - #if not self.session.weinitiate: #FIXME: one more FIXME... - # self.session.send_content_accept(self.__content((xmpp.Node( - # 'description', payload=self.iter_codecs()),))) elif name == 'farsight-error': print 'Farsight error #%d!' % message.structure['error-no'] print 'Message: %s' % message.structure['error-msg'] @@ -805,6 +898,11 @@ class JingleRTPContent(JingleContent): 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 @@ -830,8 +928,8 @@ class JingleVoIP(JingleRTPContent): # the local parts # TODO: use gconfaudiosink? # sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink']) - sink = gst.element_factory_make('alsasink') - sink.set_property('sync', False) + 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) @@ -843,21 +941,12 @@ class JingleVoIP(JingleRTPContent): self.mic_volume.set_property('volume', 1) # link gst elements - self.pipeline.add(sink, src_mic, self.mic_volume) + self.pipeline.add(self.sink, src_mic, self.mic_volume) src_mic.link(self.mic_volume) - def src_pad_added (stream, pad, codec): - if not self.funnel: - self.funnel = gst.element_factory_make('fsfunnel') - self.pipeline.add(self.funnel) - self.funnel.set_state (gst.STATE_PLAYING) - sink.set_state (gst.STATE_PLAYING) - self.funnel.link(sink) - pad.link(self.funnel.get_pad('sink%d')) - self.mic_volume.get_pad('src').link(self.p2psession.get_property( 'sink-pad')) - self.p2pstream.connect('src-pad-added', src_pad_added) + 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) @@ -875,7 +964,7 @@ class JingleVideo(JingleRTPContent): # sometimes it'll freeze... JingleRTPContent.setup_stream(self) # the local parts - src_vid = gst.element_factory_make('v4l2src') + src_vid = gst.element_factory_make('videotestsrc') 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')) @@ -884,19 +973,11 @@ class JingleVideo(JingleRTPContent): self.pipeline.add(src_vid, videoscale, caps, colorspace) gst.element_link_many(src_vid, videoscale, caps, colorspace) - def src_pad_added (stream, pad, codec): - if not self.funnel: - self.funnel = gst.element_factory_make('fsfunnel') - self.pipeline.add(self.funnel) - videosink = gst.element_factory_make('xvimagesink') - self.pipeline.add(videosink) - self.funnel.set_state (gst.STATE_PLAYING) - videosink.set_state(gst.STATE_PLAYING) - self.funnel.link(videosink) - pad.link(self.funnel.get_pad('sink%d')) + 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', src_pad_added) + 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) @@ -953,6 +1034,8 @@ class ConnectionJingle(object): raise xmpp.NodeProcessed def startVoIP(self, jid): + if self.get_jingle_session(jid, media='audio'): + return jingle = self.get_jingle_session(jid, media='video') if jingle: jingle.add_content('voice', JingleVoIP(jingle)) @@ -964,6 +1047,8 @@ class ConnectionJingle(object): return jingle.sid def startVideoIP(self, jid): + if self.get_jingle_session(jid, media='video'): + return jingle = self.get_jingle_session(jid, media='audio') if jingle: jingle.add_content('video', JingleVideo(jingle)) @@ -981,14 +1066,10 @@ class ConnectionJingle(object): else: return None elif media: - if media == 'audio': - cls = JingleVoIP - elif media == 'video': - cls = JingleVideo - else: + if media not in ('audio', 'video'): return None for session in self.__sessions.values(): - for content in session.contents.values(): - if isinstance(content, cls): - return session + if session.peerjid == jid and session.get_content(media): + return session + return None diff --git a/src/dialogs.py b/src/dialogs.py index ea2c86cfe..0ad73314b 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -32,6 +32,7 @@ import gtk import gobject import os +from weakref import WeakValueDictionary import gtkgui_helpers import vcard @@ -4442,11 +4443,15 @@ class GPGInfoWindow: self.window.destroy() class VoIPCallReceivedDialog(object): + instances = WeakValueDictionary() + def __init__(self, account, contact_jid, sid): self.account = account self.fjid = contact_jid self.sid = sid + self.instances[(contact_jid, sid)] = self + xml = gtkgui_helpers.get_glade('voip_call_received_dialog.glade') xml.signal_autoconnect(self) @@ -4461,9 +4466,17 @@ class VoIPCallReceivedDialog(object): dialog = xml.get_widget('voip_call_received_messagedialog') dialog.set_property('secondary-text', dialog.get_property('secondary-text') % {'contact': contact_text}) + self._dialog = dialog dialog.show_all() + @classmethod + def get_dialog(cls, jid, sid): + if (jid, sid) in cls.instances: + return cls.instances[(jid, sid)] + else: + return None + def on_voip_call_received_messagedialog_close(self, dialog): return self.on_voip_call_received_messagedialog_response(dialog, gtk.RESPONSE_NO) @@ -4487,7 +4500,10 @@ class VoIPCallReceivedDialog(object): if not contact: return ctrl = gajim.interface.new_chat(contact, self.account) - ctrl.set_audio_state('connecting', self.sid) + if session.get_content('audio'): + ctrl.set_audio_state('connecting', self.sid) + if session.get_content('video'): + ctrl.set_video_state('connecting', self.sid) else: # response==gtk.RESPONSE_NO session.decline_session() diff --git a/src/gajim.py b/src/gajim.py index 5a55dfc8f..cc4abe2b5 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -2119,7 +2119,10 @@ class Interface: if not ctrl: ctrl = self.msg_win_mgr.get_control(jid, account) if ctrl: - ctrl.set_audio_state('connection_received', sid) + if 'audio' in content_types: + ctrl.set_audio_state('connection_received', sid) + if 'video' in content_types: + ctrl.set_video_state('connection_received', sid) if helpers.allow_popup_window(account): dialogs.VoIPCallReceivedDialog(account, peerjid, sid) @@ -2141,14 +2144,17 @@ class Interface: def handle_event_jingle_connected(self, account, data): # ('JINGLE_CONNECTED', account, (peerjid, sid, media)) peerjid, sid, media = data - if media == 'audio': + if media in ('audio', 'video'): jid = gajim.get_jid_without_resource(peerjid) resource = gajim.get_resource_from_jid(peerjid) ctrl = self.msg_win_mgr.get_control(peerjid, account) if not ctrl: ctrl = self.msg_win_mgr.get_control(jid, account) if ctrl: - ctrl.set_audio_state('connected', sid) + if media == 'audio': + ctrl.set_audio_state('connected', sid) + else: + ctrl.set_video_state('connected', sid) def handle_event_jingle_disconnected(self, account, data): # ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason)) @@ -2160,6 +2166,10 @@ class Interface: ctrl = self.msg_win_mgr.get_control(jid, account) if ctrl: ctrl.set_audio_state('stop', sid=sid, reason=reason) + ctrl.set_video_state('stop', sid=sid, reason=reason) + dialog = dialogs.VoIPCallReceivedDialog.get_dialog(peerjid, sid) + if dialog: + dialog._dialog.destroy() def handle_event_jingle_error(self, account, data): # ('JINGLE_ERROR', account, (peerjid, sid, reason)) From 20755acedc5aa9510facf0d90539e8886c3da6d3 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Fri, 25 Sep 2009 20:40:53 +0200 Subject: [PATCH 49/67] fix typo --- src/chat_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat_control.py b/src/chat_control.py index 1f4ded938..ef77870ba 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1831,7 +1831,7 @@ class ChatControl(ChatControlBase, ChatCommands): self.set_audio_state('connecting', sid) else: session = gajim.connections[self.account].get_jingle_session( - self.contact.get_full_jid(), self.video_sid) + self.contact.get_full_jid(), self.audio_sid) if session: content = session.get_content('audio') if content: From 5f70d6b78e0f1e44b019039c83f8d6565895efec Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Fri, 25 Sep 2009 20:41:22 +0200 Subject: [PATCH 50/67] use a str instance instead of a xmpp.JID instance --- src/common/jingle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index ee909890f..7d7d0677e 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -91,7 +91,7 @@ class JingleSession(object): # our full jid self.ourjid = gajim.get_jid_from_account(self.connection.name) + '/' + \ con.server_resource - self.peerjid = str(jid) # jid we connect to + 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 @@ -1023,7 +1023,7 @@ class ConnectionJingle(object): route it adequatelly.''' # get data - jid = stanza.getFrom() + jid = helpers.get_full_jid_from_iq(stanza) id = stanza.getID() if (jid, id) in self.__iq_responses.keys(): From a8bccebfb78bd0948fd3ab81436a40b66b4cca43 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Fri, 25 Sep 2009 23:05:14 +0200 Subject: [PATCH 51/67] Added approve_content and decline_content methods. A content has now to be accepted (user-side) before {content,session}-accept --- src/common/jingle.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 8e22832e7..ee909890f 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -147,6 +147,18 @@ class JingleSession(object): 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: + content.destroy() + self.on_session_state_changed() + def end_session(self): ''' Called when user stops or cancel session in UI. ''' reason = xmpp.Node('reason') @@ -623,7 +635,7 @@ class JingleContent(object): # (a JingleContent not added to session shouldn't send anything) #self.creator = None #self.name = None - self.negotiated = False # is this content already negotiated? + self.accepted = False self.candidates = [] # Local transport candidates self.senders = 'both' #FIXME @@ -653,7 +665,8 @@ class JingleContent(object): def is_ready(self): #print '[%s] %s, %s' % (self.media, self.candidates_ready, # self.p2psession.get_property('codecs-ready')) - return self.candidates_ready and self.p2psession.get_property('codecs-ready') + return (self.accepted and self.candidates_ready + 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. ''' @@ -843,7 +856,6 @@ class JingleRTPContent(JingleContent): reason.setTag('failed-transport') self.session._session_terminate(reason) elif state == farsight.STREAM_STATE_READY: - self.negotiated = True #TODO: farsight.DIRECTION_BOTH only if senders='both' self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) self.session.content_negociated(self.media) From 011f4fe142f811ce45edc240449c67f184ce022f Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Fri, 25 Sep 2009 23:52:39 +0200 Subject: [PATCH 52/67] Auto-accept contents we have created (it would make no sense to refuse it...) --- src/common/jingle.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/jingle.py b/src/common/jingle.py index ee909890f..c02d90a57 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -200,6 +200,10 @@ class JingleSession(object): 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: From a8eedfc7817addb526e29a65220a11b55bfa8889 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sat, 26 Sep 2009 10:29:08 +0200 Subject: [PATCH 53/67] ability to accept contents and not only session --- data/glade/voip_call_received_dialog.glade | 2 +- src/common/events.py | 2 +- src/common/jingle.py | 1 + src/dialogs.py | 53 ++++++++++++++++------ src/gajim.py | 28 +++++++----- 5 files changed, 59 insertions(+), 27 deletions(-) diff --git a/data/glade/voip_call_received_dialog.glade b/data/glade/voip_call_received_dialog.glade index d2afca0a9..b4314fe22 100644 --- a/data/glade/voip_call_received_dialog.glade +++ b/data/glade/voip_call_received_dialog.glade @@ -13,7 +13,7 @@ yes-no <b><big>Incoming call</big></b> True - %(contact)s wants to start a voice chat with you. Do you want to answer the call? + diff --git a/src/common/events.py b/src/common/events.py index 88593cf45..c69fc27d1 100644 --- a/src/common/events.py +++ b/src/common/events.py @@ -46,7 +46,7 @@ class Event: gc-invitation: [room_jid, reason, password, is_continued] subscription_request: [text, nick] unsubscribed: contact - jingle-*: (fulljid, sessionid) + jingle-incoming: (fulljid, sessionid, content_types) ''' self.type_ = type_ self.time_ = time_ diff --git a/src/common/jingle.py b/src/common/jingle.py index 7d7d0677e..2f533dd2b 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -54,6 +54,7 @@ import gajim import xmpp +import helpers import farsight, gst diff --git a/src/dialogs.py b/src/dialogs.py index 0ad73314b..b96220433 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -4443,14 +4443,14 @@ class GPGInfoWindow: self.window.destroy() class VoIPCallReceivedDialog(object): - instances = WeakValueDictionary() + instances = {} - def __init__(self, account, contact_jid, sid): + def __init__(self, account, contact_jid, sid, content_types): + self.instances[(contact_jid, sid)] = self self.account = account self.fjid = contact_jid self.sid = sid - - self.instances[(contact_jid, sid)] = self + self.content_types = content_types xml = gtkgui_helpers.get_glade('voip_call_received_dialog.glade') xml.signal_autoconnect(self) @@ -4458,17 +4458,14 @@ class VoIPCallReceivedDialog(object): jid = gajim.get_jid_without_resource(self.fjid) contact = gajim.contacts.get_first_contact_from_jid(account, jid) if contact and contact.name: - contact_text = '%s (%s)' % (contact.name, jid) + self.contact_text = '%s (%s)' % (contact.name, jid) else: - contact_text = contact_jid + self.contact_text = contact_jid - # do the substitution - dialog = xml.get_widget('voip_call_received_messagedialog') - dialog.set_property('secondary-text', - dialog.get_property('secondary-text') % {'contact': contact_text}) - self._dialog = dialog + self.dialog = xml.get_widget('voip_call_received_messagedialog') + self.set_secondary_text() - dialog.show_all() + self.dialog.show_all() @classmethod def get_dialog(cls, jid, sid): @@ -4477,6 +4474,29 @@ class VoIPCallReceivedDialog(object): else: return None + def set_secondary_text(self): + if 'audio' in self.content_types and 'video' in self.content_types: + types_text = _('an audio and video') + elif 'audio' in self.content_types: + types_text = _('an audio') + elif 'video' in self.content_types: + types_text = _('a video') + + # do the substitution + self.dialog.set_property('secondary-text', + _('%(contact)s wants to start %(type)s session with you. Do you want ' + 'to answer the call?') % {'contact': self.contact_text, 'type': types_text}) + + def add_contents(self, content_types): + for type_ in content_types: + if type_ not in self.content_types: + self.content_types.append(type_) + self.set_secondary_text() + + def on_voip_call_received_messagedialog_destroy(self, dialog): + if (self.fjid, self.sid) in self.instances: + del self.instances[(self.fjid, self.sid)] + def on_voip_call_received_messagedialog_close(self, dialog): return self.on_voip_call_received_messagedialog_response(dialog, gtk.RESPONSE_NO) @@ -4485,8 +4505,13 @@ class VoIPCallReceivedDialog(object): # we've got response from user, either stop connecting or accept the call session = gajim.connections[self.account].get_jingle_session(self.fjid, self.sid) - if response==gtk.RESPONSE_YES: - session.approve_session() + if not session: + return + if response == gtk.RESPONSE_YES: + if not session.accepted: + session.approve_session() + for content in self.content_types: + session.approve_content(content) jid = gajim.get_jid_without_resource(self.fjid) resource = gajim.get_resource_from_jid(self.fjid) ctrl = gajim.interface.msg_win_mgr.get_control(self.fjid, self.account) diff --git a/src/gajim.py b/src/gajim.py index cc4abe2b5..15733dc81 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -2098,7 +2098,7 @@ class Interface: # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type, # data...)) # TODO: conditional blocking if peer is not in roster - + # unpack data peerjid, sid, contents = data content_types = set(c[0] for c in contents) @@ -2124,11 +2124,17 @@ class Interface: if 'video' in content_types: ctrl.set_video_state('connection_received', sid) - if helpers.allow_popup_window(account): - dialogs.VoIPCallReceivedDialog(account, peerjid, sid) + dlg = dialogs.VoIPCallReceivedDialog.get_dialog(peerjid, sid) + if dlg: + dlg.add_contents(content_types) return - self.add_event(account, peerjid, 'voip-incoming', (peerjid, sid,)) + if helpers.allow_popup_window(account): + dialogs.VoIPCallReceivedDialog(account, peerjid, sid, content_types) + return + + self.add_event(account, peerjid, 'jingle-incoming', (peerjid, sid, + content_types)) if helpers.allow_showing_notification(account): # TODO: we should use another pixmap ;-) @@ -2138,7 +2144,7 @@ class Interface: account, peerjid) path = gtkgui_helpers.get_path_to_generic_or_avatar(img) event_type = _('Voice Chat Request') - notify.popup(event_type, peerjid, account, 'voip-incoming', + notify.popup(event_type, peerjid, account, 'jingle-incoming', path_to_image = path, title = event_type, text = txt) def handle_event_jingle_connected(self, account, data): @@ -2169,7 +2175,7 @@ class Interface: ctrl.set_video_state('stop', sid=sid, reason=reason) dialog = dialogs.VoIPCallReceivedDialog.get_dialog(peerjid, sid) if dialog: - dialog._dialog.destroy() + dialog.dialog.destroy() def handle_event_jingle_error(self, account, data): # ('JINGLE_ERROR', account, (peerjid, sid, reason)) @@ -2457,7 +2463,7 @@ class Interface: jid = gajim.get_jid_without_resource(jid) no_queue = len(gajim.events.get_events(account, jid)) == 0 # type_ can be gc-invitation file-send-error file-error file-request-error - # file-request file-completed file-stopped voip-incoming + # file-request file-completed file-stopped jingle-incoming # event_type can be in advancedNotificationWindow.events_list event_types = {'file-request': 'ft_request', 'file-completed': 'ft_finished'} @@ -2617,10 +2623,10 @@ class Interface: self.show_unsubscribed_dialog(account, contact) gajim.events.remove_events(account, jid, event) self.roster.draw_contact(jid, account) - elif type_ == 'voip-incoming': - event = gajim.events.get_first_event(account, jid, type_) - peerjid, sid = event.parameters - dialogs.VoIPCallReceivedDialog(account, peerjid, sid) + elif type_ == 'jingle-incoming': + event = gajim.events.get_first_event(account, jid, type_) + peerjid, sid, content_types = event.parameters + dialogs.VoIPCallReceivedDialog(account, peerjid, sid, content_types) gajim.events.remove_events(account, jid, event) if w: w.set_active_tab(ctrl) From 193f2613a939875ad9c5fef9ed5ebbd562ab0718 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sat, 26 Sep 2009 12:22:41 +0200 Subject: [PATCH 54/67] Rewritten contentAddCB in order to ask the user about new contents --- src/common/jingle.py | 96 ++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 43 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index c02d90a57..a358cd179 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -384,22 +384,20 @@ class JingleSession(object): #TODO: Needs to be rewritten if self.state == JingleStates.ended: raise OutOfOrder - for element in jingle.iterTags('content'): - # checking what kind of session this will be - desc_ns = element.getTag('description').getNamespace() - media = element.getTag('description')['media'] - tran_ns = element.getTag('transport').getNamespace() - if desc_ns == xmpp.NS_JINGLE_RTP and media in ('audio', 'video') \ - and tran_ns == xmpp.NS_JINGLE_ICE_UDP: - if media == 'audio': - self.add_content(element['name'], JingleVoIP(self), 'peer') - else: - self.add_content(element['name'], JingleVideo(self), 'peer') - else: - content = JingleContent() - self.add_content(element['name'], content, 'peer') - self.__content_reject(content) - self.contents[(content.creator, content.name)].destroy() + + 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.initiator, self.sid, + contents)) def __sessionInitiateCB(self, stanza, jingle, error, action): ''' We got a jingle session request from other entity, @@ -419,24 +417,7 @@ class JingleSession(object): # error. # Lets check what kind of jingle session does the peer want - contents = [] - contents_ok = False - transports_ok = False - for element in jingle.iterTags('content'): - # checking what kind of session this will be - desc_ns = element.getTag('description').getNamespace() - media = element.getTag('description')['media'] - tran_ns = element.getTag('transport').getNamespace() - if desc_ns == xmpp.NS_JINGLE_RTP and media in ('audio', 'video'): - contents_ok = True - if tran_ns == xmpp.NS_JINGLE_ICE_UDP: - # we've got voip content - if media == 'audio': - self.add_content(element['name'], JingleVoIP(self), 'peer') - else: - self.add_content(element['name'], JingleVideo(self), 'peer') - contents.append((media,)) - transports_ok = True + contents_ok, transports_ok, contents, pouet = self.__parse_contents(jingle) # If there's no content we understand... if not contents_ok: @@ -485,15 +466,34 @@ class JingleSession(object): for content in self.contents.itervalues(): content.stanzaCB(stanza, None, error, action) - 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) + ''' Internal methods. ''' + def __parse_contents(self, jingle): + #TODO: WIP + 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: @@ -533,6 +533,16 @@ class JingleSession(object): 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) From 08b7f18f50bd88c7e2d4d5b8da392e80ee944079 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sat, 26 Sep 2009 12:31:56 +0200 Subject: [PATCH 55/67] Fix content reject --- src/common/jingle.py | 2 ++ src/dialogs.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 8632350b2..9795f3db7 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -157,6 +157,8 @@ class JingleSession(object): 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() diff --git a/src/dialogs.py b/src/dialogs.py index b96220433..a6bfeafc3 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -4530,7 +4530,11 @@ class VoIPCallReceivedDialog(object): if session.get_content('video'): ctrl.set_video_state('connecting', self.sid) else: # response==gtk.RESPONSE_NO - session.decline_session() + if not session.accepted: + session.decline_session() + else: + for content in self.content_types: + session.reject_content(content) dialog.destroy() From b5c7519740f402a4c7d356be738e109ec727da94 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sat, 26 Sep 2009 14:14:58 +0200 Subject: [PATCH 56/67] A little fix with content acceptance ; modified a bit JINGLE_DISCONNECTED --- src/common/jingle.py | 20 ++++++++++++++------ src/gajim.py | 8 +++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 9795f3db7..5b910372e 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -364,7 +364,11 @@ class JingleSession(object): creator = content['creator'] name = content['name'] if (creator, name) in self.contents: - self.contents[(creator, name)].destroy() + 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') @@ -399,7 +403,7 @@ class JingleSession(object): self.__content_reject(content) self.contents[(content.creator, content.name)].destroy() - self.connection.dispatch('JINGLE_INCOMING', (self.initiator, self.sid, + self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, contents)) def __sessionInitiateCB(self, stanza, jingle, error, action): @@ -442,7 +446,7 @@ class JingleSession(object): self.state = JingleStates.pending # Send event about starting a session - self.connection.dispatch('JINGLE_INCOMING', (self.initiator, self.sid, + self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, contents)) def __broadcastCB(self, stanza, jingle, error, action): @@ -462,7 +466,8 @@ class JingleSession(object): text = '%s (%s)' % (reason, text) else: text = reason#TODO - self.connection.dispatch('JINGLE_DISCONNECTED', (self.peerjid, self.sid, text)) + 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. ''' @@ -598,7 +603,8 @@ class JingleSession(object): else: text = reason self.connection.delete_jingle(self) - self.connection.dispatch('JINGLE_DISCONNECTED', (self.peerjid, self.sid, text)) + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, None, text)) def __content_add(self, content): #TODO: test @@ -630,7 +636,9 @@ class JingleSession(object): stanza, jingle = self.__make_jingle('content-remove') self.__append_content(jingle, content) self.connection.connection.send(stanza) - #TODO: dispatch something? + #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, diff --git a/src/gajim.py b/src/gajim.py index 15733dc81..5f2413576 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -2164,15 +2164,17 @@ class Interface: def handle_event_jingle_disconnected(self, account, data): # ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason)) - peerjid, sid, reason = data + peerjid, sid, media, reason = data jid = gajim.get_jid_without_resource(peerjid) resource = gajim.get_resource_from_jid(peerjid) ctrl = self.msg_win_mgr.get_control(peerjid, account) if not ctrl: ctrl = self.msg_win_mgr.get_control(jid, account) if ctrl: - ctrl.set_audio_state('stop', sid=sid, reason=reason) - ctrl.set_video_state('stop', sid=sid, reason=reason) + if media in ('audio', None): + ctrl.set_audio_state('stop', sid=sid, reason=reason) + if media in ('video', None): + ctrl.set_video_state('stop', sid=sid, reason=reason) dialog = dialogs.VoIPCallReceivedDialog.get_dialog(peerjid, sid) if dialog: dialog.dialog.destroy() From 5a1a36e348c6179e476b4e6b0e16dd7fe029b3d3 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sat, 26 Sep 2009 21:55:43 +0200 Subject: [PATCH 57/67] Fix somes issues with content-add/content-accept/session-accept, and other things. --- src/common/jingle.py | 37 +++++++++++++++++++++++-------------- src/dialogs.py | 2 +- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 5b910372e..92ae64091 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -17,10 +17,7 @@ # - 'senders' attribute of 'content' element # - security preconditions # * actions: -# - content-accept: see content-add -# - content-reject: sending it ; receiving is ok -# - content-add: handling ; sending is ok -# - content-modify: both +# - content-modify # - description-info, session-info # - security-info # - transport-accept, transport-reject @@ -223,7 +220,20 @@ class JingleSession(object): pass def on_session_state_changed(self, content=None): - if self.state == JingleStates.active and self.accepted: + 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: @@ -232,11 +242,6 @@ class JingleSession(object): else: # The other side created this content, we accept it. self.__content_accept(content) - elif self.is_ready(): - if not self.weinitiate and self.state == JingleStates.pending: - self.__session_accept() - elif self.weinitiate and self.state == JingleStates.ended: - self.__session_initiate() def is_ready(self): ''' Returns True when all codecs and candidates are ready @@ -388,7 +393,6 @@ class JingleSession(object): name = content['name']#TODO... def __contentAddCB(self, stanza, jingle, error, action): - #TODO: Needs to be rewritten if self.state == JingleStates.ended: raise OutOfOrder @@ -476,7 +480,7 @@ class JingleSession(object): ''' Internal methods. ''' def __parse_contents(self, jingle): - #TODO: WIP + #TODO: Needs some reworking contents = [] contents_rejected = [] contents_ok = False @@ -627,6 +631,9 @@ class JingleSession(object): 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 @@ -734,6 +741,7 @@ class JingleContent(object): # Instead, it should be etablished after session-accept! if len(candidates) > 0: 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. ''' @@ -1002,6 +1010,7 @@ class JingleVideo(JingleRTPContent): 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')) @@ -1072,7 +1081,7 @@ class ConnectionJingle(object): def startVoIP(self, jid): if self.get_jingle_session(jid, media='audio'): - return + 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)) @@ -1085,7 +1094,7 @@ class ConnectionJingle(object): def startVideoIP(self, jid): if self.get_jingle_session(jid, media='video'): - return + return self.get_jingle_session(jid, media='video').sid jingle = self.get_jingle_session(jid, media='audio') if jingle: jingle.add_content('video', JingleVideo(jingle)) diff --git a/src/dialogs.py b/src/dialogs.py index a6bfeafc3..6c665acb9 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -4490,7 +4490,7 @@ class VoIPCallReceivedDialog(object): def add_contents(self, content_types): for type_ in content_types: if type_ not in self.content_types: - self.content_types.append(type_) + self.content_types.add(type_) self.set_secondary_text() def on_voip_call_received_messagedialog_destroy(self, dialog): From e141d7875af8d381f284a1b27c4a0aeb7db900f2 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sat, 26 Sep 2009 23:07:09 +0200 Subject: [PATCH 58/67] Don't send audio/video data before session acceptance. --- src/common/jingle.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 92ae64091..fe53b8186 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -816,10 +816,14 @@ class JingleRTPContent(JingleContent): self.candidates_ready = False # True when local candidates are prepared - self.callbacks['content-accept'] += [self.__getRemoteCodecsCB] - self.callbacks['content-add'] += [self.__getRemoteCodecsCB] - self.callbacks['session-accept'] += [self.__getRemoteCodecsCB] 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] @@ -888,10 +892,6 @@ class JingleRTPContent(JingleContent): reason = xmpp.Node('reason') reason.setTag('failed-transport') self.session._session_terminate(reason) - elif state == farsight.STREAM_STATE_READY: - #TODO: farsight.DIRECTION_BOTH only if senders='both' - self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) - self.session.content_negociated(self.media) elif name == 'farsight-error': print 'Farsight error #%d!' % message.structure['error-no'] print 'Message: %s' % message.structure['error-msg'] @@ -899,6 +899,12 @@ class JingleRTPContent(JingleContent): else: print name + def __contentAcceptCB(self, stanza, content, error, action): + if self.accepted: + #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: From 85195e99ad016b9cdbabb266d4e7df83cf93601e Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sun, 27 Sep 2009 22:56:09 +0200 Subject: [PATCH 59/67] workaround for psi, little fixes... --- src/common/jingle.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index fe53b8186..489f42ec7 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -119,7 +119,7 @@ class JingleSession(object): 'security-info': [self.__defaultCB], #TODO 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], - 'session-info': [self.__sessionInfoCB, self.__broadcastCB], + 'session-info': [self.__sessionInfoCB, self.__broadcastCB, self.__defaultCB], 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, self.__defaultCB], 'session-terminate': [self.__sessionTerminateCB, self.__broadcastAllCB, @@ -359,6 +359,7 @@ class JingleSession(object): 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') @@ -668,6 +669,7 @@ class JingleContent(object): #self.creator = None #self.name = None self.accepted = False + self.sent = False self.candidates = [] # Local transport candidates self.senders = 'both' #FIXME @@ -697,7 +699,7 @@ class JingleContent(object): 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 + 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): @@ -792,6 +794,8 @@ class JingleContent(object): ''' 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, @@ -843,6 +847,9 @@ class JingleRTPContent(JingleContent): 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} @@ -968,12 +975,15 @@ class JingleVoIP(JingleRTPContent): JingleRTPContent.setup_stream(self) # Configure SPEEX - #FIXME: codec ID is an important thing for psi (and pidgin?) - # So, if it doesn't work with pidgin or psi, LOOK AT THIS + # 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, 8000), + farsight.MEDIA_TYPE_AUDIO, 16000), farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', - farsight.MEDIA_TYPE_AUDIO, 16000)] + farsight.MEDIA_TYPE_AUDIO, 8000)] self.p2psession.set_codec_preferences(codecs) # the local parts From b4d2227662d5a59f1c4425747884274f2e1a400a Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Mon, 28 Sep 2009 22:23:48 +0200 Subject: [PATCH 60/67] Fixed a typo ; Prepared tie breaking and content-info handling --- src/common/jingle.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 489f42ec7..79c54a152 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -115,7 +115,7 @@ class JingleSession(object): 'content-modify': [self.__defaultCB], #TODO 'content-reject': [self.__defaultCB, self.__contentRemoveCB], #TODO 'content-remove': [self.__defaultCB, self.__contentRemoveCB], - 'description-info': [self.__defaultCB], #TODO + 'description-info': [self.__broadcastCB, self.__defaultCB], #TODO 'security-info': [self.__defaultCB], #TODO 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], @@ -680,12 +680,18 @@ class JingleContent(object): '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 @@ -738,6 +744,8 @@ class JingleContent(object): '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! @@ -753,7 +761,7 @@ class JingleContent(object): def __candidate(self, candidate): types = {farsight.CANDIDATE_TYPE_HOST: 'host', - farsight.CANDIDATE_TYPE_SRFLX: 'srlfx', + farsight.CANDIDATE_TYPE_SRFLX: 'srflx', farsight.CANDIDATE_TYPE_PRFLX: 'prlfx', farsight.CANDIDATE_TYPE_RELAY: 'relay', farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} @@ -882,6 +890,7 @@ class JingleRTPContent(JingleContent): 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(): @@ -1087,6 +1096,7 @@ class ConnectionJingle(object): # do we need to create a new jingle object if (jid, sid) not in self.__sessions: + #TODO: tie-breaking and other things... newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) self.add_jingle(newjingle) From 10a6b0683c1959ed8c93dd0ac7b3c18ebbdb16b7 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Mon, 28 Sep 2009 22:31:28 +0200 Subject: [PATCH 61/67] Second typo corrected... I need to sleep... --- src/common/jingle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/jingle.py b/src/common/jingle.py index 79c54a152..417e024ab 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -762,7 +762,7 @@ class JingleContent(object): def __candidate(self, candidate): types = {farsight.CANDIDATE_TYPE_HOST: 'host', farsight.CANDIDATE_TYPE_SRFLX: 'srflx', - farsight.CANDIDATE_TYPE_PRFLX: 'prlfx', + farsight.CANDIDATE_TYPE_PRFLX: 'prflx', farsight.CANDIDATE_TYPE_RELAY: 'relay', farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} attrs = { From b2b8ac4b7680dd425ebe02a3002d4ded41705a00 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sat, 3 Oct 2009 22:40:12 +0200 Subject: [PATCH 62/67] Connect only if user accepts, move jingle detection to 'update_toolbar' This allows jingle availability to be updated if contact sign in/out. This patch will also wait for user acceptance before connecting. This will, among other things, ensure that audio/video state won't be set to JINGLE_STATE_CONNECTING while the connection is already up. --- src/chat_control.py | 64 +++++++++++++++++++++++++++++--------------- src/common/jingle.py | 12 +++++++-- src/dialogs.py | 12 ++++++--- 3 files changed, 60 insertions(+), 28 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index ef77870ba..e66ff82ba 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -50,7 +50,7 @@ from common.logger import constants from common.pep import MOODS, ACTIVITIES from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION -from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO +from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_JINGLE_ICE_UDP from commands.implementation import CommonCommands, ChatCommands @@ -1256,16 +1256,6 @@ class ChatControl(ChatControlBase, ChatCommands): self.audio_state = self.JINGLE_STATE_NOT_AVAILABLE self.video_sid = None self.video_state = self.JINGLE_STATE_NOT_AVAILABLE - if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_AUDIO) and \ - gajim.HAVE_FARSIGHT: - self.set_audio_state('available') - else: - self.set_audio_state('not_available') - if gajim.capscache.is_supported(contact, NS_JINGLE_RTP_VIDEO) and \ - gajim.HAVE_FARSIGHT: - self.set_video_state('available') - else: - self.set_video_state('not_available') self.update_toolbar() @@ -1375,6 +1365,26 @@ class ChatControl(ChatControlBase, ChatCommands): else: self._add_to_roster_button.hide() + # Jingle detection + if gajim.capscache.is_supported(self.contact, NS_JINGLE_ICE_UDP) and \ + gajim.HAVE_FARSIGHT and self.contact.resource: + if gajim.capscache.is_supported(self.contact, NS_JINGLE_RTP_AUDIO): + if self.audio_state == self.JINGLE_STATE_NOT_AVAILABLE: + self.set_audio_state('available') + else: + self.set_audio_state('not_available') + + if gajim.capscache.is_supported(self.contact, NS_JINGLE_RTP_VIDEO): + if self.video_state == self.JINGLE_STATE_NOT_AVAILABLE: + self.set_video_state('available') + else: + self.set_video_state('not_available') + else: + if self.audio_state != self.JINGLE_STATE_NOT_AVAILABLE: + self.set_audio_state('not_available') + if self.video_state != self.JINGLE_STATE_NOT_AVAILABLE: + self.set_video_state('not_available') + # Audio buttons if self.audio_state == self.JINGLE_STATE_NOT_AVAILABLE: self._audio_button.set_sensitive(False) @@ -1579,22 +1589,26 @@ class ChatControl(ChatControlBase, ChatCommands): elif state == 'connecting': self.audio_state = self.JINGLE_STATE_CONNECTING self.audio_sid = sid - self._audio_button.set_active(True) elif state == 'connection_received': self.audio_state = self.JINGLE_STATE_CONNECTION_RECEIVED self.audio_sid = sid - self._audio_button.set_active(True) elif state == 'connected': self.audio_state = self.JINGLE_STATE_CONNECTED elif state == 'stop': self.audio_state = self.JINGLE_STATE_AVAILABLE self.audio_sid = None - self._audio_button.set_active(False) elif state == 'error': self.audio_state = self.JINGLE_STATE_ERROR + + if state in ('connecting', 'connected', 'connection_received'): + self._audio_button.set_active(True) + elif state in ('not_available', 'stop'): + #TODO: Destroy existing session(s) with this user? + self._audio_button.set_active(False) self.update_audio() def set_video_state(self, state, sid=None, reason=None): + #TODO: Share code with set_audio_state? if state in ('connecting', 'connected', 'stop'): str = _('Video state : %s') % state if reason: @@ -1608,11 +1622,9 @@ class ChatControl(ChatControlBase, ChatCommands): self.video_sid = None elif state == 'connecting': self.video_state = self.JINGLE_STATE_CONNECTING - self._video_button.set_active(True) self.video_sid = sid elif state == 'connection_received': self.video_state = self.JINGLE_STATE_CONNECTION_RECEIVED - self._video_button.set_active(True) self.video_sid = sid elif state == 'connected': self.video_state = self.JINGLE_STATE_CONNECTED @@ -1622,6 +1634,12 @@ class ChatControl(ChatControlBase, ChatCommands): self._video_button.set_active(False) elif state == 'error': self.video_state = self.JINGLE_STATE_ERROR + + if state in ('connecting', 'connected', 'connection_received'): + self._video_button.set_active(True) + elif state in ('not_available', 'stop'): + #TODO: Destroy existing session(s) with this user? + self._video_button.set_active(False) self.update_video() def on_avatar_eventbox_enter_notify_event(self, widget, event): @@ -1826,9 +1844,10 @@ class ChatControl(ChatControlBase, ChatCommands): def on_audio_button_toggled(self, widget): if widget.get_active(): - sid = gajim.connections[self.account].startVoIP( - self.contact.get_full_jid()) - self.set_audio_state('connecting', sid) + if self.audio_state == self.JINGLE_STATE_AVAILABLE: + sid = gajim.connections[self.account].startVoIP( + self.contact.get_full_jid()) + self.set_audio_state('connecting', sid) else: session = gajim.connections[self.account].get_jingle_session( self.contact.get_full_jid(), self.audio_sid) @@ -1839,9 +1858,10 @@ class ChatControl(ChatControlBase, ChatCommands): def on_video_button_toggled(self, widget): if widget.get_active(): - sid = gajim.connections[self.account].startVideoIP( - self.contact.get_full_jid()) - self.set_video_state('connecting', sid) + if self.video_state == self.JINGLE_STATE_AVAILABLE: + sid = gajim.connections[self.account].startVideoIP( + self.contact.get_full_jid()) + self.set_video_state('connecting', sid) else: session = gajim.connections[self.account].get_jingle_session( self.contact.get_full_jid(), self.video_sid) diff --git a/src/common/jingle.py b/src/common/jingle.py index 417e024ab..7d79182fc 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -671,6 +671,7 @@ class JingleContent(object): 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' @@ -750,8 +751,12 @@ class JingleContent(object): #FIXME: connectivity should not be etablished yet # Instead, it should be etablished after session-accept! if len(candidates) > 0: - self.p2pstream.set_remote_candidates(candidates) - print self.media, self.creator, self.name, candidates + 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. ''' @@ -917,6 +922,9 @@ class JingleRTPContent(JingleContent): 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) diff --git a/src/dialogs.py b/src/dialogs.py index 59f8cb42a..a93583aab 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -4518,10 +4518,7 @@ class VoIPCallReceivedDialog(object): if not session: return if response == gtk.RESPONSE_YES: - if not session.accepted: - session.approve_session() - for content in self.content_types: - session.approve_content(content) + #TODO: Ensure that ctrl.contact.resource == resource jid = gajim.get_jid_without_resource(self.fjid) resource = gajim.get_resource_from_jid(self.fjid) ctrl = gajim.interface.msg_win_mgr.get_control(self.fjid, self.account) @@ -4535,10 +4532,17 @@ class VoIPCallReceivedDialog(object): if not contact: return ctrl = gajim.interface.new_chat(contact, self.account) + # Chat control opened, update content's status if session.get_content('audio'): ctrl.set_audio_state('connecting', self.sid) if session.get_content('video'): ctrl.set_video_state('connecting', self.sid) + # Now, accept the content/sessions. + # This should be done after the chat control is running + if not session.accepted: + session.approve_session() + for content in self.content_types: + session.approve_content(content) else: # response==gtk.RESPONSE_NO if not session.accepted: session.decline_session() From bc90bc11575b4f565b8b15a150bab00756cb4c59 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Fri, 16 Oct 2009 19:04:04 +0200 Subject: [PATCH 63/67] Destroy session when remote signs off --- src/chat_control.py | 30 ++++++++++++++++++++---------- src/common/jingle.py | 28 +++++++++++++++++++--------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 31a7704c1..26108d54a 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1587,6 +1587,16 @@ class ChatControl(ChatControlBase): if reason: str += ', ' + _('reason: %s') % reason self.print_conversation(str, 'info') + + if state in ('connecting', 'connected', 'connection_received'): + self._audio_button.set_active(True) + elif state in ('not_available', 'stop'): + # Destroy existing session with the user when he signs off + if state == 'not_available': + gajim.connections[self.account].delete_jingle_session( + self.contact.get_full_jid(), self.audio_sid) + self._audio_button.set_active(False) + if state == 'not_available': self.audio_state = self.JINGLE_STATE_NOT_AVAILABLE self.audio_sid = None @@ -1607,11 +1617,6 @@ class ChatControl(ChatControlBase): elif state == 'error': self.audio_state = self.JINGLE_STATE_ERROR - if state in ('connecting', 'connected', 'connection_received'): - self._audio_button.set_active(True) - elif state in ('not_available', 'stop'): - #TODO: Destroy existing session(s) with this user? - self._audio_button.set_active(False) self.update_audio() def set_video_state(self, state, sid=None, reason=None): @@ -1621,6 +1626,16 @@ class ChatControl(ChatControlBase): if reason: str += ', ' + _('reason: %s') % reason self.print_conversation(str, 'info') + + if state in ('connecting', 'connected', 'connection_received'): + self._video_button.set_active(True) + elif state in ('not_available', 'stop'): + # Destroy existing session with the user when he signs off + if state == 'not_available': + gajim.connections[self.account].delete_jingle_session( + self.contact.get_full_jid(), self.video_sid) + self._video_button.set_active(False) + if state == 'not_available': self.video_state = self.JINGLE_STATE_NOT_AVAILABLE self.video_sid = None @@ -1642,11 +1657,6 @@ class ChatControl(ChatControlBase): elif state == 'error': self.video_state = self.JINGLE_STATE_ERROR - if state in ('connecting', 'connected', 'connection_received'): - self._video_button.set_active(True) - elif state in ('not_available', 'stop'): - #TODO: Destroy existing session(s) with this user? - self._video_button.set_active(False) self.update_video() def on_avatar_eventbox_enter_notify_event(self, widget, event): diff --git a/src/common/jingle.py b/src/common/jingle.py index 7d79182fc..f05886533 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -39,13 +39,11 @@ # * 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 -# * destroy sessions when user is unavailable, see handle_event_notify? # * timeout -# * security (see XEP 0166) # * split this file in several modules # For example, a file dedicated for XEP0166, one for XEP0176, -# and one for each media of XEP0167 +# and one for XEP0167 # * handle different kinds of sink and src elements @@ -78,6 +76,9 @@ class TransportType(object): 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): @@ -306,6 +307,8 @@ class JingleSession(object): 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 @@ -328,7 +331,7 @@ class JingleSession(object): 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(self) + self.connection.delete_jingle_session(self.peerjid, self.sid) def __transportReplaceCB(self, stanza, jingle, error, action): for content in jingle.iterTags('content'): @@ -415,7 +418,8 @@ class JingleSession(object): ''' 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: #FIXME + + if self.state != JingleStates.ended: raise OutOfOrder self.initiator = jingle['initiator'] @@ -463,7 +467,7 @@ class JingleSession(object): cn.stanzaCB(stanza, content, error, action) def __sessionTerminateCB(self, stanza, jingle, error, action): - self.connection.delete_jingle(self) + 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) @@ -607,7 +611,7 @@ class JingleSession(object): text = '%s (%s)' % (reason, text) else: text = reason - self.connection.delete_jingle(self) + self.connection.delete_jingle_session(self.peerjid, self.sid) self.connection.dispatch('JINGLE_DISCONNECTED', (self.peerjid, self.sid, None, text)) @@ -1078,9 +1082,15 @@ class ConnectionJingle(object): ''' self.__sessions[(jingle.peerjid, jingle.sid)] = jingle - def delete_jingle(self, jingle): + def delete_jingle_session(self, peerjid, sid): ''' Remove a jingle session from a jingle stanza dispatcher ''' - del self.__sessions[(jingle.peerjid, jingle.sid)] + key = (peerjid, sid) + if key in self.__sessions: + #FIXME: Move this elsewhere? + for content in self.__sessions[key].contents.values(): + content.destroy() + self.__sessions[key].callbacks = [] + del self.__sessions[key] def _JingleCB(self, con, stanza): ''' The jingle stanza dispatcher. From d495deaa02d81bf69def77074627f95c172e4e52 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Fri, 16 Oct 2009 19:36:55 +0200 Subject: [PATCH 64/67] Some rewriting in set_audio_state/set_video_state --- src/chat_control.py | 86 +++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 26108d54a..314e6d7ec 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1553,6 +1553,7 @@ class ChatControl(ChatControlBase): self.update_toolbar() def update_video(self): + #TODO: Share code with update_audio? if self.video_state in (self.JINGLE_STATE_NOT_AVAILABLE, self.JINGLE_STATE_AVAILABLE): self._video_banner_image.hide() @@ -1588,74 +1589,67 @@ class ChatControl(ChatControlBase): str += ', ' + _('reason: %s') % reason self.print_conversation(str, 'info') + states = {'not_available': self.JINGLE_STATE_NOT_AVAILABLE, + 'available': self.JINGLE_STATE_AVAILABLE, + 'connecting': self.JINGLE_STATE_CONNECTING, + 'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED, + 'connected': self.JINGLE_STATE_CONNECTED, + 'stop': self.JINGLE_STATE_AVAILABLE, + 'error': self.JINGLE_STATE_ERROR} + + if state in states: + self.audio_state = states[state] + + if state in ('not_available', 'available', 'stop'): + self.audio_sid = None + if state in ('connection_received', 'connecting'): + self.audio_sid = sid + if state in ('connecting', 'connected', 'connection_received'): self._audio_button.set_active(True) elif state in ('not_available', 'stop'): - # Destroy existing session with the user when he signs off - if state == 'not_available': - gajim.connections[self.account].delete_jingle_session( - self.contact.get_full_jid(), self.audio_sid) self._audio_button.set_active(False) + # Destroy existing session with the user when he signs off if state == 'not_available': - self.audio_state = self.JINGLE_STATE_NOT_AVAILABLE - self.audio_sid = None - elif state == 'available': - self.audio_state = self.JINGLE_STATE_AVAILABLE - self.audio_sid = None - elif state == 'connecting': - self.audio_state = self.JINGLE_STATE_CONNECTING - self.audio_sid = sid - elif state == 'connection_received': - self.audio_state = self.JINGLE_STATE_CONNECTION_RECEIVED - self.audio_sid = sid - elif state == 'connected': - self.audio_state = self.JINGLE_STATE_CONNECTED - elif state == 'stop': - self.audio_state = self.JINGLE_STATE_AVAILABLE - self.audio_sid = None - elif state == 'error': - self.audio_state = self.JINGLE_STATE_ERROR + gajim.connections[self.account].delete_jingle_session( + self.contact.get_full_jid(), self.audio_sid) self.update_audio() def set_video_state(self, state, sid=None, reason=None): #TODO: Share code with set_audio_state? - if state in ('connecting', 'connected', 'stop'): + if state in ('connecting', 'connected', 'stop'): str = _('Video state : %s') % state if reason: str += ', ' + _('reason: %s') % reason self.print_conversation(str, 'info') + states = {'not_available': self.JINGLE_STATE_NOT_AVAILABLE, + 'available': self.JINGLE_STATE_AVAILABLE, + 'connecting': self.JINGLE_STATE_CONNECTING, + 'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED, + 'connected': self.JINGLE_STATE_CONNECTED, + 'stop': self.JINGLE_STATE_AVAILABLE, + 'error': self.JINGLE_STATE_ERROR} + + if state in states: + self.video_state = states[state] + + if state in ('not_available', 'available', 'stop'): + self.video_sid = None + if state in ('connection_received', 'connecting'): + self.video_sid = sid + if state in ('connecting', 'connected', 'connection_received'): self._video_button.set_active(True) elif state in ('not_available', 'stop'): - # Destroy existing session with the user when he signs off - if state == 'not_available': - gajim.connections[self.account].delete_jingle_session( - self.contact.get_full_jid(), self.video_sid) self._video_button.set_active(False) + # Destroy existing session with the user when he signs off if state == 'not_available': - self.video_state = self.JINGLE_STATE_NOT_AVAILABLE - self.video_sid = None - elif state == 'available': - self.video_state = self.JINGLE_STATE_AVAILABLE - self.video_sid = None - elif state == 'connecting': - self.video_state = self.JINGLE_STATE_CONNECTING - self.video_sid = sid - elif state == 'connection_received': - self.video_state = self.JINGLE_STATE_CONNECTION_RECEIVED - self.video_sid = sid - elif state == 'connected': - self.video_state = self.JINGLE_STATE_CONNECTED - elif state == 'stop': - self.video_state = self.JINGLE_STATE_AVAILABLE - self.video_sid = None - self._video_button.set_active(False) - elif state == 'error': - self.video_state = self.JINGLE_STATE_ERROR + gajim.connections[self.account].delete_jingle_session( + self.contact.get_full_jid(), self.video_sid) self.update_video() From 313f00295814970f9be2f23b1b78e6f5902a2ac4 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Fri, 16 Oct 2009 20:21:42 +0200 Subject: [PATCH 65/67] Fixes an indentation error aswell as the previous commit --- src/chat_control.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 314e6d7ec..8b5fea11d 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1600,6 +1600,12 @@ class ChatControl(ChatControlBase): if state in states: self.audio_state = states[state] + # Destroy existing session with the user when he signs off + # We need to do that before modifying the sid + if state == 'not_available': + gajim.connections[self.account].delete_jingle_session( + self.contact.get_full_jid(), self.audio_sid) + if state in ('not_available', 'available', 'stop'): self.audio_sid = None if state in ('connection_received', 'connecting'): @@ -1610,16 +1616,11 @@ class ChatControl(ChatControlBase): elif state in ('not_available', 'stop'): self._audio_button.set_active(False) - # Destroy existing session with the user when he signs off - if state == 'not_available': - gajim.connections[self.account].delete_jingle_session( - self.contact.get_full_jid(), self.audio_sid) - self.update_audio() def set_video_state(self, state, sid=None, reason=None): #TODO: Share code with set_audio_state? - if state in ('connecting', 'connected', 'stop'): + if state in ('connecting', 'connected', 'stop'): str = _('Video state : %s') % state if reason: str += ', ' + _('reason: %s') % reason @@ -1636,6 +1637,12 @@ class ChatControl(ChatControlBase): if state in states: self.video_state = states[state] + # Destroy existing session with the user when he signs off + # We need to do that before modifying the sid + if state == 'not_available': + gajim.connections[self.account].delete_jingle_session( + self.contact.get_full_jid(), self.video_sid) + if state in ('not_available', 'available', 'stop'): self.video_sid = None if state in ('connection_received', 'connecting'): @@ -1646,11 +1653,6 @@ class ChatControl(ChatControlBase): elif state in ('not_available', 'stop'): self._video_button.set_active(False) - # Destroy existing session with the user when he signs off - if state == 'not_available': - gajim.connections[self.account].delete_jingle_session( - self.contact.get_full_jid(), self.video_sid) - self.update_video() def on_avatar_eventbox_enter_notify_event(self, widget, event): From 760e0fb48f70af2a03d03b91141b4842012c2363 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Fri, 16 Oct 2009 21:48:28 +0200 Subject: [PATCH 66/67] refactor jingle functions in chat_control.py --- src/chat_control.py | 107 +++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 71 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 8b5fea11d..46d49a69f 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1536,38 +1536,33 @@ class ChatControl(ChatControlBase): else: self._tune_image.hide() - def update_audio(self): - if self.audio_state in (self.JINGLE_STATE_NOT_AVAILABLE, - self.JINGLE_STATE_AVAILABLE): - self._audio_banner_image.hide() + def _update_jingle(self, jingle_type): + if jingle_type not in ('audio', 'video'): + return + if self.__dict__[jingle_type + '_state'] in ( + self.JINGLE_STATE_NOT_AVAILABLE, self.JINGLE_STATE_AVAILABLE): + self.__dict__['_' + jingle_type + '_banner_image'].hide() else: - self._audio_banner_image.show() + self.__dict__['_' + jingle_type + '_banner_image'].show() if self.audio_state == self.JINGLE_STATE_CONNECTING: - self._audio_banner_image.set_from_stock(gtk.STOCK_CONVERT, 1) + self.__dict__['_' + jingle_type + '_banner_image'].set_from_stock( + gtk.STOCK_CONVERT, 1) elif self.audio_state == self.JINGLE_STATE_CONNECTION_RECEIVED: - self._audio_banner_image.set_from_stock(gtk.STOCK_NETWORK, 1) + self.__dict__['_' + jingle_type + '_banner_image'].set_from_stock( + gtk.STOCK_NETWORK, 1) elif self.audio_state == self.JINGLE_STATE_CONNECTED: - self._audio_banner_image.set_from_stock(gtk.STOCK_CONNECT, 1) + self.__dict__['_' + jingle_type + '_banner_image'].set_from_stock( + gtk.STOCK_CONNECT, 1) elif self.audio_state == self.JINGLE_STATE_ERROR: - self._audio_banner_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 1) + self.__dict__['_' + jingle_type + '_banner_image'].set_from_stock( + gtk.STOCK_DIALOG_WARNING, 1) self.update_toolbar() + def update_audio(self): + self._update_jingle('audio') + def update_video(self): - #TODO: Share code with update_audio? - if self.video_state in (self.JINGLE_STATE_NOT_AVAILABLE, - self.JINGLE_STATE_AVAILABLE): - self._video_banner_image.hide() - else: - self._video_banner_image.show() - if self.video_state == self.JINGLE_STATE_CONNECTING: - self._video_banner_image.set_from_stock(gtk.STOCK_CONVERT, 1) - elif self.video_state == self.JINGLE_STATE_CONNECTION_RECEIVED: - self._video_banner_image.set_from_stock(gtk.STOCK_NETWORK, 1) - elif self.video_state == self.JINGLE_STATE_CONNECTED: - self._video_banner_image.set_from_stock(gtk.STOCK_CONNECT, 1) - elif self.video_state == self.JINGLE_STATE_ERROR: - self._video_banner_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 1) - self.update_toolbar() + self._update_jingle('video') def change_resource(self, resource): old_full_jid = self.get_full_jid() @@ -1582,11 +1577,12 @@ class ChatControl(ChatControlBase): # update MessageWindow._controls self.parent_win.change_jid(self.account, old_full_jid, new_full_jid) - def set_audio_state(self, state, sid=None, reason=None): - if state in ('connecting', 'connected', 'stop'): - str = _('Audio state : %s') % state - if reason: - str += ', ' + _('reason: %s') % reason + def _set_jingle_state(self, jingle_type, state, sid=None, reason=None): + if jingle_type not in ('audio', 'video'): + return + if state in ('connecting', 'connected', 'stop') and reason: + str = _('%(type)s state : %(state)s, reason: %(reason)s') % { + 'type': jingle_type.capitalize(), 'state': state, 'reason': reason} self.print_conversation(str, 'info') states = {'not_available': self.JINGLE_STATE_NOT_AVAILABLE, @@ -1598,62 +1594,31 @@ class ChatControl(ChatControlBase): 'error': self.JINGLE_STATE_ERROR} if state in states: - self.audio_state = states[state] + self.__dict__[jingle_type + '_state'] = states[state] # Destroy existing session with the user when he signs off # We need to do that before modifying the sid if state == 'not_available': gajim.connections[self.account].delete_jingle_session( - self.contact.get_full_jid(), self.audio_sid) + self.contact.get_full_jid(), self.__dict__[jingle_type + '_sid']) if state in ('not_available', 'available', 'stop'): - self.audio_sid = None + self.__dict__[jingle_type + '_sid'] = None if state in ('connection_received', 'connecting'): - self.audio_sid = sid + self.__dict__[jingle_type + '_sid'] = sid if state in ('connecting', 'connected', 'connection_received'): - self._audio_button.set_active(True) + self.__dict__['_' + jingle_type + '_button'].set_active(True) elif state in ('not_available', 'stop'): - self._audio_button.set_active(False) + self.__dict__['_' + jingle_type + '_button'].set_active(False) - self.update_audio() + eval('self.update_' + jingle_type)() + + def set_audio_state(self, state, sid=None, reason=None): + self._set_jingle_state('audio', state, sid=sid, reason=reason) def set_video_state(self, state, sid=None, reason=None): - #TODO: Share code with set_audio_state? - if state in ('connecting', 'connected', 'stop'): - str = _('Video state : %s') % state - if reason: - str += ', ' + _('reason: %s') % reason - self.print_conversation(str, 'info') - - states = {'not_available': self.JINGLE_STATE_NOT_AVAILABLE, - 'available': self.JINGLE_STATE_AVAILABLE, - 'connecting': self.JINGLE_STATE_CONNECTING, - 'connection_received': self.JINGLE_STATE_CONNECTION_RECEIVED, - 'connected': self.JINGLE_STATE_CONNECTED, - 'stop': self.JINGLE_STATE_AVAILABLE, - 'error': self.JINGLE_STATE_ERROR} - - if state in states: - self.video_state = states[state] - - # Destroy existing session with the user when he signs off - # We need to do that before modifying the sid - if state == 'not_available': - gajim.connections[self.account].delete_jingle_session( - self.contact.get_full_jid(), self.video_sid) - - if state in ('not_available', 'available', 'stop'): - self.video_sid = None - if state in ('connection_received', 'connecting'): - self.video_sid = sid - - if state in ('connecting', 'connected', 'connection_received'): - self._video_button.set_active(True) - elif state in ('not_available', 'stop'): - self._video_button.set_active(False) - - self.update_video() + self._set_jingle_state('video', state, sid=sid, reason=reason) def on_avatar_eventbox_enter_notify_event(self, widget, event): ''' From 7ae959dbba39d815dd52f6d7a4bba11790ef7ecd Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Thu, 29 Oct 2009 10:08:22 +0100 Subject: [PATCH 67/67] Avoid insane recursion --- src/chat_control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/chat_control.py b/src/chat_control.py index 46d49a69f..2873066c9 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1594,7 +1594,10 @@ class ChatControl(ChatControlBase): 'error': self.JINGLE_STATE_ERROR} if state in states: - self.__dict__[jingle_type + '_state'] = states[state] + jingle_state = states[state] + if self.__dict__[jingle_type + '_state'] == jingle_state: + return + self.__dict__[jingle_type + '_state'] = jingle_state # Destroy existing session with the user when he signs off # We need to do that before modifying the sid