Merge changes from default branch into refactoring branch
This commit is contained in:
commit
8b3a82ff12
|
@ -1,5 +1,5 @@
|
|||
AC_INIT([Gajim - A Jabber Instant Messager],
|
||||
[0.12.5.8-dev],[http://trac.gajim.org/],[gajim])
|
||||
[0.13.0.1-dev],[http://trac.gajim.org/],[gajim])
|
||||
AC_PREREQ([2.59])
|
||||
|
||||
AC_CONFIG_HEADER(config.h)
|
||||
|
|
|
@ -98,6 +98,22 @@ class StandardChatCommands(CommandContainer):
|
|||
raise CommandError(_('Command is not supported for zeroconf accounts'))
|
||||
gajim.connections[self.account].sendPing(self.contact)
|
||||
|
||||
@command('dtmf')
|
||||
@documentation(_("Sends DTMF events through an open audio session"))
|
||||
def dtmf(self, events):
|
||||
if not self.audio_sid:
|
||||
raise CommandError(_("There is no open audio session with this contact"))
|
||||
# Valid values for DTMF tones are *, # or a number
|
||||
events = [event for event in events
|
||||
if event in ('*', '#') or event.isdigit()]
|
||||
if events:
|
||||
session = gajim.connections[self.account].get_jingle_session(
|
||||
self.contact.get_full_jid(), self.audio_sid)
|
||||
content = session.get_content('audio')
|
||||
content.batch_dtmf(events)
|
||||
else:
|
||||
raise CommandError(_("No valid DTMF event specified"))
|
||||
|
||||
@command('audio')
|
||||
@documentation(_("Toggle audio session"))
|
||||
def audio(self):
|
||||
|
|
|
@ -37,8 +37,11 @@ import base64
|
|||
import hashlib
|
||||
|
||||
from common.xmpp import NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES
|
||||
from common.xmpp import NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO
|
||||
from common.xmpp import NS_JINGLE_RTP_VIDEO
|
||||
# Features where we cannot safely assume that the other side supports them
|
||||
FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION]
|
||||
FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION,
|
||||
NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO]
|
||||
|
||||
# Query entry status codes
|
||||
NEW = 0
|
||||
|
|
|
@ -207,6 +207,7 @@ class Contacts:
|
|||
|
||||
def change_account_name(self, old_name, new_name):
|
||||
self._accounts[new_name] = self._accounts[old_name]
|
||||
self._accounts[new_name].name = new_name
|
||||
del self._accounts[old_name]
|
||||
|
||||
self._metacontact_manager.change_account_name(old_name, new_name)
|
||||
|
|
1044
src/common/jingle.py
1044
src/common/jingle.py
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,109 @@
|
|||
##
|
||||
## Copyright (C) 2006 Gajim Team
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published
|
||||
## by the Free Software Foundation; version 2 only.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
''' Handles Jingle contents (XEP 0166). '''
|
||||
|
||||
contents = {}
|
||||
|
||||
def get_jingle_content(node):
|
||||
namespace = node.getNamespace()
|
||||
if namespace in contents:
|
||||
return contents[namespace](node)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class JingleContent(object):
|
||||
''' An abstraction of content in Jingle sessions. '''
|
||||
def __init__(self, session, transport):
|
||||
self.session = session
|
||||
self.transport = transport
|
||||
# will be filled by JingleSession.add_content()
|
||||
# don't uncomment these lines, we will catch more buggy code then
|
||||
# (a JingleContent not added to session shouldn't send anything)
|
||||
#self.creator = None
|
||||
#self.name = None
|
||||
self.accepted = False
|
||||
self.sent = False
|
||||
|
||||
self.media = None
|
||||
|
||||
self.senders = 'both' #FIXME
|
||||
self.allow_sending = True # Used for stream direction, attribute 'senders'
|
||||
|
||||
self.callbacks = {
|
||||
# these are called when *we* get stanzas
|
||||
'content-accept': [self.__transportInfoCB],
|
||||
'content-add': [self.__transportInfoCB],
|
||||
'content-modify': [],
|
||||
'content-reject': [],
|
||||
'content-remove': [],
|
||||
'description-info': [],
|
||||
'security-info': [],
|
||||
'session-accept': [self.__transportInfoCB],
|
||||
'session-info': [],
|
||||
'session-initiate': [self.__transportInfoCB],
|
||||
'session-terminate': [],
|
||||
'transport-info': [self.__transportInfoCB],
|
||||
'transport-replace': [],
|
||||
'transport-accept': [],
|
||||
'transport-reject': [],
|
||||
'iq-result': [],
|
||||
'iq-error': [],
|
||||
# these are called when *we* sent these stanzas
|
||||
'content-accept-sent': [self.__fillJingleStanza],
|
||||
'content-add-sent': [self.__fillJingleStanza],
|
||||
'session-initiate-sent': [self.__fillJingleStanza],
|
||||
'session-accept-sent': [self.__fillJingleStanza],
|
||||
'session-terminate-sent': [],
|
||||
}
|
||||
|
||||
def is_ready(self):
|
||||
return (self.accepted and not self.sent)
|
||||
|
||||
def add_remote_candidates(self, candidates):
|
||||
''' Add a list of candidates to the list of remote candidates. '''
|
||||
pass
|
||||
|
||||
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 = self.transport.parse_transport_stanza(
|
||||
content.getTag('transport'))
|
||||
if candidates:
|
||||
self.add_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 send_candidate(self, candidate):
|
||||
content = self.__content()
|
||||
content.addChild(self.transport.make_transport([candidate]))
|
||||
self.session.send_transport_info(content)
|
||||
|
||||
def __fillJingleStanza(self, stanza, content, error, action):
|
||||
''' Add our things to session-initiate stanza. '''
|
||||
self._fillContent(content)
|
||||
self.sent = True
|
||||
content.addChild(node=self.transport.make_transport())
|
||||
|
||||
def destroy(self):
|
||||
self.callbacks = None
|
||||
del self.session.contents[(self.creator, self.name)]
|
|
@ -0,0 +1,321 @@
|
|||
##
|
||||
## Copyright (C) 2006 Gajim Team
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published
|
||||
## by the Free Software Foundation; version 2 only.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
''' Handles Jingle RTP sessions (XEP 0167). '''
|
||||
|
||||
|
||||
import gobject
|
||||
|
||||
import xmpp
|
||||
import farsight, gst
|
||||
|
||||
from jingle_transport import JingleTransportICEUDP
|
||||
from jingle_content import contents, JingleContent
|
||||
|
||||
# TODO: Will that be even used?
|
||||
def get_first_gst_element(elements):
|
||||
''' Returns, if it exists, the first available element of the list. '''
|
||||
for name in elements:
|
||||
factory = gst.element_factory_find(name)
|
||||
if factory:
|
||||
return factory.create()
|
||||
|
||||
|
||||
class JingleRTPContent(JingleContent):
|
||||
def __init__(self, session, media, transport=None):
|
||||
if transport is None:
|
||||
transport = JingleTransportICEUDP()
|
||||
JingleContent.__init__(self, session, transport)
|
||||
self.media = media
|
||||
self._dtmf_running = False
|
||||
self.farsight_media = {'audio': farsight.MEDIA_TYPE_AUDIO,
|
||||
'video': farsight.MEDIA_TYPE_VIDEO}[media]
|
||||
self.got_codecs = False
|
||||
|
||||
self.candidates_ready = False # True when local candidates are prepared
|
||||
|
||||
self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB]
|
||||
self.callbacks['content-add'] += [self.__getRemoteCodecsCB]
|
||||
self.callbacks['content-accept'] += [self.__getRemoteCodecsCB,
|
||||
self.__contentAcceptCB]
|
||||
self.callbacks['session-accept'] += [self.__getRemoteCodecsCB,
|
||||
self.__contentAcceptCB]
|
||||
self.callbacks['session-accept-sent'] += [self.__contentAcceptCB]
|
||||
self.callbacks['content-accept-sent'] += [self.__contentAcceptCB]
|
||||
self.callbacks['session-terminate'] += [self.__stop]
|
||||
self.callbacks['session-terminate-sent'] += [self.__stop]
|
||||
|
||||
def setup_stream(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
|
||||
|
||||
self.p2psession = self.conference.new_session(self.farsight_media)
|
||||
|
||||
participant = self.conference.new_participant(self.session.peerjid)
|
||||
#FIXME: Consider a workaround, here...
|
||||
# pidgin and telepathy-gabble don't follow the XEP, and it won't work
|
||||
# due to bad controlling-mode
|
||||
params = {'controlling-mode': self.session.weinitiate,# 'debug': False}
|
||||
'stun-ip': '69.0.208.27', 'debug': False}
|
||||
|
||||
self.p2pstream = self.p2psession.new_stream(participant,
|
||||
farsight.DIRECTION_RECV, 'nice', params)
|
||||
|
||||
def is_ready(self):
|
||||
return (JingleContent.is_ready(self) and self.candidates_ready
|
||||
and self.p2psession.get_property('codecs-ready'))
|
||||
|
||||
def add_remote_candidates(self, candidates):
|
||||
JingleContent.add_remote_candidates(self, candidates)
|
||||
#FIXME: connectivity should not be etablished yet
|
||||
# Instead, it should be etablished after session-accept!
|
||||
if self.sent:
|
||||
self.p2pstream.set_remote_candidates(candidates)
|
||||
|
||||
def batch_dtmf(self, events):
|
||||
if self._dtmf_running:
|
||||
raise Exception #TODO: Proper exception
|
||||
self._dtmf_running = True
|
||||
self._start_dtmf(events.pop(0))
|
||||
gobject.timeout_add(500, self._next_dtmf, events)
|
||||
|
||||
def _next_dtmf(self, events):
|
||||
self._stop_dtmf()
|
||||
if events:
|
||||
self._start_dtmf(events.pop(0))
|
||||
gobject.timeout_add(500, self._next_dtmf, events)
|
||||
else:
|
||||
self._dtmf_running = False
|
||||
|
||||
def _start_dtmf(self, event):
|
||||
if event in ('*', '#'):
|
||||
event = {'*': farsight.DTMF_EVENT_STAR,
|
||||
'#': farsight.DTMF_EVENT_POUND}[event]
|
||||
else:
|
||||
event = int(event)
|
||||
self.p2psession.start_telephony_event(event, 2,
|
||||
farsight.DTMF_METHOD_RTP_RFC4733)
|
||||
|
||||
def _stop_dtmf(self):
|
||||
self.p2psession.stop_telephony_event(farsight.DTMF_METHOD_RTP_RFC4733)
|
||||
|
||||
def _fillContent(self, content):
|
||||
content.addChild(xmpp.NS_JINGLE_RTP + ' description',
|
||||
attrs={'media': self.media}, payload=self.iter_codecs())
|
||||
|
||||
def _setup_funnel(self):
|
||||
self.funnel = gst.element_factory_make('fsfunnel')
|
||||
self.pipeline.add(self.funnel)
|
||||
self.funnel.set_state(gst.STATE_PLAYING)
|
||||
self.sink.set_state(gst.STATE_PLAYING)
|
||||
self.funnel.link(self.sink)
|
||||
|
||||
def _on_src_pad_added(self, stream, pad, codec):
|
||||
if not self.funnel:
|
||||
self._setup_funnel()
|
||||
pad.link(self.funnel.get_pad('sink%d'))
|
||||
|
||||
def _on_gst_message(self, bus, message):
|
||||
if message.type == gst.MESSAGE_ELEMENT:
|
||||
name = message.structure.get_name()
|
||||
if name == 'farsight-new-active-candidate-pair':
|
||||
pass
|
||||
elif name == 'farsight-recv-codecs-changed':
|
||||
pass
|
||||
elif name == 'farsight-codecs-changed':
|
||||
if self.is_ready():
|
||||
self.session.on_session_state_changed(self)
|
||||
#TODO: description-info
|
||||
elif name == 'farsight-local-candidates-prepared':
|
||||
self.candidates_ready = True
|
||||
if self.is_ready():
|
||||
self.session.on_session_state_changed(self)
|
||||
elif name == 'farsight-new-local-candidate':
|
||||
candidate = message.structure['candidate']
|
||||
self.transport.candidates.append(candidate)
|
||||
if self.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_FAILED:
|
||||
reason = xmpp.Node('reason')
|
||||
reason.setTag('failed-transport')
|
||||
self.session._session_terminate(reason)
|
||||
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']
|
||||
else:
|
||||
print name
|
||||
|
||||
def __contentAcceptCB(self, stanza, content, error, action):
|
||||
if self.accepted:
|
||||
if self.transport.remote_candidates:
|
||||
self.p2pstream.set_remote_candidates(self.transport.remote_candidates)
|
||||
self.transport.remote_candidates = []
|
||||
#TODO: farsight.DIRECTION_BOTH only if senders='both'
|
||||
self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH)
|
||||
self.session.content_negociated(self.media)
|
||||
|
||||
def __getRemoteCodecsCB(self, stanza, content, error, action):
|
||||
''' Get peer codecs from what we get from peer. '''
|
||||
if self.got_codecs:
|
||||
return
|
||||
|
||||
codecs = []
|
||||
for codec in content.getTag('description').iterTags('payload-type'):
|
||||
c = farsight.Codec(int(codec['id']), codec['name'],
|
||||
self.farsight_media, int(codec['clockrate']))
|
||||
if 'channels' in codec:
|
||||
c.channels = int(codec['channels'])
|
||||
else:
|
||||
c.channels = 1
|
||||
c.optional_params = [(str(p['name']), str(p['value'])) for p in \
|
||||
codec.iterTags('parameter')]
|
||||
codecs.append(c)
|
||||
|
||||
if len(codecs) > 0:
|
||||
#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.got_codecs = True
|
||||
|
||||
def iter_codecs(self):
|
||||
codecs = self.p2psession.get_property('codecs')
|
||||
for codec in codecs:
|
||||
attrs = {'name': codec.encoding_name,
|
||||
'id': codec.id,
|
||||
'channels': codec.channels}
|
||||
if codec.clock_rate:
|
||||
attrs['clockrate'] = codec.clock_rate
|
||||
if codec.optional_params:
|
||||
payload = (xmpp.Node('parameter', {'name': name, 'value': value})
|
||||
for name, value in codec.optional_params)
|
||||
else: payload = ()
|
||||
yield xmpp.Node('payload-type', attrs, payload)
|
||||
|
||||
def __stop(self, *things):
|
||||
self.pipeline.set_state(gst.STATE_NULL)
|
||||
|
||||
def __del__(self):
|
||||
self.__stop()
|
||||
|
||||
def destroy(self):
|
||||
JingleContent.destroy(self)
|
||||
self.p2pstream.disconnect_by_func(self._on_src_pad_added)
|
||||
self.pipeline.get_bus().disconnect_by_func(self._on_gst_message)
|
||||
|
||||
|
||||
class JingleAudio(JingleRTPContent):
|
||||
''' Jingle VoIP sessions consist of audio content transported
|
||||
over an ICE UDP protocol. '''
|
||||
def __init__(self, session, transport=None):
|
||||
JingleRTPContent.__init__(self, session, 'audio', transport)
|
||||
self.setup_stream()
|
||||
|
||||
|
||||
''' Things to control the gstreamer's pipeline '''
|
||||
def setup_stream(self):
|
||||
JingleRTPContent.setup_stream(self)
|
||||
|
||||
# Configure SPEEX
|
||||
# Workaround for psi (not needed since rev
|
||||
# 147aedcea39b43402fe64c533d1866a25449888a):
|
||||
# place 16kHz before 8kHz, as buggy psi versions will take in
|
||||
# account only the first codec
|
||||
|
||||
codecs = [farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX',
|
||||
farsight.MEDIA_TYPE_AUDIO, 16000),
|
||||
farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX',
|
||||
farsight.MEDIA_TYPE_AUDIO, 8000)]
|
||||
self.p2psession.set_codec_preferences(codecs)
|
||||
|
||||
# the local parts
|
||||
# TODO: use gconfaudiosink?
|
||||
# sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink'])
|
||||
self.sink = gst.element_factory_make('alsasink')
|
||||
self.sink.set_property('sync', False)
|
||||
#sink.set_property('latency-time', 20000)
|
||||
#sink.set_property('buffer-time', 80000)
|
||||
|
||||
# TODO: use gconfaudiosrc?
|
||||
src_mic = gst.element_factory_make('alsasrc')
|
||||
src_mic.set_property('blocksize', 320)
|
||||
|
||||
self.mic_volume = gst.element_factory_make('volume')
|
||||
self.mic_volume.set_property('volume', 1)
|
||||
|
||||
# link gst elements
|
||||
self.pipeline.add(self.sink, src_mic, self.mic_volume)
|
||||
src_mic.link(self.mic_volume)
|
||||
|
||||
self.mic_volume.get_pad('src').link(self.p2psession.get_property(
|
||||
'sink-pad'))
|
||||
self.p2pstream.connect('src-pad-added', self._on_src_pad_added)
|
||||
|
||||
# The following is needed for farsight to process ICE requests:
|
||||
self.pipeline.set_state(gst.STATE_PLAYING)
|
||||
|
||||
|
||||
class JingleVideo(JingleRTPContent):
|
||||
def __init__(self, session, transport=None):
|
||||
JingleRTPContent.__init__(self, session, 'video', transport)
|
||||
self.setup_stream()
|
||||
|
||||
''' Things to control the gstreamer's pipeline '''
|
||||
def setup_stream(self):
|
||||
#TODO: Everything is not working properly:
|
||||
# sometimes, one window won't show up,
|
||||
# sometimes it'll freeze...
|
||||
JingleRTPContent.setup_stream(self)
|
||||
# the local parts
|
||||
src_vid = gst.element_factory_make('videotestsrc')
|
||||
src_vid.set_property('is-live', True)
|
||||
videoscale = gst.element_factory_make('videoscale')
|
||||
caps = gst.element_factory_make('capsfilter')
|
||||
caps.set_property('caps', gst.caps_from_string('video/x-raw-yuv, width=320, height=240'))
|
||||
colorspace = gst.element_factory_make('ffmpegcolorspace')
|
||||
|
||||
self.pipeline.add(src_vid, videoscale, caps, colorspace)
|
||||
gst.element_link_many(src_vid, videoscale, caps, colorspace)
|
||||
|
||||
self.sink = gst.element_factory_make('xvimagesink')
|
||||
self.pipeline.add(self.sink)
|
||||
|
||||
colorspace.get_pad('src').link(self.p2psession.get_property('sink-pad'))
|
||||
self.p2pstream.connect('src-pad-added', self._on_src_pad_added)
|
||||
|
||||
# The following is needed for farsight to process ICE requests:
|
||||
self.pipeline.set_state(gst.STATE_PLAYING)
|
||||
|
||||
|
||||
def get_content(desc):
|
||||
if desc['media'] == 'audio':
|
||||
return JingleAudio
|
||||
elif desc['media'] == 'video':
|
||||
return JingleVideo
|
||||
else:
|
||||
return None
|
||||
|
||||
contents[xmpp.NS_JINGLE_RTP] = get_content
|
|
@ -0,0 +1,613 @@
|
|||
##
|
||||
## Copyright (C) 2006 Gajim Team
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published
|
||||
## by the Free Software Foundation; version 2 only.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
''' Handles Jingle sessions (XEP 0166). '''
|
||||
|
||||
#TODO:
|
||||
# * Have JingleContent here
|
||||
# * 'senders' attribute of 'content' element
|
||||
# * security preconditions
|
||||
# * actions:
|
||||
# - content-modify
|
||||
# - description-info, session-info
|
||||
# - security-info
|
||||
# - transport-accept, transport-reject
|
||||
# - Tie-breaking
|
||||
# * timeout
|
||||
|
||||
import gajim #Get rid of that?
|
||||
import xmpp
|
||||
from jingle_transport import get_jingle_transport
|
||||
from jingle_content import get_jingle_content
|
||||
|
||||
#FIXME: Move it to JingleSession.States?
|
||||
class JingleStates(object):
|
||||
''' States in which jingle session may exist. '''
|
||||
ended = 0
|
||||
pending = 1
|
||||
active = 2
|
||||
|
||||
class OutOfOrder(Exception):
|
||||
''' Exception that should be raised when an action is received when in the wrong state. '''
|
||||
|
||||
class TieBreak(Exception):
|
||||
''' Exception that should be raised in case of a tie, when we overrule the other action. '''
|
||||
|
||||
class JingleSession(object):
|
||||
''' This represents one jingle session. '''
|
||||
def __init__(self, con, weinitiate, jid, sid=None):
|
||||
''' con -- connection object,
|
||||
weinitiate -- boolean, are we the initiator?
|
||||
jid - jid of the other entity'''
|
||||
self.contents = {} # negotiated contents
|
||||
self.connection = con # connection to use
|
||||
# our full jid
|
||||
#FIXME: Get rid of gajim here?
|
||||
self.ourjid = gajim.get_jid_from_account(self.connection.name) + '/' + \
|
||||
con.server_resource
|
||||
self.peerjid = jid # jid we connect to
|
||||
# jid we use as the initiator
|
||||
self.initiator = weinitiate and self.ourjid or self.peerjid
|
||||
# jid we use as the responder
|
||||
self.responder = weinitiate and self.peerjid or self.ourjid
|
||||
# are we an initiator?
|
||||
self.weinitiate = weinitiate
|
||||
# what state is session in? (one from JingleStates)
|
||||
self.state = JingleStates.ended
|
||||
if not sid:
|
||||
sid = con.connection.getAnID()
|
||||
self.sid = sid # sessionid
|
||||
|
||||
self.accepted = True # is this session accepted by user
|
||||
|
||||
# callbacks to call on proper contents
|
||||
# use .prepend() to add new callbacks, especially when you're going
|
||||
# to send error instead of ack
|
||||
self.callbacks = {
|
||||
'content-accept': [self.__contentAcceptCB, self.__broadcastCB,
|
||||
self.__defaultCB],
|
||||
'content-add': [self.__contentAddCB, self.__broadcastCB,
|
||||
self.__defaultCB], #TODO
|
||||
'content-modify': [self.__defaultCB], #TODO
|
||||
'content-reject': [self.__defaultCB, self.__contentRemoveCB], #TODO
|
||||
'content-remove': [self.__defaultCB, self.__contentRemoveCB],
|
||||
'description-info': [self.__broadcastCB, self.__defaultCB], #TODO
|
||||
'security-info': [self.__defaultCB], #TODO
|
||||
'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB,
|
||||
self.__broadcastCB, self.__defaultCB],
|
||||
'session-info': [self.__sessionInfoCB, self.__broadcastCB, self.__defaultCB],
|
||||
'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB,
|
||||
self.__defaultCB],
|
||||
'session-terminate': [self.__sessionTerminateCB, self.__broadcastAllCB,
|
||||
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-error': [self.__errorCB],
|
||||
}
|
||||
|
||||
''' Interaction with user '''
|
||||
def approve_session(self):
|
||||
''' Called when user accepts session in UI (when we aren't the initiator).
|
||||
'''
|
||||
self.accept_session()
|
||||
|
||||
def decline_session(self):
|
||||
''' Called when user declines session in UI (when we aren't the initiator)
|
||||
'''
|
||||
reason = xmpp.Node('reason')
|
||||
reason.addChild('decline')
|
||||
self._session_terminate(reason)
|
||||
|
||||
def approve_content(self, media):
|
||||
content = self.get_content(media)
|
||||
if content:
|
||||
content.accepted = True
|
||||
self.on_session_state_changed(content)
|
||||
|
||||
def reject_content(self, media):
|
||||
content = self.get_content(media)
|
||||
if content:
|
||||
if self.state == JingleStates.active:
|
||||
self.__content_reject(content)
|
||||
content.destroy()
|
||||
self.on_session_state_changed()
|
||||
|
||||
def end_session(self):
|
||||
''' Called when user stops or cancel session in UI. '''
|
||||
reason = xmpp.Node('reason')
|
||||
if self.state == JingleStates.active:
|
||||
reason.addChild('success')
|
||||
else:
|
||||
reason.addChild('cancel')
|
||||
self._session_terminate(reason)
|
||||
|
||||
''' Middle-level functions to manage contents. Handle local content
|
||||
cache and send change notifications. '''
|
||||
def get_content(self, media=None):
|
||||
if media is None:
|
||||
return None
|
||||
|
||||
for content in self.contents.values():
|
||||
if content.media == media:
|
||||
return content
|
||||
|
||||
def add_content(self, name, content, creator='we'):
|
||||
''' Add new content to session. If the session is active,
|
||||
this will send proper stanza to update session.
|
||||
Creator must be one of ('we', 'peer', 'initiator', 'responder')'''
|
||||
assert creator in ('we', 'peer', 'initiator', 'responder')
|
||||
|
||||
if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \
|
||||
not self.weinitiate):
|
||||
creator = 'initiator'
|
||||
elif (creator == 'peer' and self.weinitiate) or (creator == 'we' and \
|
||||
not self.weinitiate):
|
||||
creator = 'responder'
|
||||
content.creator = creator
|
||||
content.name = name
|
||||
self.contents[(creator, name)] = content
|
||||
|
||||
if (creator == 'initiator') == self.weinitiate:
|
||||
# The content is from us, accept it
|
||||
content.accepted = True
|
||||
|
||||
def remove_content(self, creator, name):
|
||||
''' We do not need this now '''
|
||||
#TODO:
|
||||
if (creator, name) in self.contents:
|
||||
content = self.contents[(creator, name)]
|
||||
if len(self.contents) > 1:
|
||||
self.__content_remove(content)
|
||||
self.contents[(creator, name)].destroy()
|
||||
if len(self.contents) == 0:
|
||||
self.end_session()
|
||||
|
||||
def modify_content(self, creator, name, *someother):
|
||||
''' We do not need this now '''
|
||||
pass
|
||||
|
||||
def on_session_state_changed(self, content=None):
|
||||
if self.state == JingleStates.ended:
|
||||
# Session not yet started, only one action possible: session-initiate
|
||||
if self.is_ready() and self.weinitiate:
|
||||
self.__session_initiate()
|
||||
elif self.state == JingleStates.pending:
|
||||
# We can either send a session-accept or a content-add
|
||||
if self.is_ready() and not self.weinitiate:
|
||||
self.__session_accept()
|
||||
elif content and (content.creator == 'initiator') == self.weinitiate:
|
||||
self.__content_add(content)
|
||||
elif content and self.weinitiate:
|
||||
self.__content_accept(content)
|
||||
elif self.state == JingleStates.active:
|
||||
# We can either send a content-add or a content-accept
|
||||
if not content:
|
||||
return
|
||||
if (content.creator == 'initiator') == self.weinitiate:
|
||||
# We initiated this content. It's a pending content-add.
|
||||
self.__content_add(content)
|
||||
else:
|
||||
# The other side created this content, we accept it.
|
||||
self.__content_accept(content)
|
||||
|
||||
def is_ready(self):
|
||||
''' Returns True when all codecs and candidates are ready
|
||||
(for all contents). '''
|
||||
return (all((content.is_ready() for content in self.contents.itervalues()))
|
||||
and self.accepted)
|
||||
|
||||
''' Middle-level function to do stanza exchange. '''
|
||||
def accept_session(self):
|
||||
''' Mark the session as accepted. '''
|
||||
self.accepted = True
|
||||
self.on_session_state_changed()
|
||||
|
||||
def start_session(self):
|
||||
''' Mark the session as ready to be started. '''
|
||||
self.accepted = True
|
||||
self.on_session_state_changed()
|
||||
|
||||
def send_session_info(self):
|
||||
pass
|
||||
|
||||
def send_content_accept(self, content):
|
||||
assert self.state != JingleStates.ended
|
||||
stanza, jingle = self.__make_jingle('content-accept')
|
||||
jingle.addChild(node=content)
|
||||
self.connection.connection.send(stanza)
|
||||
|
||||
def send_transport_info(self, content):
|
||||
assert self.state != JingleStates.ended
|
||||
stanza, jingle = self.__make_jingle('transport-info')
|
||||
jingle.addChild(node=content)
|
||||
self.connection.connection.send(stanza)
|
||||
|
||||
''' Session callbacks. '''
|
||||
def stanzaCB(self, stanza):
|
||||
''' A callback for ConnectionJingle. It gets stanza, then
|
||||
tries to send it to all internally registered callbacks.
|
||||
First one to raise xmpp.NodeProcessed breaks function.'''
|
||||
jingle = stanza.getTag('jingle')
|
||||
error = stanza.getTag('error')
|
||||
if error:
|
||||
# it's an iq-error stanza
|
||||
action = 'iq-error'
|
||||
elif jingle:
|
||||
# it's a jingle action
|
||||
action = jingle.getAttr('action')
|
||||
if action not in self.callbacks:
|
||||
self.__send_error(stanza, 'bad_request')
|
||||
return
|
||||
#FIXME: If we aren't initiated and it's not a session-initiate...
|
||||
if action != 'session-initiate' and self.state == JingleStates.ended:
|
||||
self.__send_error(stanza, 'item-not-found', 'unknown-session')
|
||||
return
|
||||
else:
|
||||
# it's an iq-result (ack) stanza
|
||||
action = 'iq-result'
|
||||
|
||||
callables = self.callbacks[action]
|
||||
|
||||
try:
|
||||
for callable in callables:
|
||||
callable(stanza=stanza, jingle=jingle, error=error, action=action)
|
||||
except xmpp.NodeProcessed:
|
||||
pass
|
||||
except TieBreak:
|
||||
self.__send_error(stanza, 'conflict', 'tiebreak')
|
||||
except OutOfOrder:
|
||||
self.__send_error(stanza, 'unexpected-request', 'out-of-order')#FIXME
|
||||
|
||||
def __defaultCB(self, stanza, jingle, error, action):
|
||||
''' Default callback for action stanzas -- simple ack
|
||||
and stop processing. '''
|
||||
response = stanza.buildReply('result')
|
||||
self.connection.connection.send(response)
|
||||
|
||||
def __errorCB(self, stanza, jingle, error, action):
|
||||
#FIXME
|
||||
text = error.getTagData('text')
|
||||
jingle_error = None
|
||||
xmpp_error = None
|
||||
for child in error.getChildren():
|
||||
if child.getNamespace() == xmpp.NS_JINGLE_ERRORS:
|
||||
jingle_error = child.getName()
|
||||
elif child.getNamespace() == xmpp.NS_STANZAS:
|
||||
xmpp_error = child.getName()
|
||||
self.__dispatch_error(xmpp_error, jingle_error, text)
|
||||
#FIXME: Not sure when we would want to do that...
|
||||
if xmpp_error == 'item-not-found':
|
||||
self.connection.delete_jingle_session(self.peerjid, self.sid)
|
||||
|
||||
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.__make_jingle('transport-reject')
|
||||
content = jingle.setTag('content', attrs={'creator': creator,
|
||||
'name': name})
|
||||
content.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.__make_jingle('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):
|
||||
#TODO: ringing, active, (un)hold, (un)mute
|
||||
payload = jingle.getPayload()
|
||||
if len(payload) > 0:
|
||||
self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info')
|
||||
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:
|
||||
content = self.contents[(creator, name)]
|
||||
#TODO: this will fail if content is not an RTP content
|
||||
self.connection.dispatch('JINGLE_DISCONNECTED',
|
||||
(self.peerjid, self.sid, content.media, 'removed'))
|
||||
content.destroy()
|
||||
if len(self.contents) == 0:
|
||||
reason = xmpp.Node('reason')
|
||||
reason.setTag('success')
|
||||
self._session_terminate(reason)
|
||||
|
||||
def __sessionAcceptCB(self, stanza, jingle, error, action):
|
||||
if self.state != JingleStates.pending: #FIXME
|
||||
raise OutOfOrder
|
||||
self.state = JingleStates.active
|
||||
|
||||
def __contentAcceptCB(self, stanza, jingle, error, action):
|
||||
''' Called when we get content-accept stanza or equivalent one
|
||||
(like session-accept).'''
|
||||
# check which contents are accepted
|
||||
for content in jingle.iterTags('content'):
|
||||
creator = content['creator']
|
||||
name = content['name']#TODO...
|
||||
|
||||
def __contentAddCB(self, stanza, jingle, error, action):
|
||||
if self.state == JingleStates.ended:
|
||||
raise OutOfOrder
|
||||
|
||||
parse_result = self.__parse_contents(jingle)
|
||||
contents = parse_result[2]
|
||||
rejected_contents = parse_result[3]
|
||||
|
||||
for name, creator in rejected_contents:
|
||||
#TODO:
|
||||
content = JingleContent()
|
||||
self.add_content(name, content, creator)
|
||||
self.__content_reject(content)
|
||||
self.contents[(content.creator, content.name)].destroy()
|
||||
|
||||
self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid,
|
||||
contents))
|
||||
|
||||
def __sessionInitiateCB(self, stanza, jingle, error, action):
|
||||
''' We got a jingle session request from other entity,
|
||||
therefore we are the receiver... Unpack the data,
|
||||
inform the user. '''
|
||||
|
||||
if self.state != JingleStates.ended:
|
||||
raise OutOfOrder
|
||||
|
||||
self.initiator = jingle['initiator']
|
||||
self.responder = self.ourjid
|
||||
self.peerjid = self.initiator
|
||||
self.accepted = False # user did not accept this session yet
|
||||
|
||||
# TODO: If the initiator is unknown to the receiver (e.g., via presence
|
||||
# subscription) and the receiver has a policy of not communicating via
|
||||
# Jingle with unknown entities, it SHOULD return a <service-unavailable/>
|
||||
# error.
|
||||
|
||||
# Lets check what kind of jingle session does the peer want
|
||||
contents_ok, transports_ok, contents, pouet = self.__parse_contents(jingle)
|
||||
|
||||
# If there's no content we understand...
|
||||
if not contents_ok:
|
||||
# TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate
|
||||
reason = xmpp.Node('reason')
|
||||
reason.setTag('unsupported-applications')
|
||||
self.__defaultCB(stanza, jingle, error, action)
|
||||
self._session_terminate(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._session_terminate(reason)
|
||||
raise xmpp.NodeProcessed
|
||||
|
||||
self.state = JingleStates.pending
|
||||
|
||||
# Send event about starting a session
|
||||
self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid,
|
||||
contents))
|
||||
|
||||
def __broadcastCB(self, stanza, jingle, error, action):
|
||||
''' Broadcast the stanza contents to proper content handlers. '''
|
||||
for content in jingle.iterTags('content'):
|
||||
name = content['name']
|
||||
creator = content['creator']
|
||||
cn = self.contents[(creator, name)]
|
||||
cn.stanzaCB(stanza, content, error, action)
|
||||
|
||||
def __sessionTerminateCB(self, stanza, jingle, error, action):
|
||||
self.connection.delete_jingle_session(self.peerjid, self.sid)
|
||||
reason, text = self.__reason_from_stanza(jingle)
|
||||
if reason not in ('success', 'cancel', 'decline'):
|
||||
self.__dispatch_error(reason, reason, text)
|
||||
if text:
|
||||
text = '%s (%s)' % (reason, text)
|
||||
else:
|
||||
text = reason#TODO
|
||||
self.connection.dispatch('JINGLE_DISCONNECTED',
|
||||
(self.peerjid, self.sid, None, text))
|
||||
|
||||
def __broadcastAllCB(self, stanza, jingle, error, action):
|
||||
''' Broadcast the stanza to all content handlers. '''
|
||||
for content in self.contents.itervalues():
|
||||
content.stanzaCB(stanza, None, error, action)
|
||||
|
||||
''' Internal methods. '''
|
||||
def __parse_contents(self, jingle):
|
||||
#TODO: Needs some reworking
|
||||
contents = []
|
||||
contents_rejected = []
|
||||
contents_ok = False
|
||||
transports_ok = False
|
||||
|
||||
for element in jingle.iterTags('content'):
|
||||
transport = get_jingle_transport(element.getTag('transport'))
|
||||
content_type = get_jingle_content(element.getTag('description'))
|
||||
if content_type:
|
||||
contents_ok = True
|
||||
if transport:
|
||||
content = content_type(self, transport)
|
||||
self.add_content(element['name'],
|
||||
content, 'peer')
|
||||
contents.append((content.media,))
|
||||
transports_ok = True
|
||||
else:
|
||||
contents_rejected.append((element['name'], 'peer'))
|
||||
else:
|
||||
contents_rejected.append((element['name'], 'peer'))
|
||||
|
||||
return (contents_ok, transports_ok, contents, contents_rejected)
|
||||
|
||||
def __dispatch_error(self, error, jingle_error=None, text=None):
|
||||
if jingle_error:
|
||||
error = jingle_error
|
||||
if text:
|
||||
text = '%s (%s)' % (error, text)
|
||||
else:
|
||||
text = error
|
||||
self.connection.dispatch('JINGLE_ERROR', (self.peerjid, self.sid, text))
|
||||
|
||||
def __reason_from_stanza(self, stanza):
|
||||
reason = 'success'
|
||||
reasons = ['success', 'busy', 'cancel', 'connectivity-error',
|
||||
'decline', 'expired', 'failed-application', 'failed-transport',
|
||||
'general-error', 'gone', 'incompatible-parameters', 'media-error',
|
||||
'security-error', 'timeout', 'unsupported-applications',
|
||||
'unsupported-transports']
|
||||
tag = stanza.getTag('reason')
|
||||
if tag:
|
||||
text = tag.getTagData('text')
|
||||
for r in reasons:
|
||||
if tag.getTag(r):
|
||||
reason = r
|
||||
break
|
||||
return (reason, text)
|
||||
|
||||
''' Methods that make/send proper pieces of XML. They check if the session
|
||||
is in appropriate state. '''
|
||||
def __make_jingle(self, action):
|
||||
stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid))
|
||||
attrs = {'action': action,
|
||||
'sid': self.sid}
|
||||
if action == 'session-initiate':
|
||||
attrs['initiator'] = self.initiator
|
||||
elif action == 'session-accept':
|
||||
attrs['responder'] = self.responder
|
||||
jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE)
|
||||
return stanza, jingle
|
||||
|
||||
def __send_error(self, stanza, error, jingle_error=None, text=None):
|
||||
err = xmpp.Error(stanza, error)
|
||||
err.setNamespace(xmpp.NS_STANZAS)
|
||||
if jingle_error:
|
||||
err.setTag(jingle_error, namespace=xmpp.NS_JINGLE_ERRORS)
|
||||
if text:
|
||||
err.setTagData('text', text)
|
||||
self.connection.connection.send(err)
|
||||
self.__dispatch_error(error, jingle_error, text)
|
||||
|
||||
def __append_content(self, jingle, content):
|
||||
''' Append <content/> element to <jingle/> element,
|
||||
with (full=True) or without (full=False) <content/>
|
||||
children. '''
|
||||
jingle.addChild('content',
|
||||
attrs={'name': content.name, 'creator': content.creator})
|
||||
|
||||
def __append_contents(self, jingle):
|
||||
''' Append all <content/> elements to <jingle/>.'''
|
||||
# TODO: integrate with __appendContent?
|
||||
# TODO: parameters 'name', 'content'?
|
||||
for content in self.contents.values():
|
||||
self.__append_content(jingle, content)
|
||||
|
||||
def __session_initiate(self):
|
||||
assert self.state == JingleStates.ended
|
||||
stanza, jingle = self.__make_jingle('session-initiate')
|
||||
self.__append_contents(jingle)
|
||||
self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent')
|
||||
self.connection.connection.send(stanza)
|
||||
self.state = JingleStates.pending
|
||||
|
||||
def __session_accept(self):
|
||||
assert self.state == JingleStates.pending
|
||||
stanza, jingle = self.__make_jingle('session-accept')
|
||||
self.__append_contents(jingle)
|
||||
self.__broadcastCB(stanza, jingle, None, 'session-accept-sent')
|
||||
self.connection.connection.send(stanza)
|
||||
self.state = JingleStates.active
|
||||
|
||||
def __session_info(self, payload=None):
|
||||
assert self.state != JingleStates.ended
|
||||
stanza, jingle = self.__make_jingle('session-info')
|
||||
if payload:
|
||||
jingle.addChild(node=payload)
|
||||
self.connection.connection.send(stanza)
|
||||
|
||||
def _session_terminate(self, reason=None):
|
||||
assert self.state != JingleStates.ended
|
||||
stanza, jingle = self.__make_jingle('session-terminate')
|
||||
if reason is not None:
|
||||
jingle.addChild(node=reason)
|
||||
self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent')
|
||||
self.connection.connection.send(stanza)
|
||||
reason, text = self.__reason_from_stanza(jingle)
|
||||
if reason not in ('success', 'cancel', 'decline'):
|
||||
self.__dispatch_error(reason, reason, text)
|
||||
if text:
|
||||
text = '%s (%s)' % (reason, text)
|
||||
else:
|
||||
text = reason
|
||||
self.connection.delete_jingle_session(self.peerjid, self.sid)
|
||||
self.connection.dispatch('JINGLE_DISCONNECTED',
|
||||
(self.peerjid, self.sid, None, text))
|
||||
|
||||
def __content_add(self, content):
|
||||
#TODO: test
|
||||
assert self.state != JingleStates.ended
|
||||
stanza, jingle = self.__make_jingle('content-add')
|
||||
self.__append_content(jingle, content)
|
||||
self.__broadcastCB(stanza, jingle, None, 'content-add-sent')
|
||||
self.connection.connection.send(stanza)
|
||||
|
||||
def __content_accept(self, content):
|
||||
#TODO: test
|
||||
assert self.state != JingleStates.ended
|
||||
stanza, jingle = self.__make_jingle('content-accept')
|
||||
self.__append_content(jingle, content)
|
||||
self.__broadcastCB(stanza, jingle, None, 'content-accept-sent')
|
||||
self.connection.connection.send(stanza)
|
||||
|
||||
def __content_reject(self, content):
|
||||
assert self.state != JingleStates.ended
|
||||
stanza, jingle = self.__make_jingle('content-reject')
|
||||
self.__append_content(jingle, content)
|
||||
self.connection.connection.send(stanza)
|
||||
#TODO: this will fail if content is not an RTP content
|
||||
self.connection.dispatch('JINGLE_DISCONNECTED',
|
||||
(self.peerjid, self.sid, content.media, 'rejected'))
|
||||
|
||||
def __content_modify(self):
|
||||
assert self.state != JingleStates.ended
|
||||
|
||||
def __content_remove(self, content):
|
||||
assert self.state != JingleStates.ended
|
||||
stanza, jingle = self.__make_jingle('content-remove')
|
||||
self.__append_content(jingle, content)
|
||||
self.connection.connection.send(stanza)
|
||||
#TODO: this will fail if content is not an RTP content
|
||||
self.connection.dispatch('JINGLE_DISCONNECTED',
|
||||
(self.peerjid, self.sid, content.media, 'removed'))
|
||||
|
||||
def content_negociated(self, media):
|
||||
self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid,
|
||||
media))
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
##
|
||||
## Copyright (C) 2006 Gajim Team
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published
|
||||
## by the Free Software Foundation; version 2 only.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
''' Handles Jingle Transports (currently only ICE-UDP). '''
|
||||
|
||||
import xmpp
|
||||
|
||||
transports = {}
|
||||
|
||||
def get_jingle_transport(node):
|
||||
namespace = node.getNamespace()
|
||||
if namespace in transports:
|
||||
return transports[namespace]()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class TransportType(object):
|
||||
''' Possible types of a JingleTransport '''
|
||||
datagram = 1
|
||||
streaming = 2
|
||||
|
||||
|
||||
class JingleTransport(object):
|
||||
''' An abstraction of a transport in Jingle sessions. '''
|
||||
def __init__(self, type_):
|
||||
self.type = type_
|
||||
self.candidates = []
|
||||
self.remote_candidates = []
|
||||
|
||||
def _iter_candidates(self):
|
||||
for candidate in self.candidates:
|
||||
yield self.make_candidate(candidate)
|
||||
|
||||
def make_candidate(self, candidate):
|
||||
''' Build a candidate stanza for the given candidate. '''
|
||||
pass
|
||||
|
||||
def make_transport(self, candidates=None):
|
||||
''' Build a transport stanza with the given candidates (or
|
||||
self.candidates if candidates is None). '''
|
||||
if not candidates:
|
||||
candidates = self._iter_candidates()
|
||||
transport = xmpp.Node('transport', payload=candidates)
|
||||
return transport
|
||||
|
||||
def parse_transport_stanza(self, transport):
|
||||
''' Returns the list of transport candidates from a transport stanza. '''
|
||||
return []
|
||||
|
||||
|
||||
import farsight
|
||||
|
||||
class JingleTransportICEUDP(JingleTransport):
|
||||
def __init__(self):
|
||||
JingleTransport.__init__(self, TransportType.datagram)
|
||||
|
||||
def make_candidate(self, candidate):
|
||||
types = {farsight.CANDIDATE_TYPE_HOST: 'host',
|
||||
farsight.CANDIDATE_TYPE_SRFLX: 'srflx',
|
||||
farsight.CANDIDATE_TYPE_PRFLX: 'prflx',
|
||||
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 make_transport(self, candidates=None):
|
||||
transport = JingleTransport.make_transport(self, candidates)
|
||||
transport.setNamespace(xmpp.NS_JINGLE_ICE_UDP)
|
||||
if self.candidates and self.candidates[0].username and \
|
||||
self.candidates[0].password:
|
||||
transport.setAttr('ufrag', self.candidates[0].username)
|
||||
transport.setAttr('pwd', self.candidates[0].password)
|
||||
return transport
|
||||
|
||||
def parse_transport_stanza(self, transport):
|
||||
candidates = []
|
||||
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']]
|
||||
else:
|
||||
print 'Unknown type %s', candidate['type']
|
||||
candidates.append(cand)
|
||||
self.remote_candidates.extend(candidates)
|
||||
return candidates
|
||||
|
||||
transports[xmpp.NS_JINGLE_ICE_UDP] = JingleTransportICEUDP
|
||||
|
|
@ -610,8 +610,8 @@ class HistoryManager:
|
|||
liststore, list_of_paths))
|
||||
|
||||
def on_search_db_button_clicked(self, widget):
|
||||
text = self.search_entry.get_text()
|
||||
if text == '':
|
||||
text = self.search_entry.get_text().decode('utf-8')
|
||||
if not text:
|
||||
return
|
||||
|
||||
self.welcome_vbox.hide()
|
||||
|
|
Loading…
Reference in New Issue