support for content-{add,reject,accept}, new helpers, and other few things

This commit is contained in:
Thibaut GIRKA 2009-09-25 19:32:13 +02:00
parent a051d1ec95
commit 77541f3e7f
4 changed files with 196 additions and 81 deletions

View File

@ -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:

View File

@ -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():
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):
if session.peerjid == jid and session.get_content(media):
return session
return None

View File

@ -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)
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()

View File

@ -2119,7 +2119,10 @@ class Interface:
if not ctrl:
ctrl = self.msg_win_mgr.get_control(jid, account)
if ctrl:
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:
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))