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:
parent
94f3db54ec
commit
32ad59aa42
|
@ -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)
|
||||||
|
|
|
@ -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 = {}
|
||||||
|
|
|
@ -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. '''
|
||||||
|
if self.weinitiate and self.candidates_ready:
|
||||||
self.__sessionInitiate()
|
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':
|
||||||
|
contents_ok = True
|
||||||
|
if tran_ns == xmpp.NS_JINGLE_ICE_UDP:
|
||||||
# we've got voip content
|
# we've got voip content
|
||||||
self.addContent(element['name'], JingleVoiP(self), 'peer')
|
self.addContent(element['name'], JingleVoIP(self), 'peer')
|
||||||
contents.append(('VOIP',))
|
contents.append(('VOIP',))
|
||||||
fail = False
|
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:
|
|
||||||
|
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)
|
callback(stanza, content, error, action)
|
||||||
|
|
||||||
def __sessionInitiateSentCB(self, 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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,))
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue