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