diff options
author | David Laban <david.laban@collabora.co.uk> | 2011-01-31 18:23:54 +0000 |
---|---|---|
committer | David Laban <david.laban@collabora.co.uk> | 2011-01-31 18:24:31 +0000 |
commit | 776194c93cc447aede374f011dff5b1d49b2b0ed (patch) | |
tree | cda3b69c44c23d621986b71da8a460fff177fad8 | |
parent | abcfb59b2ae9613585515fd887f026591ffe77fd (diff) | |
parent | 2cf3c1e745c27dd48d71b05e5daa68479333885b (diff) |
Merge remote branch 'alsuren/streamedmedia_tests'
Reviewed-by: Will Thompson <will.thompson@collabora.co.uk>
-rw-r--r-- | src/sip-media-channel.c | 164 | ||||
-rw-r--r-- | tests/twisted/Makefile.am | 7 | ||||
-rw-r--r-- | tests/twisted/sofiatest.py | 30 | ||||
-rw-r--r-- | tests/twisted/voip/incoming-basics.py | 161 | ||||
-rw-r--r-- | tests/twisted/voip/outgoing-basics.py | 297 | ||||
-rw-r--r-- | tests/twisted/voip/voip_test.py | 172 |
6 files changed, 747 insertions, 84 deletions
diff --git a/src/sip-media-channel.c b/src/sip-media-channel.c index 8f896ed..b78a57f 100644 --- a/src/sip-media-channel.c +++ b/src/sip-media-channel.c @@ -191,7 +191,7 @@ tpsip_media_channel_constructed (GObject *obj) G_OBJECT_CLASS (tpsip_media_channel_parent_class); TpDBusDaemon *bus; TpHandleRepoIface *contact_repo; - TpIntSet *set; + TpIntSet *add; if (parent_object_class->constructed != NULL) parent_object_class->constructed (obj); @@ -219,16 +219,17 @@ tpsip_media_channel_constructed (GObject *obj) g_assert (priv->initiator != 0); tp_handle_ref (contact_repo, priv->initiator); - set = tp_intset_new (); - tp_intset_add (set, priv->initiator); - - tp_group_mixin_change_members (obj, "", set, NULL, NULL, NULL, 0, 0); - - tp_intset_destroy (set); + add = tp_intset_new_containing (priv->initiator); + tp_group_mixin_change_members (obj, "", add, NULL, NULL, NULL, 0, 0); + tp_intset_destroy (add); - /* Allow member adding; also, we implement the 0.17.6 properties */ + /* We start off with lots of flags, and then delete them as we work out what + * kind of channel we are, rather than trying to track what we need to + * add/remove over time. We should always have the right flags before we are + * advertised on the bus. */ tp_group_mixin_change_flags (obj, - TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_PROPERTIES, 0); + TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_REMOVE | + TP_CHANNEL_GROUP_FLAG_CAN_RESCIND | TP_CHANNEL_GROUP_FLAG_PROPERTIES, 0); } static void tpsip_media_channel_dispose (GObject *object); @@ -747,7 +748,14 @@ static void tpsip_media_channel_get_handle (TpSvcChannel *iface, DBusGMethodInvocation *context) { - tp_svc_channel_return_from_get_handle (context, 0, 0); + TpsipMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + TpsipMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->handle != 0) + tp_svc_channel_return_from_get_handle (context, TP_HANDLE_TYPE_CONTACT, + priv->handle); + else + tp_svc_channel_return_from_get_handle (context, TP_HANDLE_TYPE_NONE, 0); } /** @@ -1032,6 +1040,11 @@ tpsip_media_channel_create_initial_streams (TpsipMediaChannel *self) g_assert (priv->initiator != priv->handle); + /* RequestChannel(None, 0) => channel is anonymous: + * caller uses RequestStreams to set the peer and start the call. */ + if (priv->handle == 0) + return; + priv_outbound_call (self, priv->handle); g_assert (priv->session != NULL); @@ -1135,7 +1148,7 @@ tpsip_media_channel_peer_error (TpsipMediaChannel *self, const char* message) { TpGroupMixin *mixin = TP_GROUP_MIXIN (self); - TpIntSet *set; + TpIntSet *remove; guint reason = TP_CHANNEL_GROUP_CHANGE_REASON_ERROR; switch (status) @@ -1172,12 +1185,12 @@ tpsip_media_channel_peer_error (TpsipMediaChannel *self, if (message == NULL || !g_utf8_validate (message, -1, NULL)) message = ""; - set = tp_intset_new (); - tp_intset_add (set, peer); - tp_intset_add (set, mixin->self_handle); + remove = tp_intset_new (); + tp_intset_add (remove, peer); + tp_intset_add (remove, mixin->self_handle); tp_group_mixin_change_members ((GObject *)self, message, - NULL, set, NULL, NULL, peer, reason); - tp_intset_destroy (set); + NULL, remove, NULL, NULL, peer, reason); + tp_intset_destroy (remove); } guint @@ -1221,26 +1234,21 @@ priv_nua_i_bye_cb (TpsipMediaChannel *self, { TpsipMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); TpGroupMixin *mixin = TP_GROUP_MIXIN (self); - TpIntSet *set; + TpIntSet *remove; TpHandle peer; g_return_val_if_fail (priv->session != NULL, FALSE); peer = tpsip_media_session_get_peer (priv->session); - set = tp_intset_new (); - tp_intset_add (set, peer); - tp_intset_add (set, mixin->self_handle); + remove = tp_intset_new (); + tp_intset_add (remove, peer); + tp_intset_add (remove, mixin->self_handle); - tp_group_mixin_change_members ((GObject *) self, - "", - NULL, /* add */ - set, /* remove */ - NULL, - NULL, - peer, - TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + tp_group_mixin_change_members ((GObject *) self, "", + NULL, remove, NULL, NULL, + peer, TP_CHANNEL_GROUP_CHANGE_REASON_NONE); - tp_intset_destroy (set); + tp_intset_destroy (remove); return TRUE; } @@ -1253,7 +1261,7 @@ priv_nua_i_cancel_cb (TpsipMediaChannel *self, { TpsipMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); TpGroupMixin *mixin = TP_GROUP_MIXIN (self); - TpIntSet *set; + TpIntSet *remove; TpHandle actor = 0; TpHandle peer; const sip_reason_t *reason; @@ -1297,20 +1305,15 @@ priv_nua_i_cancel_cb (TpsipMediaChannel *self, if (message == NULL || !g_utf8_validate (message, -1, NULL)) message = ""; - set = tp_intset_new (); - tp_intset_add (set, peer); - tp_intset_add (set, mixin->self_handle); + remove = tp_intset_new (); + tp_intset_add (remove, peer); + tp_intset_add (remove, mixin->self_handle); - tp_group_mixin_change_members ((GObject *) self, - message, - NULL, /* add */ - set, /* remove */ - NULL, - NULL, - actor, - TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + tp_group_mixin_change_members ((GObject *) self, message, + NULL, remove, NULL, NULL, + actor, TP_CHANNEL_GROUP_CHANGE_REASON_NONE); - tp_intset_destroy (set); + tp_intset_destroy (remove); return TRUE; } @@ -1391,7 +1394,22 @@ priv_nua_i_state_cb (TpsipMediaChannel *self, TPSIP_CHANNEL_CALL_STATE_PROCEEDING_MASK); if (status < 300) - tpsip_media_session_accept (priv->session); + { + TpIntSet *add = tp_intset_new_containing (peer); + + tp_group_mixin_change_members ((GObject *) self, + "", + add, /* add */ + NULL, /* remove */ + NULL, + NULL, + peer, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + tp_intset_destroy (add); + + tpsip_media_session_accept (priv->session); + } else if (status == 491) tpsip_media_session_resolve_glare (priv->session); else @@ -1445,12 +1463,10 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session, switch (state) { case TPSIP_MEDIA_SESSION_STATE_INVITE_SENT: - set = tp_intset_new (); - g_assert (priv->initiator == self_handle); /* add the peer to remote pending */ - tp_intset_add (set, peer); + set = tp_intset_new_containing (peer); tp_group_mixin_change_members ((GObject *)channel, "", NULL, /* add */ @@ -1460,18 +1476,15 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session, self_handle, /* actor */ TP_CHANNEL_GROUP_CHANGE_REASON_INVITED); - /* update flags: allow removal and rescinding, no more adding */ - tp_group_mixin_change_flags ((GObject *)channel, - TP_CHANNEL_GROUP_FLAG_CAN_REMOVE | TP_CHANNEL_GROUP_FLAG_CAN_RESCIND, + /* update flags: no more adding */ + tp_group_mixin_change_flags ((GObject *)channel, 0, TP_CHANNEL_GROUP_FLAG_CAN_ADD); break; case TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED: - set = tp_intset_new (); - /* add ourself to local pending */ - tp_intset_add (set, self_handle); + set = tp_intset_new_containing (self_handle); tp_group_mixin_change_members ((GObject *) channel, "", NULL, /* add */ NULL, /* remove */ @@ -1480,10 +1493,15 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session, priv->initiator, /* actor */ TP_CHANNEL_GROUP_CHANGE_REASON_INVITED); - /* No adding more members to the incoming call, removing is OK */ - tp_group_mixin_change_flags ((GObject *) channel, - TP_CHANNEL_GROUP_FLAG_CAN_REMOVE, - TP_CHANNEL_GROUP_FLAG_CAN_ADD); + /* No adding more members to the incoming call. Therefore also not + * possible to add anyone to remote-pending, so rescinding would make + * utterly no sense. We also disallow removing the remote peer if + * we are not the initiator, so disallow that too. + * Removing yourself to end the call is not represented by group flags. + */ + tp_group_mixin_change_flags ((GObject *) channel, 0, + TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_REMOVE | + TP_CHANNEL_GROUP_FLAG_CAN_RESCIND); break; @@ -1493,10 +1511,8 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session, if (!tp_handle_set_is_member (mixin->remote_pending, peer)) break; /* no-op */ - set = tp_intset_new (); - /* the peer has promoted itself to members */ - tp_intset_add (set, peer); + set = tp_intset_new_containing (peer); tp_group_mixin_change_members ((GObject *)channel, "", set, /* add */ NULL, /* remove */ @@ -1509,10 +1525,8 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session, if (!tp_handle_set_is_member (mixin->local_pending, self_handle)) break; /* no-op */ - set = tp_intset_new (); - /* promote ourselves to members */ - tp_intset_add (set, self_handle); + set = tp_intset_new_containing (self_handle); tp_group_mixin_change_members ((GObject *)channel, "", set, /* add */ NULL, /* remove */ @@ -1521,11 +1535,12 @@ static void priv_session_state_changed_cb (TpsipMediaSession *session, self_handle, 0); } - /* update flags: allow removal, deny adding and rescinding */ - tp_group_mixin_change_flags ((GObject *)channel, - TP_CHANNEL_GROUP_FLAG_CAN_REMOVE, - TP_CHANNEL_GROUP_FLAG_CAN_ADD - | TP_CHANNEL_GROUP_FLAG_CAN_RESCIND); + /* update flags: deny adding and rescinding. Removing the remote peer is + * still allowed. + * Removing yourself to end the call is not represented by group flags. + */ + tp_group_mixin_change_flags ((GObject *)channel, 0, + TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_RESCIND); break; @@ -1717,7 +1732,7 @@ _tpsip_media_channel_add_member (GObject *iface, if (priv->initiator == mixin->self_handle) { - TpIntSet *set; + TpIntSet *remote_pending; /* case a: an old-school outbound call * (we are the initiator, a new handle added with AddMembers) */ @@ -1727,21 +1742,20 @@ _tpsip_media_channel_add_member (GObject *iface, /* Backwards compatible behavior: * add the peer to remote pending without waiting for the actual request * to be sent */ - set = tp_intset_new (); - tp_intset_add (set, handle); + remote_pending = tp_intset_new_containing (handle); tp_group_mixin_change_members (iface, "", NULL, /* add */ NULL, /* remove */ NULL, /* local pending */ - set, /* remote pending */ + remote_pending, /* remote pending */ mixin->self_handle, /* actor */ TP_CHANNEL_GROUP_CHANGE_REASON_INVITED); - tp_intset_destroy (set); + tp_intset_destroy (remote_pending); - /* update flags: allow removal and rescinding, no more adding */ - tp_group_mixin_change_flags (iface, - TP_CHANNEL_GROUP_FLAG_CAN_REMOVE | TP_CHANNEL_GROUP_FLAG_CAN_RESCIND, + /* update flags: no more adding. + * Removal and rescinding are still allowed. */ + tp_group_mixin_change_flags (iface, 0, TP_CHANNEL_GROUP_FLAG_CAN_ADD); return TRUE; diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index 086e437..14bdc06 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -1,3 +1,5 @@ +NULL = + TWISTED_TESTS = \ cm/protocol.py \ test-register.py \ @@ -6,7 +8,10 @@ TWISTED_TESTS = \ test-handle-normalisation.py \ test-message.py \ test-self-alias.py \ - text/initiate-requestotron.py + text/initiate-requestotron.py \ + voip/incoming-basics.py \ + voip/outgoing-basics.py \ + $(NULL) TESTS = diff --git a/tests/twisted/sofiatest.py b/tests/twisted/sofiatest.py index 94263b9..23142b9 100644 --- a/tests/twisted/sofiatest.py +++ b/tests/twisted/sofiatest.py @@ -9,6 +9,8 @@ from twisted.internet import reactor import os import sys +import random + import dbus import dbus.glib @@ -31,22 +33,31 @@ class SipProxy(sip.RegisterProxy): def handle_request(self, message, addr): if message.method == 'REGISTER': return sip.RegisterProxy.handle_request(self, message, addr) - if message.method == 'MESSAGE': - self.event_func(servicetest.Event('sip-message', + elif message.method == 'OPTIONS' and \ + 'REGISTRATION PROBE' == message.headers.get('subject','')[0]: + self.deliverResponse(self.responseFromRequest(200, message)) + else: + headers = {} + for key, values in message.headers.items(): + headers[key.replace('-', '_')] = values[0] + self.event_func(servicetest.Event('sip-%s' % message.method.lower(), uri=str(message.uri), headers=message.headers, body=message.body, - sip_message=message)) + sip_message=message, **headers)) def handle_response(self, message, addr): + headers = {} + for key, values in message.headers.items(): + headers[key.replace('-', '_')] = values[0] self.event_func(servicetest.Event('sip-response', code=message.code, headers=message.headers, body=message.body, - sip_message=message)) + sip_message=message, **headers)) def prepare_test(event_func, register_cb, params=None): actual_params = { 'account': 'testacc@127.0.0.1', 'password': 'testpwd', 'proxy-host': '127.0.0.1', - 'port': dbus.UInt16(9090), + 'port': dbus.UInt16(random.randint(9090, 9999)), 'local-ip-address': '127.0.0.1' } @@ -114,8 +125,8 @@ def exec_test(fun, params=None, register_cb=default_register_cb, timeout=None): return '\x1b[32m%s\x1b[0m' % s patterns = { - 'handled': green, - 'not handled': red, + 'handled,': green, + 'not hand': red, } class Colourer: @@ -124,8 +135,11 @@ def exec_test(fun, params=None, register_cb=default_register_cb, timeout=None): self.patterns = patterns def write(self, s): - f = self.patterns.get(s, lambda x: x) + f = self.patterns.get(s[:len('handled,')], lambda x: x) self.fh.write(f(s)) + + def isatty(self): + return False sys.stdout = Colourer(sys.stdout, patterns) diff --git a/tests/twisted/voip/incoming-basics.py b/tests/twisted/voip/incoming-basics.py new file mode 100644 index 0000000..c3cf8dd --- /dev/null +++ b/tests/twisted/voip/incoming-basics.py @@ -0,0 +1,161 @@ +""" +Test incoming call handling. +""" + +import dbus +from twisted.words.xish import xpath + +from sofiatest import exec_test +from servicetest import ( + make_channel_proxy, wrap_channel, + EventPattern, call_async, + assertEquals, assertContains, assertLength, + ) +import constants as cs +from voip_test import VoipTestContext + +from twisted.words.xish import xpath + +def test(q, bus, conn, sip_proxy, peer='foo@bar.com'): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) + + context = VoipTestContext(q, conn, bus, sip_proxy, 'sip:testacc@127.0.0.1', peer) + + self_handle = conn.GetSelfHandle() + remote_handle = conn.RequestHandles(cs.HT_CONTACT, [context.peer])[0] + + # Remote end calls us + context.incoming_call() + + nc, e = q.expect_many( + EventPattern('dbus-signal', signal='NewChannels'), + EventPattern('dbus-signal', signal='NewSessionHandler'), + )[0:2] + + path, props = nc.args[0][0] + ct = props[cs.CHANNEL_TYPE] + ht = props[cs.CHANNEL + '.TargetHandleType'] + h = props[cs.CHANNEL + '.TargetHandle'] + + assert ct == cs.CHANNEL_TYPE_STREAMED_MEDIA, ct + assert ht == cs.HT_CONTACT, ht + assert h == remote_handle, h + + media_chan = make_channel_proxy(conn, path, 'Channel.Interface.Group') + media_iface = make_channel_proxy(conn, path, 'Channel.Type.StreamedMedia') + + # S-E was notified about new session handler, and calls Ready on it + assert e.args[1] == 'rtp' + session_handler = make_channel_proxy(conn, e.args[0], 'Media.SessionHandler') + session_handler.Ready() + + nsh_event = q.expect('dbus-signal', signal='NewStreamHandler') + + # S-E gets notified about a newly-created stream + stream_handler = make_channel_proxy(conn, nsh_event.args[0], + 'Media.StreamHandler') + + streams = media_iface.ListStreams() + assertLength(1, streams) + + stream_id, stream_handle, stream_type, _, stream_direction, pending_flags =\ + streams[0] + assertEquals(remote_handle, stream_handle) + assertEquals(cs.MEDIA_STREAM_TYPE_AUDIO, stream_type) + assertEquals(cs.MEDIA_STREAM_DIRECTION_RECEIVE, stream_direction) + assertEquals(cs.MEDIA_STREAM_PENDING_LOCAL_SEND, pending_flags) + + # Exercise channel properties + channel_props = media_chan.GetAll( + cs.CHANNEL, dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals(remote_handle, channel_props['TargetHandle']) + assertEquals(cs.HT_CONTACT, channel_props['TargetHandleType']) + assertEquals((cs.HT_CONTACT, remote_handle), + media_chan.GetHandle(dbus_interface=cs.CHANNEL)) + assertEquals(context.peer_id, channel_props['TargetID']) + assertEquals(context.peer_id, channel_props['InitiatorID']) + assertEquals(remote_handle, channel_props['InitiatorHandle']) + assertEquals(False, channel_props['Requested']) + + group_props = media_chan.GetAll( + cs.CHANNEL_IFACE_GROUP, dbus_interface=dbus.PROPERTIES_IFACE) + + assert group_props['SelfHandle'] == self_handle, \ + (group_props['SelfHandle'], self_handle) + + flags = group_props['GroupFlags'] + assert flags & cs.GF_PROPERTIES, flags + # Changing members in any way other than adding or removing yourself is + # meaningless for incoming calls, and the flags need not be sent to change + # your own membership. + assert not flags & cs.GF_CAN_ADD, flags + assert not flags & cs.GF_CAN_REMOVE, flags + assert not flags & cs.GF_CAN_RESCIND, flags + + assert group_props['Members'] == [remote_handle], group_props['Members'] + assert group_props['RemotePendingMembers'] == [], \ + group_props['RemotePendingMembers'] + # We're local pending because remote_handle invited us. + assert group_props['LocalPendingMembers'] == \ + [(self_handle, remote_handle, cs.GC_REASON_INVITED, '')], \ + unwrap(group_props['LocalPendingMembers']) + + streams = media_chan.ListStreams( + dbus_interface=cs.CHANNEL_TYPE_STREAMED_MEDIA) + assert len(streams) == 1, streams + assert len(streams[0]) == 6, streams[0] + # streams[0][0] is the stream identifier, which in principle we can't + # make any assertion about (although in practice it's probably 1) + assert streams[0][1] == remote_handle, (streams[0], remote_handle) + assert streams[0][2] == cs.MEDIA_STREAM_TYPE_AUDIO, streams[0] + # We haven't connected yet + assert streams[0][3] == cs.MEDIA_STREAM_STATE_DISCONNECTED, streams[0] + # In Gabble, incoming streams start off with remote send enabled, and + # local send requested + assert streams[0][4] == cs.MEDIA_STREAM_DIRECTION_RECEIVE, streams[0] + assert streams[0][5] == cs.MEDIA_STREAM_PENDING_LOCAL_SEND, streams[0] + + # Connectivity checks happen before we have accepted the call + stream_handler.NewNativeCandidate("fake", context.get_remote_transports_dbus()) + stream_handler.NativeCandidatesPrepared() + stream_handler.Ready(context.get_audio_codecs_dbus()) + stream_handler.StreamState(cs.MEDIA_STREAM_STATE_CONNECTED) + stream_handler.SupportedCodecs(context.get_audio_codecs_dbus()) + + # At last, accept the call + media_chan.AddMembers([self_handle], 'accepted') + + # Call is accepted, we become a member, and the stream that was pending + # local send is now sending. + memb, acc, _, _, _ = q.expect_many( + EventPattern('dbus-signal', signal='MembersChanged', + args=[u'', [self_handle], [], [], [], self_handle, + cs.GC_REASON_NONE]), + EventPattern('sip-response', call_id=context.call_id, code=200), + EventPattern('dbus-signal', signal='SetStreamSending', args=[True]), + EventPattern('dbus-signal', signal='SetStreamPlaying', args=[True]), + EventPattern('dbus-signal', signal='StreamDirectionChanged', + args=[stream_id, cs.MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, 0]), + ) + + context.check_call_sdp(acc.sip_message.body) + + context.ack(acc.sip_message) + + # we are now both in members + members = media_chan.GetMembers() + assert set(members) == set([self_handle, remote_handle]), members + + # Connected! Blah, blah, ... + + # 'Nuff said + bye_msg = context.terminate() + + q.expect_many(EventPattern('dbus-signal', signal='Closed', path=path), + EventPattern('sip-response', cseq=bye_msg.headers['cseq'][0])) + +if __name__ == '__main__': + exec_test(test) + exec_test(lambda q, bus, conn, stream: + test(q, bus, conn, stream, 'foo@sip.bar.com')) diff --git a/tests/twisted/voip/outgoing-basics.py b/tests/twisted/voip/outgoing-basics.py new file mode 100644 index 0000000..933b4ad --- /dev/null +++ b/tests/twisted/voip/outgoing-basics.py @@ -0,0 +1,297 @@ +""" +Test basic outgoing call handling, using CreateChannel and all three variations +of RequestChannel. +""" + +import dbus +from twisted.words.xish import xpath + +from sofiatest import exec_test +from servicetest import ( + make_channel_proxy, wrap_channel, + EventPattern, call_async, + assertEquals, assertContains, assertLength, + ) +import constants as cs +from voip_test import VoipTestContext + +# There are various deprecated APIs for requesting calls, documented at +# <http://telepathy.freedesktop.org/wiki/Requesting StreamedMedia channels>. +# These are ordered from most recent to most deprecated. +CREATE = 0 # CreateChannel({TargetHandleType: Contact, TargetHandle: h}); + # RequestStreams() +REQUEST_ANONYMOUS = 1 # RequestChannel(HandleTypeNone, 0); RequestStreams() +REQUEST_ANONYMOUS_AND_ADD = 2 # RequestChannel(HandleTypeNone, 0); + # AddMembers([h], ...); RequestStreams(h,...) +REQUEST_NONYMOUS = 3 # RequestChannel(HandleTypeContact, h); + # RequestStreams(h, ...) + +def create(q, bus, conn, sip_proxy, peer='foo@bar.com'): + worker(q, bus, conn, sip_proxy, CREATE, peer) + +def request_anonymous(q, bus, conn, sip_proxy, peer='publish@foo.com'): + worker(q, bus, conn, sip_proxy, REQUEST_ANONYMOUS, peer) + +def request_anonymous_and_add(q, bus, conn, sip_proxy, + peer='publish-subscribe@foo.com/Res'): + worker(q, bus, conn, sip_proxy, REQUEST_ANONYMOUS_AND_ADD, peer) + +def request_nonymous(q, bus, conn, sip_proxy, peer='subscribe@foo.com'): + worker(q, bus, conn, sip_proxy, REQUEST_NONYMOUS, peer) + +def worker(q, bus, conn, sip_proxy, variant, peer): + conn.Connect() + q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) + + self_handle = conn.GetSelfHandle() + context = VoipTestContext(q, conn, bus, sip_proxy, 'sip:testacc@127.0.0.1', peer) + + self_handle = conn.GetSelfHandle() + remote_handle = conn.RequestHandles(1, [context.peer])[0] + + if variant == REQUEST_NONYMOUS: + path = conn.RequestChannel(cs.CHANNEL_TYPE_STREAMED_MEDIA, + cs.HT_CONTACT, remote_handle, True) + elif variant == CREATE: + path = conn.Requests.CreateChannel({ + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_STREAMED_MEDIA, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_HANDLE: remote_handle, + })[0] + else: + path = conn.RequestChannel(cs.CHANNEL_TYPE_STREAMED_MEDIA, + cs.HT_NONE, 0, True) + + old_sig, new_sig = q.expect_many( + EventPattern('dbus-signal', signal='NewChannel', + predicate=lambda e: cs.CHANNEL_TYPE_CONTACT_LIST not in e.args), + EventPattern('dbus-signal', signal='NewChannels', + predicate=lambda e: + cs.CHANNEL_TYPE_CONTACT_LIST not in e.args[0][0][1].values()), + ) + + if variant == REQUEST_NONYMOUS or variant == CREATE: + assertEquals( [path, cs.CHANNEL_TYPE_STREAMED_MEDIA, cs.HT_CONTACT, + remote_handle, True], old_sig.args) + else: + assertEquals( [path, cs.CHANNEL_TYPE_STREAMED_MEDIA, cs.HT_NONE, 0, + True], old_sig.args) + + assertLength(1, new_sig.args) + assertLength(1, new_sig.args[0]) # one channel + assertLength(2, new_sig.args[0][0]) # two struct members + emitted_props = new_sig.args[0][0][1] + + assertEquals( + cs.CHANNEL_TYPE_STREAMED_MEDIA, emitted_props[cs.CHANNEL_TYPE]) + + if variant == REQUEST_NONYMOUS or variant == CREATE: + assertEquals(remote_handle, emitted_props[cs.TARGET_HANDLE]) + assertEquals(cs.HT_CONTACT, emitted_props[cs.TARGET_HANDLE_TYPE]) + assertEquals(context.peer_id, emitted_props[cs.TARGET_ID]) + else: + assertEquals(0, emitted_props[cs.TARGET_HANDLE]) + assertEquals(cs.HT_NONE, emitted_props[cs.TARGET_HANDLE_TYPE]) + assertEquals('', emitted_props[cs.TARGET_ID]) + + assertEquals(True, emitted_props[cs.REQUESTED]) + assertEquals(self_handle, emitted_props[cs.INITIATOR_HANDLE]) + assertEquals('sip:testacc@127.0.0.1', emitted_props[cs.INITIATOR_ID]) + + chan = wrap_channel(bus.get_object(conn.bus_name, path), 'StreamedMedia', + ['MediaSignalling']) + + # Exercise basic Channel Properties + channel_props = chan.Properties.GetAll(cs.CHANNEL) + + assertEquals(cs.CHANNEL_TYPE_STREAMED_MEDIA, + channel_props.get('ChannelType')) + + if variant == REQUEST_NONYMOUS or variant == CREATE: + assertEquals(remote_handle, channel_props['TargetHandle']) + assertEquals(cs.HT_CONTACT, channel_props['TargetHandleType']) + assertEquals(context.peer_id, channel_props['TargetID']) + assertEquals((cs.HT_CONTACT, remote_handle), chan.GetHandle()) + else: + assertEquals(0, channel_props['TargetHandle']) + assertEquals(cs.HT_NONE, channel_props['TargetHandleType']) + assertEquals('', channel_props['TargetID']) + assertEquals((cs.HT_NONE, 0), chan.GetHandle()) + + for interface in [ + cs.CHANNEL_IFACE_GROUP, cs.CHANNEL_IFACE_MEDIA_SIGNALLING, + cs.TP_AWKWARD_PROPERTIES, cs.CHANNEL_IFACE_HOLD]: + assertContains(interface, channel_props['Interfaces']) + + assertEquals(True, channel_props['Requested']) + assertEquals('sip:testacc@127.0.0.1', channel_props['InitiatorID']) + assertEquals(conn.GetSelfHandle(), channel_props['InitiatorHandle']) + + # Exercise Group Properties + group_props = chan.Properties.GetAll(cs.CHANNEL_IFACE_GROUP) + + assertEquals([self_handle], group_props['Members']) + assertEquals([], group_props['LocalPendingMembers']) + + if variant == REQUEST_NONYMOUS: + # In this variant, they're meant to be in RP even though we've sent + # nothing + assertEquals([remote_handle], group_props['RemotePendingMembers']) + else: + # For an anonymous channel, the peer isn't yet known; for a Create-d + # channel, the peer only appears in RP when we actually send them the + # session-initiate + assertEquals([], group_props['RemotePendingMembers']) + + if variant == REQUEST_ANONYMOUS_AND_ADD: + # but we should be allowed to add the peer. + chan.Group.AddMembers([remote_handle], 'I love backwards compat') + + base_flags = cs.GF_PROPERTIES | cs.GF_CAN_REMOVE | cs.GF_CAN_RESCIND + + if variant in [REQUEST_ANONYMOUS_AND_ADD, REQUEST_ANONYMOUS, CREATE]: + expected_flags = base_flags | cs.GF_CAN_ADD + else: + expected_flags = base_flags + + assertEquals(bin(expected_flags), bin(group_props['GroupFlags'])) + assertEquals({}, group_props['HandleOwners']) + + assertEquals([], chan.StreamedMedia.ListStreams()) + streams = chan.StreamedMedia.RequestStreams(remote_handle, + [cs.MEDIA_STREAM_TYPE_AUDIO]) + assertEquals(streams, chan.StreamedMedia.ListStreams()) + assertLength(1, streams) + + # streams[0][0] is the stream identifier, which in principle we can't + # make any assertion about (although in practice it's probably 1) + + assertEquals(( + remote_handle, + cs.MEDIA_STREAM_TYPE_AUDIO, + # We haven't connected yet + cs.MEDIA_STREAM_STATE_DISCONNECTED, + # In Gabble, requested streams start off bidirectional + cs.MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, + cs.MEDIA_STREAM_PENDING_REMOTE_SEND), + streams[0][1:]) + + # S-E does state recovery to get the session handler, and calls Ready on it + session_handlers = chan.MediaSignalling.GetSessionHandlers() + sh_path, sh_type = session_handlers[0] + + assert sh_type == 'rtp' + + session_handler = make_channel_proxy(conn, sh_path, 'Media.SessionHandler') + session_handler.Ready() + + e = q.expect('dbus-signal', signal='NewStreamHandler') + + stream_handler = make_channel_proxy(conn, e.args[0], 'Media.StreamHandler') + + stream_handler.NewNativeCandidate("fake", context.get_remote_transports_dbus()) + stream_handler.NativeCandidatesPrepared() + stream_handler.Ready(context.get_audio_codecs_dbus()) + stream_handler.StreamState(cs.MEDIA_STREAM_STATE_CONNECTED) + + sh_props = stream_handler.GetAll( + cs.STREAM_HANDLER, dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals('none', sh_props['NATTraversal']) + assertEquals(True, sh_props['CreatedLocally']) + + if variant == CREATE: + # When we actually send INVITE to the peer, they should pop up in remote + # pending. + invite_event, _ = q.expect_many( + EventPattern('sip-invite'), + EventPattern('dbus-signal', signal='MembersChanged', + args=["", [], [], [], [remote_handle], self_handle, + cs.GC_REASON_INVITED]), + ) + else: + invite_event = q.expect('sip-invite') + + # Check the Group interface's properties again. Regardless of the call + # requesting API in use, the state should be the same here: + group_props = chan.Properties.GetAll(cs.CHANNEL_IFACE_GROUP) + assertContains('HandleOwners', group_props) + assertEquals([self_handle], group_props['Members']) + assertEquals([], group_props['LocalPendingMembers']) + assertEquals([remote_handle], group_props['RemotePendingMembers']) + + context.check_call_sdp(invite_event.sip_message.body) + context.accept(invite_event.sip_message) + + ack_cseq = "%s ACK" % invite_event.cseq.split()[0] + q.expect_many( + EventPattern('sip-ack', cseq=ack_cseq), + # Call accepted + EventPattern('dbus-signal', signal='MembersChanged', + args=['', [remote_handle], [], [], [], remote_handle, + cs.GC_REASON_NONE]), + ) + + # Time passes ... afterwards we close the chan + + chan.Group.RemoveMembers([self_handle], 'closed') + + + mc_event, _, bye_event = q.expect_many( + EventPattern('dbus-signal', signal='MembersChanged'), + EventPattern('dbus-signal', signal='Close'), + EventPattern('sip-bye', call_id=context.call_id), + ) + # Check that we're the actor + assertEquals(self_handle, mc_event.args[5]) + + # For completeness, reply to the BYE. + bye_response = sip_proxy.responseFromRequest(200, bye_event.sip_message) + sip_proxy.deliverResponse(bye_response) + +def rccs(q, bus, conn, stream): + """ + Tests that the connection's RequestableChannelClasses for StreamedMedia are + sane. + """ + conn.Connect() + + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + rccs = conn.Properties.Get(cs.CONN_IFACE_REQUESTS, + 'RequestableChannelClasses') + + # Test Channel.Type.StreamedMedia + media_classes = [ rcc for rcc in rccs + if rcc[0][cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_STREAMED_MEDIA ] + + assertLength(1, media_classes) + + fixed, allowed = media_classes[0] + + assertEquals(cs.HT_CONTACT, fixed[cs.TARGET_HANDLE_TYPE]) + + expected_allowed = [ + cs.TARGET_ID, cs.TARGET_HANDLE, + cs.INITIAL_VIDEO, cs.INITIAL_AUDIO + ] + + allowed.sort() + expected_allowed.sort() + assertEquals(expected_allowed, allowed) + +if __name__ == '__main__': + + exec_test(rccs) + exec_test(create) + exec_test(request_anonymous) + exec_test(request_anonymous_and_add) + exec_test(request_nonymous) + exec_test(lambda q, b, c, s: + create(q, b, c, s, peer='foo@gw.bar.com')) + exec_test(lambda q, b, c, s: + request_anonymous(q, b, c, s, peer='foo@gw.bar.com')) + exec_test(lambda q, b, c, s: + request_anonymous_and_add(q, b, c, s, peer='foo@gw.bar.com')) + exec_test(lambda q, b, c, s: + request_nonymous(q, b, c, s, peer='foo@gw.bar.com')) diff --git a/tests/twisted/voip/voip_test.py b/tests/twisted/voip/voip_test.py new file mode 100644 index 0000000..0b41828 --- /dev/null +++ b/tests/twisted/voip/voip_test.py @@ -0,0 +1,172 @@ + +import dbus +import uuid + +import twisted.protocols.sip + +from servicetest import assertContains + +class VoipTestContext(object): + # Default audio codecs for the remote end + audio_codecs = [ ('GSM', 3, 8000, {}), + ('PCMA', 8, 8000, {}), + ('PCMU', 0, 8000, {}) ] + + # Default video codecs for the remote end. I have no idea what's + # a suitable value here... + video_codecs = [ ('WTF', 42, 80000, {}) ] + + # Default candidates for the remote end + remote_transports = [ + ( "192.168.0.1", # host + 666, # port + 0, # protocol = TP_MEDIA_STREAM_BASE_PROTO_UDP + "RTP", # protocol subtype + "AVP", # profile + 1.0, # preference + 0, # transport type = TP_MEDIA_STREAM_TRANSPORT_TYPE_LOCAL, + "username", + "password" ) ] + + _mline_template = 'm=audio %(port)s %(subtype)s/%(profile)s %(codec_ids)s' + _aline_template = 'a=rtpmap:%(codec_id)s %(name)s/%(rate)s' + + def __init__(self, q, conn, bus, sip_proxy, our_uri, peer): + self.bus = bus + self.conn = conn + self.q = q + self.our_uri = our_uri + self.peer = peer + self.peer_id = "sip:" + peer + self.sip_proxy = sip_proxy + self._cseq_id = 1 + + def dbusify_codecs(self, codecs): + dbussed_codecs = [ (id, name, 0, rate, 0, params ) + for (name, id, rate, params) in codecs ] + return dbus.Array(dbussed_codecs, signature='(usuuua{ss})') + + def dbusify_codecs_with_params (self, codecs): + return self.dbusify_codecs(codecs) + + def get_audio_codecs_dbus(self): + return self.dbusify_codecs(self.audio_codecs) + + def get_video_codecs_dbus(self): + return self.dbusify_codecs(self.video_codecs) + + def dbusify_call_codecs(self, codecs): + dbussed_codecs = [ (id, name, rate, 0, params) + for (name, id, rate, params) in codecs ] + return dbus.Array(dbussed_codecs, signature='(usuua{ss})') + + def dbusify_call_codecs_with_params(self, codecs): + return dbusify_call_codecs (self, codecs) + + def get_call_audio_codecs_dbus(self): + return self.dbusify_call_codecs(self.audio_codecs) + + def get_call_video_codecs_dbus(self): + return self.dbusify_call_codecs(self.video_codecs) + + + def get_remote_transports_dbus(self): + return dbus.Array([ + (dbus.UInt32(1 + i), host, port, proto, subtype, + profile, pref, transtype, user, pwd) + for i, (host, port, proto, subtype, profile, + pref, transtype, user, pwd) + in enumerate(self.remote_transports) ], + signature='(usuussduss)') + + def get_call_remote_transports_dbus(self): + return dbus.Array([ + (1 , host, port, + { "Type": transtype, + "Foundation": "", + "Protocol": proto, + "Priority": int((1+i) * 65536), + "Username": user, + "Password": pwd } + ) for i, (host, port, proto, subtype, profile, + pref, transtype, user, pwd) + in enumerate(self.remote_transports) ], + signature='(usqa{sv})') + + def get_call_sdp(self): + (ip, port, protocol, subtype, profile, preference, + transport, username, password) = self.remote_transports[0] + + codec_id_list = [] + codec_list = [] + for name, codec_id, rate, _misc in self.audio_codecs: + codec_list.append('a=rtpmap:%(codec_id)s %(name)s/%(rate)s' % locals()) + codec_id_list.append(str(codec_id)) + codec_ids = ' '.join(codec_id_list) + codecs = '\r\n'.join(codec_list) + + sdp_string = ('v=0\r\n' + 'o=- 7047265765596858314 2813734028456100815 IN IP4 %(ip)s\r\n' + 's=-\r\n' + 't=0 0\r\n' + 'm=audio %(port)s RTP/AVP 3 8 0\r\n' + 'c=IN IP4 %(ip)s\r\n' + '%(codecs)s\r\n') % locals() + return sdp_string + + def check_call_sdp(self, sdp_string): + codec_id_list = [] + for name, codec_id, rate, _misc in self.audio_codecs: + assertContains (self._aline_template % locals(), sdp_string) + codec_id_list.append(str(codec_id)) + codec_ids = ' '.join(codec_id_list) + + (ip, port, protocol, subtype, profile, preference, + transport, username, password) = self.remote_transports[0] + assert self._mline_template % locals() in sdp_string + + def send_message(self, message_type, body='', **additional_headers): + url = twisted.protocols.sip.parseURL('sip:testacc@127.0.0.1') + msg = twisted.protocols.sip.Request(message_type, url) + if body: + msg.body = body + msg.addHeader('content-length', '%d' % len(msg.body)) + msg.addHeader('from', '<%s>;tag=XYZ' % self.peer_id) + msg.addHeader('to', '<sip:testacc@127.0.0.1>') + self._cseq_id += 1 + additional_headers.setdefault('cseq', '%d %s' % (self._cseq_id, message_type)) + for key, vals in additional_headers.items(): + if not isinstance(vals, list): + vals = [vals] + k = key.replace('_', '-') + for v in vals: + msg.addHeader(k, v) + via = self.sip_proxy.getVia() + via.branch = 'z9hG4bKXYZ' + msg.addHeader('via', via.toString()) + _expire, destination = self.sip_proxy.registry.users['testacc'] + self.sip_proxy.sendMessage(destination, msg) + return msg + + def accept(self, invite_message): + self.call_id = invite_message.headers['call-id'][0] + response = self.sip_proxy.responseFromRequest(200, invite_message) + # Echo sofiasip's SDP back to it. It doesn't care. + response.addHeader('content-type', 'application/sdp') + response.body = invite_message.body + response.addHeader('content-length', '%d' % len(response.body)) + self.sip_proxy.deliverResponse(response) + return response + + def ack(self, ok_message): + cseq = '%s ACK' % ok_message.headers['cseq'][0].split()[0] + self.send_message('ACK', call_id=self.call_id, cseq=cseq) + + def incoming_call(self): + self.call_id = uuid.uuid4().hex + body = self.get_call_sdp() + return self.send_message('INVITE', body, content_type='application/sdp', + supported='timer, 100rel', call_id=self.call_id) + + def terminate(self): + return self.send_message('BYE', call_id=self.call_id) |