Changeset 8558 for branches/jingle

Show
Ignore:
Timestamp:
08/24/07 01:42:31 (16 months ago)
Author:
liori
Message:

Jingle: dialog for accepting voice calls

Location:
branches/jingle
Files:
1 added
4 modified

Legend:

Unmodified
Added
Removed
  • branches/jingle/src/common/farsight/test-jingle.py

    r8557 r8558  
    5353                print "WW: stream.signal_native_candidates_prepared()" 
    5454                print "WW: stream.start()" 
    55                 exit() 
    5655                stream.signal_native_candidates_prepared() 
    5756                stream.start() 
  • branches/jingle/src/common/jingle.py

    r8532 r8558  
    1313''' Handles the jingle signalling protocol. ''' 
    1414 
     15# note: if there will be more types of sessions (possibly file transfer, 
     16# video...), split this file 
     17 
    1518import gajim 
     19import gobject 
    1620import xmpp 
    1721 
    18 # ugly hack 
     22# ugly hack, fixed in farsight 0.1.24 
    1923import sys, dl, gst 
    2024sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_GLOBAL) 
    2125import farsight 
    2226sys.setdlopenflags(dl.RTLD_NOW | dl.RTLD_LOCAL) 
     27 
     28def timeout_add_and_call(timeout, callable, *args, **kwargs): 
     29        ''' Call a callback once. If it returns True, add a timeout handler to call it more times. 
     30        Helper function. ''' 
     31        if callable(*args, **kwargs): 
     32                return gobject.timeout_add(timeout, callable, *args, **kwargs) 
     33        return -1       # gobject.source_remove will not object 
    2334 
    2435class JingleStates(object): 
     
    2839        active=2 
    2940 
    30 class Exception(object): pass 
    31 class WrongState(Exception): pass 
    32 class NoCommonCodec(Exception): pass 
     41class Error(Exception): pass 
     42class WrongState(Error): pass 
     43class NoSuchSession(Error): pass 
    3344 
    3445class JingleSession(object): 
     
    5566                self.sid=sid            # sessionid 
    5667 
     68                self.accepted=True      # is this session accepted by user 
     69 
    5770                # callbacks to call on proper contents 
    5871                # use .prepend() to add new callbacks, especially when you're going 
     
    7689                self.p2psession.connect('error', self.on_p2psession_error) 
    7790 
     91        ''' Interaction with user ''' 
     92        def approveSession(self): 
     93                ''' Called when user accepts session in UI (when we aren't the initiator).''' 
     94                self.accepted=True 
     95                self.acceptSession() 
     96 
     97        def declineSession(self): 
     98                ''' Called when user declines session in UI (when we aren't the initiator, 
     99                or when the user wants to stop session completly. ''' 
     100                self.__sessionTerminate() 
     101 
    78102        ''' Middle-level functions to manage contents. Handle local content 
    79103        cache and send change notifications. ''' 
     
    83107                The protocol prohibits changing that when pending. 
    84108                Creator must be one of ('we', 'peer', 'initiator', 'responder')''' 
     109                assert creator in ('we', 'peer', 'initiator', 'responder') 
     110 
    85111                if self.state==JingleStates.pending: 
    86112                        raise WrongState 
     
    105131                pass 
    106132 
     133        def acceptSession(self): 
     134                ''' Check if all contents and user agreed to start session. ''' 
     135                if not self.weinitiate and \ 
     136                    self.accepted and \ 
     137                    all(c.negotiated for c in self.contents.itervalues()): 
     138                        self.__sessionAccept() 
     139                else: 
    107140        ''' Middle-level function to do stanza exchange. ''' 
    108141        def startSession(self): 
     
    112145        def sendSessionInfo(self): pass 
    113146 
    114         ''' Callbacks. ''' 
     147        ''' Session callbacks. ''' 
    115148        def stanzaCB(self, stanza): 
    116149                ''' A callback for ConnectionJingle. It gets stanza, then 
     
    153186        def __sessionInitiateCB(self, stanza, jingle, error, action): 
    154187                ''' We got a jingle session request from other entity, 
    155                 therefore we are the receiver... Unpack the data. ''' 
     188                therefore we are the receiver... Unpack the data, 
     189                inform the user. ''' 
    156190                self.initiator = jingle['initiator'] 
    157191                self.responder = self.ourjid 
    158192                self.peerjid = self.initiator 
    159  
     193                self.accepted = False   # user did not accept this session yet 
     194 
     195                # TODO: If the initiator is unknown to the receiver (e.g., via presence 
     196                # subscription) and the receiver has a policy of not communicating via 
     197                # Jingle with unknown entities, it SHOULD return a <service-unavailable/> 
     198                # error. 
     199 
     200                # Lets check what kind of jingle session does the peer want 
    160201                fail = True 
     202                contents = [] 
    161203                for element in jingle.iterTags('content'): 
    162204                        # checking what kind of session this will be 
     
    166208                                # we've got voip content 
    167209                                self.addContent(element['name'], JingleVoiP(self), 'peer') 
     210                                contents.append(('VOIP',)) 
    168211                                fail = False 
    169212 
     213                # If there's no content we understand... 
    170214                if fail: 
    171215                        # TODO: we should send <unsupported-content/> inside too 
     216                        # TODO: delete this instance 
    172217                        self.connection.connection.send( 
    173218                                xmpp.Error(stanza, xmpp.NS_STANZAS + 'feature-not-implemented')) 
     
    176221 
    177222                self.state = JingleStates.pending 
     223 
     224                # Send event about starting a session 
     225                self.connection.dispatch('JINGLE_INCOMING', (self.initiator, self.sid, contents)) 
    178226 
    179227        def __broadcastCB(self, stanza, jingle, error, action): 
     
    199247                return stanza, jingle 
    200248 
    201         def __appendContent(self, jingle, content, full=True): 
     249        def __appendContent(self, jingle, content): 
    202250                ''' Append <content/> element to <jingle/> element, 
    203251                with (full=True) or without (full=False) <content/> 
    204252                children. ''' 
    205                 if full: 
    206                         jingle.addChild(node=content.toXML()) 
    207                 else: 
    208                         jingle.addChild('content', 
    209                                 attrs={'name': content.name, 'creator': content.creator}) 
    210  
    211         def __appendContents(self, jingle, full=True): 
     253                jingle.addChild('content', 
     254                        attrs={'name': content.name, 'creator': content.creator}) 
     255 
     256        def __appendContents(self, jingle): 
    212257                ''' Append all <content/> elements to <jingle/>.''' 
    213258                # TODO: integrate with __appendContent? 
    214259                # TODO: parameters 'name', 'content'? 
    215260                for content in self.contents.values(): 
    216                         self.__appendContent(jingle, content, full=full) 
     261                        self.__appendContent(jingle, content) 
    217262 
    218263        def __sessionInitiate(self): 
     
    220265                stanza, jingle = self.__makeJingle('session-initiate') 
    221266                self.__appendContents(jingle) 
     267                self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent') 
    222268                self.connection.connection.send(stanza) 
    223269 
    224270        def __sessionAccept(self): 
    225271                assert self.state==JingleStates.pending 
    226                 stanza, jingle = self.__jingle('session-accept') 
    227                 self.__appendContents(jingle, False) 
     272                stanza, jingle = self.__makeJingle('session-accept') 
     273                self.__appendContents(jingle) 
     274                self.__broadcastCB(stanza, jingle, None, 'session-accept-sent') 
    228275                self.connection.connection.send(stanza) 
    229276                self.state=JingleStates.active 
     
    231278        def __sessionInfo(self, payload=None): 
    232279                assert self.state!=JingleStates.ended 
    233                 stanza, jingle = self.__jingle('session-info') 
     280                stanza, jingle = self.__makeJingle('session-info') 
    234281                if payload: 
    235282                        jingle.addChild(node=payload) 
     
    238285        def __sessionTerminate(self): 
    239286                assert self.state!=JingleStates.ended 
    240                 stanza, jingle = self.__jingle('session-terminate') 
     287                stanza, jingle = self.__makeJingle('session-terminate') 
    241288                self.connection.connection.send(stanza) 
     289                self.__broadcastCB(stanza, jingle, None, 'session-terminate-sent') 
    242290 
    243291        def __contentAdd(self): 
     
    252300        def __contentRemove(self): 
    253301                assert self.state!=JingleStates.ended 
     302 
     303        def sendContentAccept(self, content): 
     304                assert self.state!=JingleStates.ended 
     305                stanza, jingle = self.__makeJingle('content-accept') 
     306                jingle.addChild(node=content) 
     307                self.connection.connection.send(stanza) 
    254308 
    255309        def sendTransportInfo(self, content): 
     
    271325                #self.creator = None 
    272326                #self.name = None 
     327                self.negotiated = False         # is this content already negotiated? 
    273328 
    274329class JingleVoiP(JingleContent): 
     
    289344                ''' Called when something related to our content was sent by peer. ''' 
    290345                callbacks = { 
     346                        # these are called when *we* get stanzas 
    291347                        'content-accept': [self.__getRemoteCodecsCB], 
    292348                        'content-add': [], 
    293349                        'content-modify': [], 
    294350                        'content-remove': [], 
    295                         'session-accept': [self.__getRemoteCodecsCB], 
     351                        'session-accept': [self.__getRemoteCodecsCB, self.__startMic], 
    296352                        'session-info': [], 
    297353                        'session-initiate': [self.__getRemoteCodecsCB], 
    298                         'session-terminate': [], 
     354                        'session-terminate': [self.__stop], 
    299355                        'transport-info': [self.__transportInfoCB], 
    300356                        'iq-result': [], 
    301357                        'iq-error': [], 
     358                        # these are called when *we* sent these stanzas 
     359                        'session-initiate-sent': [self.__sessionInitiateSentCB], 
     360                        'session-accept-sent': [self.__startMic], 
     361                        'session-terminate-sent': [self.__stop], 
    302362                }[action] 
    303363                for callback in callbacks: 
    304364                        callback(stanza, content, error, action) 
    305365 
     366        def __sessionInitiateSentCB(self, stanza, content, error, action): 
     367                ''' Add our things to session-initiate stanza. ''' 
     368                content.setAttr('profile', 'RTP/AVP') 
     369                content.addChild(xmpp.NS_JINGLE_AUDIO+' description', payload=self.iterCodecs()) 
     370                content.addChild(xmpp.NS_JINGLE_ICE_UDP+' transport') 
     371 
    306372        def __getRemoteCodecsCB(self, stanza, content, error, action): 
     373                ''' Get peer codecs from what we get from peer. ''' 
    307374                if self.got_codecs: return 
    308375 
     
    363430                        payload=payload) 
    364431 
    365         def setupStream(self): 
    366                 self.p2pstream = self.session.p2psession.create_stream( 
    367                         farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) 
    368                 self.p2pstream.set_property('transmitter', 'libjingle') 
    369                 self.p2pstream.connect('error', self.on_p2pstream_error) 
    370                 self.p2pstream.connect('new-active-candidate-pair', self.on_p2pstream_new_active_candidate_pair) 
    371                 self.p2pstream.connect('codec-changed', self.on_p2pstream_codec_changed) 
    372                 self.p2pstream.connect('native-candidates-prepared', self.on_p2pstream_native_candidates_prepared) 
    373                 self.p2pstream.connect('state-changed', self.on_p2pstream_state_changed) 
    374                 self.p2pstream.connect('new-native-candidate', self.on_p2pstream_new_native_candidate) 
    375  
    376                 self.p2pstream.set_remote_codecs(self.p2pstream.get_local_codecs()) 
    377  
    378                 self.p2pstream.prepare_transports() 
    379  
    380                 self.p2pstream.set_active_codec(8)      #??? 
    381  
    382                 sink = gst.element_factory_make('alsasink') 
    383                 sink.set_property('sync', False) 
    384                 sink.set_property('latency-time', 20000) 
    385                 sink.set_property('buffer-time', 80000) 
    386  
    387                 src = gst.element_factory_make('audiotestsrc') 
    388                 src.set_property('blocksize', 320) 
    389                 #src.set_property('latency-time', 20000) 
    390                 src.set_property('is-live', True) 
    391  
    392                 self.p2pstream.set_sink(sink) 
    393                 self.p2pstream.set_source(src) 
    394  
    395432        def on_p2pstream_error(self, *whatever): pass 
    396433        def on_p2pstream_new_active_candidate_pair(self, stream, native, remote): pass 
    397434        def on_p2pstream_codec_changed(self, stream, codecid): pass 
    398435        def on_p2pstream_native_candidates_prepared(self, *whatever): 
    399                 for candidate in self.p2pstream.get_native_candidate_list(): 
    400                         self.send_candidate(candidate) 
     436                pass 
     437 
    401438        def on_p2pstream_state_changed(self, stream, state, dir): 
    402439                if state==farsight.STREAM_STATE_CONNECTED: 
    403440                        stream.signal_native_candidates_prepared() 
    404441                        stream.start() 
     442                        self.pipeline.set_state(gst.STATE_PLAYING) 
     443 
     444                        self.negotiated = True 
     445                        if not self.session.weinitiate: 
     446                                self.session.sendContentAccept(self.__content((xmpp.Node('description', payload=self.iterCodecs()),))) 
     447                        self.session.acceptSession() 
     448 
    405449        def on_p2pstream_new_native_candidate(self, p2pstream, candidate_id): 
    406450                candidates = p2pstream.get_native_candidate(candidate_id) 
     
    408452                for candidate in candidates: 
    409453                        self.send_candidate(candidate) 
     454 
    410455        def send_candidate(self, candidate): 
    411456                attrs={ 
     
    443488                        yield xmpp.Node('payload-type', a, p) 
    444489 
     490        ''' Things to control the gstreamer's pipeline ''' 
     491        def setupStream(self): 
     492                # the pipeline 
     493                self.pipeline = gst.Pipeline() 
     494 
     495                # the network part 
     496                self.p2pstream = self.session.p2psession.create_stream( 
     497                        farsight.MEDIA_TYPE_AUDIO, farsight.STREAM_DIRECTION_BOTH) 
     498                self.p2pstream.set_pipeline(self.pipeline) 
     499                self.p2pstream.set_property('transmitter', 'libjingle') 
     500                self.p2pstream.connect('error', self.on_p2pstream_error) 
     501                self.p2pstream.connect('new-active-candidate-pair', self.on_p2pstream_new_active_candidate_pair) 
     502                self.p2pstream.connect('codec-changed', self.on_p2pstream_codec_changed) 
     503                self.p2pstream.connect('native-candidates-prepared', self.on_p2pstream_native_candidates_prepared) 
     504                self.p2pstream.connect('state-changed', self.on_p2pstream_state_changed) 
     505                self.p2pstream.connect('new-native-candidate', self.on_p2pstream_new_native_candidate) 
     506 
     507                self.p2pstream.set_remote_codecs(self.p2pstream.get_local_codecs()) 
     508 
     509                self.p2pstream.prepare_transports() 
     510 
     511                self.p2pstream.set_active_codec(8)      #??? 
     512 
     513                # the local parts 
     514                # TODO: use gconfaudiosink? 
     515                sink = gst.element_factory_make('alsasink') 
     516                sink.set_property('sync', False) 
     517                sink.set_property('latency-time', 20000) 
     518                sink.set_property('buffer-time', 80000) 
     519                self.pipeline.add(sink) 
     520 
     521                self.src_signal = gst.element_factory_make('audiotestsrc') 
     522                self.src_signal.set_property('blocksize', 320) 
     523                self.src_signal.set_property('freq', 440) 
     524                self.pipeline.add(self.src_signal) 
     525 
     526                # TODO: use gconfaudiosrc? 
     527                self.src_mic = gst.element_factory_make('alsasrc') 
     528                self.src_mic.set_property('blocksize', 320) 
     529                self.pipeline.add(self.src_mic) 
     530 
     531                self.mic_volume = gst.element_factory_make('volume') 
     532                self.mic_volume.set_property('volume', 0) 
     533                self.pipeline.add(self.mic_volume) 
     534 
     535                self.adder = gst.element_factory_make('adder') 
     536                self.pipeline.add(self.adder) 
     537 
     538                # link gst elements 
     539                self.src_signal.link(self.adder) 
     540                self.src_mic.link(self.mic_volume) 
     541                self.mic_volume.link(self.adder) 
     542 
     543                # this will actually start before the pipeline will be started. 
     544                # no worries, though; it's only a ringing sound 
     545                def signal(): 
     546                        while True: 
     547                                self.src_signal.set_property('volume', 0.5) 
     548                                yield True # wait 750 ms 
     549                                yield True # wait 750 ms 
     550                                self.src_signal.set_property('volume', 0) 
     551                                yield True # wait 750 ms 
     552                self.signal_cb_id = timeout_add_and_call(750, signal().__iter__().next) 
     553 
     554                self.p2pstream.set_sink(sink) 
     555                self.p2pstream.set_source(self.adder) 
     556 
     557        def __startMic(self, *things): 
     558                gobject.source_remove(self.signal_cb_id) 
     559                self.src_signal.set_property('volume', 0) 
     560                self.mic_volume.set_property('volume', 1) 
     561 
     562        def __stop(self, *things): 
     563                self.pipeline.set_state(gst.STATE_NULL) 
     564                gobject.source_remove(self.signal_cb_id) 
     565 
     566        def __del__(self): 
     567                self.__stop() 
     568 
    445569class ConnectionJingle(object): 
    446570        ''' This object depends on that it is a part of Connection class. ''' 
     
    485609                # do we need to create a new jingle object 
    486610                if (jid, sid) not in self.__sessions: 
    487                         # TODO: we should check its type here... 
    488611                        newjingle = JingleSession(con=self, weinitiate=False, jid=jid, sid=sid) 
    489612                        self.addJingle(newjingle) 
     
    502625                jingle.addContent('voice', JingleVoiP(jingle)) 
    503626                jingle.startSession() 
     627        def getJingleSession(self, jid, sid): 
     628                try: 
     629                        return self.__sessions[(jid, sid)] 
     630                except KeyError: 
     631                        raise NoSuchSession 
  • branches/jingle/src/dialogs.py

    r8402 r8558  
    32993299        def on_close_window(self, widget): 
    33003300                self.window.destroy() 
     3301 
     3302class VoIPCallReceivedDialog(object): 
     3303        def __init__(self, account, contact_jid, sid): 
     3304                self.account = account 
     3305                self.jid = contact_jid 
     3306                self.sid = sid 
     3307 
     3308                xml = gtkgui_helpers.get_glade('voip_call_received_dialog.glade') 
     3309                xml.signal_autoconnect(self) 
     3310                 
     3311                contact = gajim.contacts.get_first_contact_from_jid(account, contact_jid) 
     3312                if contact and contact.name: 
     3313                        contact_text = '%s (%s)' % (contact.name, contact_jid) 
     3314                else: 
     3315                        contact_text = contact_jid 
     3316 
     3317                # do the substitution 
     3318                dialog = xml.get_widget('voip_call_received_messagedialog') 
     3319                dialog.set_property('secondary-text', 
     3320                        dialog.get_property('secondary-text') % {'contact': contact_text}) 
     3321 
     3322                dialog.show_all() 
     3323 
     3324        def on_voip_call_received_messagedialog_close(self, dialog): 
     3325                return self.on_voip_call_received_messagedialog_response(dialog, gtk.RESPONSE_NO) 
     3326        def on_voip_call_received_messagedialog_response(self, dialog, response): 
     3327                # we've got response from user, either stop connecting or accept the call 
     3328                session = gajim.connections[self.account].getJingleSession(self.jid, self.sid) 
     3329                if response==gtk.RESPONSE_YES: 
     3330                        session.approveSession() 
     3331                else: # response==gtk.RESPONSE_NO 
     3332                        session.declineSession() 
     3333 
     3334                dialog.destroy() 
  • branches/jingle/src/gajim.py

    r8495 r8558  
    18651865                        is_modal = False, ok_handler = on_ok) 
    18661866 
     1867        def handle_event_jingle_incoming(self, account, data): 
     1868                # ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type, data...)) 
     1869                # TODO: conditional blocking if peer is not in roster 
     1870                 
     1871                # unpack data 
     1872                peerjid, sid, contents = data 
     1873                content_types = set(c[0] for c in contents) 
     1874 
     1875                # check type of jingle session 
     1876                if 'VOIP' in content_types: 
     1877                        # a voip session... 
     1878                        # we now handle only voip, so the only thing we will do here is 
     1879                        # not to return from function 
     1880                        pass 
     1881                else: 
     1882                        # unknown session type... it should be declined in common/jingle.py 
     1883                        return 
     1884 
     1885                if helpers.allow_popup_window(account): 
     1886                        dialogs.VoIPCallReceivedDialog(account, peerjid, sid) 
     1887 
     1888                # TODO: not checked 
     1889                self.add_event(account, peerjid, 'jingle-session', (sid, contents)) 
     1890 
     1891                if helpers.allow_showing_notification(account): 
     1892                        # TODO: we should use another pixmap ;-) 
     1893                        img = os.path.join(gajim.DATA_DIR, 'pixmaps', 'events', 
     1894                                'ft_request.png') 
     1895                        txt = _('%s wants to start a jingle session.') % gajim.get_name_from_jid( 
     1896                                account, jid) 
     1897                        path = gtkgui_helpers.get_path_to_generic_or_avatar(img) 
     1898                        event_type = _('Jingle Session Request') 
     1899                        notify.popup(event_type, jid, account, 'jingle-request', 
     1900                                path_to_image = path, title = event_type, text = txt) 
     1901 
    18671902        def read_sleepy(self): 
    18681903                '''Check idle status and change that status if needed''' 
     
    21932228                        'SEARCH_RESULT': self.handle_event_search_result, 
    21942229                        'RESOURCE_CONFLICT': self.handle_event_resource_conflict, 
     2230                        'JINGLE_INCOMING': self.handle_event_jingle_incoming, 
    21952231                } 
    21962232                gajim.handlers = self.handlers