From 77541f3e7f78076eeae9de4267d6bd44fa279b25 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Fri, 25 Sep 2009 19:32:13 +0200 Subject: [PATCH] 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))