jingle: move from the old farsight to farsight2,

better compliance to the last version of XEP 0166, 0167 and 0176
This commit is contained in:
Thibaut GIRKA 2009-09-13 11:02:49 +02:00
parent 94f3db54ec
commit 32ad59aa42
5 changed files with 444 additions and 252 deletions

View File

@ -50,6 +50,7 @@ from common.logger import constants
from common.pep import MOODS, ACTIVITIES 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_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC
from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION
from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO
try: try:
import gtkspell import gtkspell
@ -1634,7 +1635,7 @@ class ChatControl(ChatControlBase):
banner_name_tooltip.set_tip(banner_name_label, label_tooltip) banner_name_tooltip.set_tip(banner_name_label, label_tooltip)
def _on_start_voip_menuitem_activate(self, *things): 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): def _toggle_gpg(self):
if not self.gpg_is_active and not self.contact.keyID: if not self.gpg_is_active and not self.contact.keyID:
@ -2168,6 +2169,12 @@ class ChatControl(ChatControlBase):
else: else:
send_file_menuitem.set_sensitive(False) 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 # check if it's possible to convert to groupchat
if gajim.capscache.is_supported(contact, NS_MUC): if gajim.capscache.is_supported(contact, NS_MUC):
convert_to_gc_menuitem.set_sensitive(True) convert_to_gc_menuitem.set_sensitive(True)

View File

@ -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, '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', 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, '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 # Optional features gajim supports per account
gajim_optional_features = {} gajim_optional_features = {}

View File

@ -12,42 +12,69 @@
## ##
''' Handles the jingle signalling protocol. ''' ''' Handles the jingle signalling protocol. '''
# note: if there will be more types of sessions (possibly file transfer, #TODO:
# video...), split this file # * 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 gajim
import gobject import gobject
import xmpp import xmpp
# ugly hack, fixed in farsight 0.1.24 import farsight, gst
import sys, dl, gst
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 def get_first_gst_element(elements):
for element in iterable: ''' Returns, if it exists, the first available element of the list. '''
if not element: for name in elements:
return False factory = gst.element_factory_find(name)
return True if factory:
return factory.create()
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
#FIXME: Move it to JingleSession.States?
class JingleStates(object): class JingleStates(object):
''' States in which jingle session may exist. ''' ''' States in which jingle session may exist. '''
ended=0 ended=0
pending=1 pending=1
active=2 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 Error(Exception): pass
class WrongState(Error): pass class WrongState(Error): pass
class NoSuchSession(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): class JingleSession(object):
''' This represents one jingle session. ''' ''' This represents one jingle session. '''
def __init__(self, con, weinitiate, jid, sid=None): def __init__(self, con, weinitiate, jid, sid=None):
@ -72,28 +99,31 @@ class JingleSession(object):
self.sid=sid # sessionid 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 # callbacks to call on proper contents
# use .prepend() to add new callbacks, especially when you're going # use .prepend() to add new callbacks, especially when you're going
# to send error instead of ack # to send error instead of ack
self.callbacks={ self.callbacks={
'content-accept': [self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], 'content-accept': [self.__contentAcceptCB, self.__defaultCB],
'content-add': [self.__defaultCB], 'content-add': [self.__defaultCB], #TODO
'content-modify': [self.__defaultCB], 'content-modify': [self.__defaultCB], #TODO
'content-remove': [self.__defaultCB], 'content-reject': [self.__defaultCB], #TODO
'session-accept': [self.__contentAcceptCB, self.__broadcastCB, self.__defaultCB], 'content-remove': [self.__defaultCB, self.__contentRemoveCB],
'session-info': [self.__defaultCB], '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-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-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-result': [],
'iq-error': [], '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 ''' ''' Interaction with user '''
def approveSession(self): def approveSession(self):
''' Called when user accepts session in UI (when we aren't the initiator).''' ''' Called when user accepts session in UI (when we aren't the initiator).'''
@ -101,9 +131,10 @@ class JingleSession(object):
self.acceptSession() self.acceptSession()
def declineSession(self): 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)'''
or when the user wants to stop session completly. ''' reason = xmpp.Node('reason')
self.__sessionTerminate() reason.addChild('decline')
self.__sessionTerminate(reason)
''' Middle-level functions to manage contents. Handle local content ''' Middle-level functions to manage contents. Handle local content
cache and send change notifications. ''' cache and send change notifications. '''
@ -138,17 +169,29 @@ class JingleSession(object):
def acceptSession(self): def acceptSession(self):
''' Check if all contents and user agreed to start session. ''' ''' Check if all contents and user agreed to start session. '''
if not self.weinitiate and \ if not self.weinitiate and self.accepted and self.candidates_ready:
self.accepted and \
all(c.negotiated for c in self.contents.itervalues()):
self.__sessionAccept() self.__sessionAccept()
''' Middle-level function to do stanza exchange. ''' ''' Middle-level function to do stanza exchange. '''
def startSession(self): def startSession(self):
''' Start session. ''' ''' Start session. '''
self.__sessionInitiate() if self.weinitiate and self.candidates_ready:
self.__sessionInitiate()
def sendSessionInfo(self): pass 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. ''' ''' Session callbacks. '''
def stanzaCB(self, stanza): def stanzaCB(self, stanza):
''' A callback for ConnectionJingle. It gets stanza, then ''' A callback for ConnectionJingle. It gets stanza, then
@ -162,6 +205,17 @@ class JingleSession(object):
elif jingle: elif jingle:
# it's a jingle action # it's a jingle action
action = jingle.getAttr('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: else:
# it's an iq-result (ack) stanza # it's an iq-result (ack) stanza
action = 'iq-result' action = 'iq-result'
@ -173,6 +227,10 @@ class JingleSession(object):
callable(stanza=stanza, jingle=jingle, error=error, action=action) callable(stanza=stanza, jingle=jingle, error=error, action=action)
except xmpp.NodeProcessed: except xmpp.NodeProcessed:
pass 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): def __defaultCB(self, stanza, jingle, error, action):
''' Default callback for action stanzas -- simple ack ''' Default callback for action stanzas -- simple ack
@ -180,18 +238,72 @@ class JingleSession(object):
response = stanza.buildReply('result') response = stanza.buildReply('result')
self.connection.connection.send(response) 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): def __contentAcceptCB(self, stanza, jingle, error, action):
''' Called when we get content-accept stanza or equivalent one ''' Called when we get content-accept stanza or equivalent one
(like session-accept).''' (like session-accept).'''
# check which contents are accepted # check which contents are accepted
for content in jingle.iterTags('content'): for content in jingle.iterTags('content'):
creator = content['creator'] creator = content['creator']
name = content['name'] name = content['name']#TODO...
def __sessionInitiateCB(self, stanza, jingle, error, action): def __sessionInitiateCB(self, stanza, jingle, error, action):
''' We got a jingle session request from other entity, ''' 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. ''' inform the user. '''
if self.state != JingleStates.ended: #FIXME
raise OutOfOrder
self.initiator = jingle['initiator'] self.initiator = jingle['initiator']
self.responder = self.ourjid self.responder = self.ourjid
self.peerjid = self.initiator self.peerjid = self.initiator
@ -203,25 +315,37 @@ class JingleSession(object):
# error. # error.
# Lets check what kind of jingle session does the peer want # Lets check what kind of jingle session does the peer want
fail = True
contents = [] contents = []
contents_ok = False
transports_ok = False
for element in jingle.iterTags('content'): for element in jingle.iterTags('content'):
# checking what kind of session this will be # checking what kind of session this will be
desc_ns = element.getTag('description').getNamespace() desc_ns = element.getTag('description').getNamespace()
media = element.getTag('description')['media']
tran_ns = element.getTag('transport').getNamespace() tran_ns = element.getTag('transport').getNamespace()
if desc_ns==xmpp.NS_JINGLE_AUDIO and tran_ns==xmpp.NS_JINGLE_ICE_UDP: if desc_ns == xmpp.NS_JINGLE_RTP and media == 'audio':
# we've got voip content contents_ok = True
self.addContent(element['name'], JingleVoiP(self), 'peer') if tran_ns == xmpp.NS_JINGLE_ICE_UDP:
contents.append(('VOIP',)) # we've got voip content
fail = False self.addContent(element['name'], JingleVoIP(self), 'peer')
contents.append(('VOIP',))
transports_ok = True
# If there's no content we understand... # If there's no content we understand...
if fail: if not contents_ok:
# TODO: we should send <unsupported-content/> inside too # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate
# TODO: delete this instance reason = xmpp.Node('reason')
self.connection.connection.send( reason.setTag('unsupported-applications')
xmpp.Error(stanza, xmpp.NS_STANZAS + 'feature-not-implemented')) self.__defaultCB(stanza, jingle, error, action)
self.connection.deleteJingle(self) 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 raise xmpp.NodeProcessed
self.state = JingleStates.pending self.state = JingleStates.pending
@ -237,23 +361,25 @@ class JingleSession(object):
cn = self.contents[(creator, name)] cn = self.contents[(creator, name)]
cn.stanzaCB(stanza, content, error, action) cn.stanzaCB(stanza, content, error, action)
def __sessionTerminateCB(self, stanza, jingle, error, action):
self.connection.deleteJingle(self)
def __broadcastAllCB(self, stanza, jingle, error, action): def __broadcastAllCB(self, stanza, jingle, error, action):
''' Broadcast the stanza to all content handlers. ''' ''' Broadcast the stanza to all content handlers. '''
for content in self.contents.itervalues(): for content in self.contents.itervalues():
content.stanzaCB(stanza, None, error, action) 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 ''' Methods that make/send proper pieces of XML. They check if the session
is in appropriate state. ''' is in appropriate state. '''
def __makeJingle(self, action): def __makeJingle(self, action):
stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid))
jingle = stanza.addChild('jingle', attrs={ attrs = {'action': action,
'xmlns': 'http://www.xmpp.org/extensions/xep-0166.html#ns', 'sid': self.sid}
'action': action, if action == 'session-initiate':
'initiator': self.initiator, attrs['initiator'] = self.initiator
'responder': self.responder, elif action == 'session-accept':
'sid': self.sid}) attrs['responder'] = self.responder
jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE)
return stanza, jingle return stanza, jingle
def __appendContent(self, jingle, content): def __appendContent(self, jingle, content):
@ -276,6 +402,7 @@ class JingleSession(object):
self.__appendContents(jingle) self.__appendContents(jingle)
self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent') self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent')
self.connection.connection.send(stanza) self.connection.connection.send(stanza)
self.state = JingleStates.pending
def __sessionAccept(self): def __sessionAccept(self):
assert self.state==JingleStates.pending assert self.state==JingleStates.pending
@ -292,11 +419,14 @@ class JingleSession(object):
jingle.addChild(node=payload) jingle.addChild(node=payload)
self.connection.connection.send(stanza) self.connection.connection.send(stanza)
def __sessionTerminate(self): def __sessionTerminate(self, reason=None):
assert self.state!=JingleStates.ended assert self.state!=JingleStates.ended
stanza, jingle = self.__makeJingle('session-terminate') stanza, jingle = self.__makeJingle('session-terminate')
if reason is not None:
jingle.addChild(node=reason)
self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent') self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent')
self.connection.connection.send(stanza) self.connection.connection.send(stanza)
self.connection.deleteJingle(self)
def __contentAdd(self): def __contentAdd(self):
assert self.state==JingleStates.active assert self.state==JingleStates.active
@ -310,20 +440,13 @@ class JingleSession(object):
def __contentRemove(self): def __contentRemove(self):
assert self.state!=JingleStates.ended 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): class JingleTransport(object):
assert self.state!=JingleStates.ended ''' An abstraction of a transport in Jingle sessions. '''
stanza, jingle = self.__makeJingle('transport-info') #TODO: Complete
jingle.addChild(node=content) def __init__(self):
self.connection.connection.send(stanza) pass#TODO: Complete
'''Callbacks'''
def sessionTerminateCB(self, stanza): pass
class JingleContent(object): class JingleContent(object):
''' An abstraction of content in Jingle sessions. ''' ''' An abstraction of content in Jingle sessions. '''
@ -335,49 +458,179 @@ class JingleContent(object):
#self.creator = None #self.creator = None
#self.name = None #self.name = None
self.negotiated = False # is this content already negotiated? self.negotiated = False # is this content already negotiated?
self.candidates = [] # Local transport candidates
class JingleVoiP(JingleContent): self.allow_sending = True # Used for stream direction, attribute 'senders'
''' 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
#if node is None: self.callbacks = {
# 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 = {
# these are called when *we* get stanzas # these are called when *we* get stanzas
'content-accept': [self.__getRemoteCodecsCB], 'content-accept': [],
'content-add': [], 'content-add': [],
'content-modify': [], 'content-modify': [],
'content-remove': [], 'content-remove': [],
'session-accept': [self.__getRemoteCodecsCB, self.__startMic], 'session-accept': [self.__transportInfoCB],
'session-info': [], 'session-info': [],
'session-initiate': [self.__getRemoteCodecsCB], 'session-initiate': [self.__transportInfoCB],
'session-terminate': [self.__stop], 'session-terminate': [],
'transport-info': [self.__transportInfoCB], 'transport-info': [self.__transportInfoCB],
'iq-result': [], 'iq-result': [],
'iq-error': [], 'iq-error': [],
# these are called when *we* sent these stanzas # these are called when *we* sent these stanzas
'session-initiate-sent': [self.__sessionInitiateSentCB], 'session-initiate-sent': [self.__fillJingleStanza],
'session-accept-sent': [self.__startMic], 'session-accept-sent': [self.__fillJingleStanza],
'session-terminate-sent': [self.__stop], 'session-terminate-sent': [],
}[action] }
for callback in callbacks:
callback(stanza, content, error, action)
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. ''' ''' Add our things to session-initiate stanza. '''
content.setAttr('profile', 'RTP/AVP') self._fillContent(content)
content.addChild(xmpp.NS_JINGLE_AUDIO+' description', payload=self.iterCodecs())
content.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') 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): def __getRemoteCodecsCB(self, stanza, content, error, action):
''' Get peer codecs from what we get from peer. ''' ''' Get peer codecs from what we get from peer. '''
@ -385,154 +638,86 @@ class JingleVoiP(JingleContent):
codecs = [] codecs = []
for codec in content.getTag('description').iterTags('payload-type'): for codec in content.getTag('description').iterTags('payload-type'):
c = {'id': int(codec['id']), c = farsight.Codec(int(codec['id']), codec['name'],
'encoding_name': codec['name'], farsight.MEDIA_TYPE_AUDIO, int(codec['clockrate']))
'media_type': farsight.MEDIA_TYPE_AUDIO, if 'channels' in codec:
'channels': 1, c.channels = int(codec['channels'])
'params': dict((p['name'], p['value']) for p in codec.iterTags('parameter'))} else:
if 'channels' in codec: c['channels']=codec['channels'] c.channels = 1
c.optional_params = [(str(p['name']), str(p['value'])) for p in codec.iterTags('parameter')]
codecs.append(c) 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
self.p2pstream.set_remote_codecs(codecs) self.p2pstream.set_remote_codecs(codecs)
self.got_codecs=True 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 <content/> 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): def iterCodecs(self):
codecs=self.p2pstream.get_local_codecs() codecs=self.p2psession.get_property('codecs')
for codec in codecs: for codec in codecs:
a = {'name': codec['encoding_name'], a = {'name': codec.encoding_name,
'id': codec['id'], 'id': codec.id,
'channels': 1} 'channels': codec.channels}
if 'clock_rate' in codec: a['clockrate']=codec['clock_rate'] if codec.clock_rate: a['clockrate']=codec.clock_rate
if 'optional_params' in codec: if codec.optional_params:
p = (xmpp.Node('parameter', {'name': name, 'value': value}) 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 = () else: p = ()
yield xmpp.Node('payload-type', a, 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 ''' ''' Things to control the gstreamer's pipeline '''
def setupStream(self): def setupStream(self):
# the pipeline JingleRTPContent.setupStream(self)
self.pipeline = gst.Pipeline()
# the network part # the network part
self.p2pstream = self.session.p2psession.create_stream( participant = self.conference.new_participant(self.session.peerjid)
farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) params = {'controlling-mode': self.session.weinitiate,# 'debug': False}
self.p2pstream.set_pipeline(self.pipeline) 'stun-ip': '69.0.208.27', 'debug': False}
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.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 # the local parts
# TODO: use gconfaudiosink? # TODO: use gconfaudiosink?
# sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink'])
sink = gst.element_factory_make('alsasink') sink = gst.element_factory_make('alsasink')
sink.set_property('sync', False) sink.set_property('sync', False)
sink.set_property('latency-time', 20000) #sink.set_property('latency-time', 20000)
sink.set_property('buffer-time', 80000) #sink.set_property('buffer-time', 80000)
self.pipeline.add(sink) 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? # TODO: use gconfaudiosrc?
self.src_mic = gst.element_factory_make('alsasrc') self.src_mic = gst.element_factory_make('alsasrc')
self.src_mic.set_property('blocksize', 320) self.src_mic.set_property('blocksize', 320)
@ -542,40 +727,35 @@ class JingleVoiP(JingleContent):
self.mic_volume.set_property('volume', 0) self.mic_volume.set_property('volume', 0)
self.pipeline.add(self.mic_volume) self.pipeline.add(self.mic_volume)
self.adder = gst.element_factory_make('adder')
self.pipeline.add(self.adder)
# link gst elements # link gst elements
self.src_signal.link(self.adder)
self.src_mic.link(self.mic_volume) self.src_mic.link(self.mic_volume)
self.mic_volume.link(self.adder)
# this will actually start before the pipeline will be started. def src_pad_added (stream, pad, codec):
# no worries, though; it's only a ringing sound if not self.funnel:
def signal(): self.funnel = gst.element_factory_make('fsfunnel')
while True: self.pipeline.add(self.funnel)
self.src_signal.set_property('volume', 0.5) self.funnel.set_state (gst.STATE_PLAYING)
yield True # wait 750 ms sink.set_state (gst.STATE_PLAYING)
yield True # wait 750 ms self.funnel.link(sink)
self.src_signal.set_property('volume', 0) pad.link(self.funnel.get_pad('sink%d'))
yield True # wait 750 ms
self.signal_cb_id = timeout_add_and_call(750, signal().__iter__().next)
self.p2pstream.set_sink(sink) self.mic_volume.get_pad('src').link(self.p2psession.get_property('sink-pad'))
self.p2pstream.set_source(self.adder) 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): 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) self.mic_volume.set_property('volume', 1)
def __stop(self, *things): def __stop(self, *things):
self.conference.set_state(gst.STATE_NULL)
self.pipeline.set_state(gst.STATE_NULL) self.pipeline.set_state(gst.STATE_NULL)
gobject.source_remove(self.signal_cb_id)
def __del__(self): def __del__(self):
self.__stop() self.__stop()
class ConnectionJingle(object): class ConnectionJingle(object):
''' This object depends on that it is a part of Connection class. ''' ''' This object depends on that it is a part of Connection class. '''
def __init__(self): def __init__(self):
@ -594,7 +774,7 @@ class ConnectionJingle(object):
def deleteJingle(self, jingle): def deleteJingle(self, jingle):
''' Remove a jingle session from a jingle stanza dispatcher ''' ''' 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): def _JingleCB(self, con, stanza):
''' The jingle stanza dispatcher. ''' The jingle stanza dispatcher.
@ -629,10 +809,10 @@ class ConnectionJingle(object):
def addJingleIqCallback(self, jid, id, jingle): def addJingleIqCallback(self, jid, id, jingle):
self.__iq_responses[(jid, id)]=jingle self.__iq_responses[(jid, id)]=jingle
def startVoiP(self, jid): def startVoIP(self, jid):
jingle = JingleSession(self, weinitiate=True, jid=jid) jingle = JingleSession(self, weinitiate=True, jid=jid)
self.addJingle(jingle) self.addJingle(jingle)
jingle.addContent('voice', JingleVoiP(jingle)) jingle.addContent('voice', JingleVoIP(jingle))
jingle.startSession() jingle.startSession()
def getJingleSession(self, jid, sid): def getJingleSession(self, jid, sid):
try: try:

View File

@ -63,10 +63,12 @@ NS_HTTP_BIND ='http://jabber.org/protocol/httpbind' # XEP-0124
NS_IBB ='http://jabber.org/protocol/ibb' NS_IBB ='http://jabber.org/protocol/ibb'
NS_INVISIBLE ='presence-invisible' # Jabberd2 NS_INVISIBLE ='presence-invisible' # Jabberd2
NS_IQ ='iq' # Jabberd2 NS_IQ ='iq' # Jabberd2
NS_JINGLE ='http://www.xmpp.org/extensions/xep-0166.html#ns' # XEP-0166 NS_JINGLE ='urn:xmpp:jingle:1' # XEP-0166
NS_JINGLE_AUDIO ='http://www.xmpp.org/extensions/xep-0167.html#ns' # XEP-0167 NS_JINGLE_ERRORS='urn:xmpp:jingle:errors:1' # XEP-0166
NS_JINGLE_RAW_UDP='http://www.xmpp.org/extensions/xep-0177.html#ns' # XEP-0177 NS_JINGLE_RTP ='urn:xmpp:jingle:apps:rtp:1' # XEP-0167
NS_JINGLE_ICE_UDP='http://www.xmpp.org/extensions/xep-0176.html#ns-udp' # XEP-0176 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_LAST ='jabber:iq:last'
NS_MESSAGE ='message' # Jabberd2 NS_MESSAGE ='message' # Jabberd2
NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107 NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107

View File

@ -2098,6 +2098,7 @@ class Interface:
if helpers.allow_popup_window(account): if helpers.allow_popup_window(account):
dialogs.VoIPCallReceivedDialog(account, peerjid, sid) dialogs.VoIPCallReceivedDialog(account, peerjid, sid)
return
self.add_event(account, peerjid, 'voip-incoming', (peerjid, sid,)) self.add_event(account, peerjid, 'voip-incoming', (peerjid, sid,))