diff options
author | fiaxh <git@lightrise.org> | 2021-05-11 12:57:02 +0200 |
---|---|---|
committer | fiaxh <git@lightrise.org> | 2021-05-11 12:57:02 +0200 |
commit | d71604913dd5b3372a823320db83c37c845fac5c (patch) | |
tree | 2ffbff97a02c81d48d8aef4a4b7ee870507236e9 /xmpp-vala/src/module/xep/0167_jingle_rtp | |
parent | e92ed27317ae398c867c946cf7206b1f0b32f3b4 (diff) | |
parent | 90f9ecf62b2ebfef14de2874e7942552409632bf (diff) | |
download | dino-d71604913dd5b3372a823320db83c37c845fac5c.tar.gz dino-d71604913dd5b3372a823320db83c37c845fac5c.zip |
Merge remote-tracking branch 'origin/feature/calls'
Diffstat (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp')
6 files changed, 786 insertions, 0 deletions
diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala new file mode 100644 index 00000000..344fe8b8 --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala @@ -0,0 +1,231 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; + +public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object { + + public signal void stream_created(Stream stream); + public signal void connection_ready(); + + public string media { get; private set; } + public string? ssrc { get; private set; } + public bool rtcp_mux { get; private set; } + + public string? bandwidth { get; private set; } + public string? bandwidth_type { get; private set; } + + public bool encryption_required { get; private set; default = false; } + public PayloadType? agreed_payload_type { get; private set; } + public Gee.List<PayloadType> payload_types = new ArrayList<PayloadType>(PayloadType.equals_func); + public Gee.List<HeaderExtension> header_extensions = new ArrayList<HeaderExtension>(); + public Gee.List<Crypto> remote_cryptos = new ArrayList<Crypto>(); + public Crypto? local_crypto = null; + public Crypto? remote_crypto = null; + + public weak Stream? stream { get; private set; } + + private Module parent; + + public Parameters(Module parent, + string media, Gee.List<PayloadType> payload_types, + string? ssrc = null, bool rtcp_mux = false, + string? bandwidth = null, string? bandwidth_type = null, + bool encryption_required = false, Crypto? local_crypto = null + ) { + this.parent = parent; + this.media = media; + this.ssrc = ssrc; + this.rtcp_mux = true; + this.bandwidth = bandwidth; + this.bandwidth_type = bandwidth_type; + this.encryption_required = encryption_required; + this.payload_types = payload_types; + this.local_crypto = local_crypto; + } + + public Parameters.from_node(Module parent, StanzaNode node) throws Jingle.IqError { + this.parent = parent; + this.media = node.get_attribute("media"); + this.ssrc = node.get_attribute("ssrc"); + this.rtcp_mux = node.get_subnode("rtcp-mux") != null; + StanzaNode? encryption = node.get_subnode("encryption"); + if (encryption != null) { + this.encryption_required = encryption.get_attribute_bool("required", this.encryption_required); + foreach (StanzaNode crypto in encryption.get_subnodes("crypto")) { + this.remote_cryptos.add(Crypto.parse(crypto)); + } + } + foreach (StanzaNode payloadType in node.get_subnodes(PayloadType.NAME)) { + this.payload_types.add(PayloadType.parse(payloadType)); + } + foreach (StanzaNode subnode in node.get_subnodes(HeaderExtension.NAME, HeaderExtension.NS_URI)) { + this.header_extensions.add(HeaderExtension.parse(subnode)); + } + } + + public async void handle_proposed_content(XmppStream stream, Jingle.Session session, Jingle.Content content) { + agreed_payload_type = yield parent.pick_payload_type(media, payload_types); + if (agreed_payload_type == null) { + debug("no usable payload type"); + content.reject(); + return; + } + // Drop unsupported header extensions + var iter = header_extensions.iterator(); + while(iter.next()) { + if (!parent.is_header_extension_supported(media, iter.@get())) iter.remove(); + } + remote_crypto = parent.pick_remote_crypto(remote_cryptos); + if (local_crypto == null && remote_crypto != null) { + local_crypto = parent.pick_local_crypto(remote_crypto); + } + if ((local_crypto == null || remote_crypto == null) && encryption_required) { + debug("no usable encryption, but encryption required"); + content.reject(); + return; + } + } + + public void accept(XmppStream stream, Jingle.Session session, Jingle.Content content) { + debug("[%p] Jingle RTP on_accept", stream); + + Jingle.DatagramConnection rtp_datagram = (Jingle.DatagramConnection) content.get_transport_connection(1); + Jingle.DatagramConnection rtcp_datagram = (Jingle.DatagramConnection) content.get_transport_connection(2); + + ulong rtcp_ready_handler_id = 0; + rtcp_ready_handler_id = rtcp_datagram.notify["ready"].connect((rtcp_datagram, _) => { + this.stream.on_rtcp_ready(); + + ((Jingle.DatagramConnection)rtcp_datagram).disconnect(rtcp_ready_handler_id); + rtcp_ready_handler_id = 0; + }); + + ulong rtp_ready_handler_id = 0; + rtp_ready_handler_id = rtp_datagram.notify["ready"].connect((rtp_datagram, _) => { + this.stream.on_rtp_ready(); + if (rtcp_mux) { + this.stream.on_rtcp_ready(); + } + connection_ready(); + + ((Jingle.DatagramConnection)rtp_datagram).disconnect(rtp_ready_handler_id); + rtp_ready_handler_id = 0; + }); + + ulong session_state_handler_id = 0; + session_state_handler_id = session.notify["state"].connect((obj, _) => { + Jingle.Session session2 = (Jingle.Session) obj; + if (session2.state == Jingle.Session.State.ENDED) { + if (rtcp_ready_handler_id != 0) rtcp_datagram.disconnect(rtcp_ready_handler_id); + if (rtp_ready_handler_id != 0) rtp_datagram.disconnect(rtp_ready_handler_id); + if (session_state_handler_id != 0) { + session2.disconnect(session_state_handler_id); + } + } + }); + + if (remote_crypto == null || local_crypto == null) { + if (encryption_required) { + warning("Encryption required but not provided in both directions"); + return; + } + remote_crypto = null; + local_crypto = null; + } + if (remote_crypto != null && local_crypto != null) { + var content_encryption = new Xmpp.Xep.Jingle.ContentEncryption() { encryption_ns = "", encryption_name = "SRTP", our_key=local_crypto.key, peer_key=remote_crypto.key }; + content.encryptions[content_encryption.encryption_name] = content_encryption; + } + + this.stream = parent.create_stream(content); + rtp_datagram.datagram_received.connect(this.stream.on_recv_rtp_data); + rtcp_datagram.datagram_received.connect(this.stream.on_recv_rtcp_data); + this.stream.on_send_rtp_data.connect(rtp_datagram.send_datagram); + this.stream.on_send_rtcp_data.connect(rtcp_datagram.send_datagram); + this.stream_created(this.stream); + this.stream.create(); + } + + public void handle_accept(XmppStream stream, Jingle.Session session, Jingle.Content content, StanzaNode description_node) { + rtcp_mux = description_node.get_subnode("rtcp-mux") != null; + Gee.List<StanzaNode> payload_type_nodes = description_node.get_subnodes("payload-type"); + if (payload_type_nodes.size == 0) { + warning("Counterpart didn't include any payload types"); + return; + } + PayloadType preferred_payload_type = PayloadType.parse(payload_type_nodes[0]); + if (!payload_types.contains(preferred_payload_type)) { + warning("Counterpart's preferred content type doesn't match any of our sent ones"); + } + agreed_payload_type = preferred_payload_type; + + Gee.List<StanzaNode> crypto_nodes = description_node.get_deep_subnodes("encryption", "crypto"); + if (crypto_nodes.size == 0) { + debug("Counterpart didn't include any cryptos"); + if (encryption_required) { + return; + } + } else { + Crypto preferred_crypto = Crypto.parse(crypto_nodes[0]); + if (local_crypto.crypto_suite != preferred_crypto.crypto_suite) { + warning("Counterpart's crypto suite doesn't match any of our sent ones"); + } + remote_crypto = preferred_crypto; + } + + accept(stream, session, content); + } + + public void terminate(bool we_terminated, string? reason_name, string? reason_text) { + if (stream != null) parent.close_stream(stream); + } + + public StanzaNode get_description_node() { + StanzaNode ret = new StanzaNode.build("description", NS_URI) + .add_self_xmlns() + .put_attribute("media", media); + + if (agreed_payload_type != null) { + ret.put_node(agreed_payload_type.to_xml()); + } else { + foreach (PayloadType payload_type in payload_types) { + ret.put_node(payload_type.to_xml()); + } + } + foreach (HeaderExtension ext in header_extensions) { + ret.put_node(ext.to_xml()); + } + if (local_crypto != null) { + ret.put_node(new StanzaNode.build("encryption", NS_URI) + .put_node(local_crypto.to_xml())); + } + if (rtcp_mux) { + ret.put_node(new StanzaNode.build("rtcp-mux", NS_URI)); + } + return ret; + } +} + +public class Xmpp.Xep.JingleRtp.HeaderExtension { + public const string NS_URI = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; + public const string NAME = "rtp-hdrext"; + + public uint8 id { get; private set; } + public string uri { get; private set; } + + public HeaderExtension(uint8 id, string uri) { + this.id = id; + this.uri = uri; + } + + public static HeaderExtension parse(StanzaNode node) { + return new HeaderExtension((uint8) node.get_attribute_int("id"), node.get_attribute("uri")); + } + + public StanzaNode to_xml() { + return new StanzaNode.build(NAME, NS_URI) + .add_self_xmlns() + .put_attribute("id", id.to_string()) + .put_attribute("uri", uri); + } +}
\ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_type.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_type.vala new file mode 100644 index 00000000..5a8ed1b6 --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_type.vala @@ -0,0 +1,23 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; + +public class Xmpp.Xep.JingleRtp.ContentType : Jingle.ContentType, Object { + public string ns_uri { get { return NS_URI; } } + public Jingle.TransportType required_transport_type { get { return Jingle.TransportType.DATAGRAM; } } + public uint8 required_components { get { return 2; /* RTP + RTCP */ } } + + private Module module; + + public ContentType(Module module) { + this.module = module; + } + + public Jingle.ContentParameters parse_content_parameters(StanzaNode description) throws Jingle.IqError { + return new Parameters.from_node(module, description); + } + + public Jingle.ContentParameters create_content_parameters(Object object) throws Jingle.IqError { + assert_not_reached(); + } +}
\ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala new file mode 100644 index 00000000..6b55cbe6 --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala @@ -0,0 +1,290 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; + +namespace Xmpp.Xep.JingleRtp { + +public const string NS_URI = "urn:xmpp:jingle:apps:rtp:1"; +public const string NS_URI_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; +public const string NS_URI_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; + +public abstract class Module : XmppStreamModule { + public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0167_jingle_rtp"); + + private ContentType content_type; + public SessionInfoType session_info_type = new SessionInfoType(); + + protected Module() { + content_type = new ContentType(this); + } + + public abstract async Gee.List<PayloadType> get_supported_payloads(string media); + public abstract async PayloadType? pick_payload_type(string media, Gee.List<PayloadType> payloads); + public abstract Crypto? generate_local_crypto(); + public abstract Crypto? pick_remote_crypto(Gee.List<Crypto> cryptos); + public abstract Crypto? pick_local_crypto(Crypto? remote); + public abstract Stream create_stream(Jingle.Content content); + public abstract bool is_header_extension_supported(string media, HeaderExtension ext); + public abstract Gee.List<HeaderExtension> get_suggested_header_extensions(string media); + public abstract void close_stream(Stream stream); + + public async Jingle.Session start_call(XmppStream stream, Jid receiver_full_jid, bool video, string? sid = null) throws Jingle.Error { + + Jingle.Module jingle_module = stream.get_module(Jingle.Module.IDENTITY); + + Jid? my_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid; + if (my_jid == null) { + throw new Jingle.Error.GENERAL("Couldn't determine own JID"); + } + + ArrayList<Jingle.Content> contents = new ArrayList<Jingle.Content>(); + + // Create audio content + Parameters audio_content_parameters = new Parameters(this, "audio", yield get_supported_payloads("audio")); + audio_content_parameters.local_crypto = generate_local_crypto(); + audio_content_parameters.header_extensions.add_all(get_suggested_header_extensions("audio")); + Jingle.Transport? audio_transport = yield jingle_module.select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); + if (audio_transport == null) { + throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable audio transports"); + } + Jingle.TransportParameters audio_transport_params = audio_transport.create_transport_parameters(stream, content_type.required_components, my_jid, receiver_full_jid); + Jingle.Content audio_content = new Jingle.Content.initiate_sent("voice", Jingle.Senders.BOTH, + content_type, audio_content_parameters, + audio_transport, audio_transport_params, + null, null, + my_jid, receiver_full_jid); + contents.add(audio_content); + + Jingle.Content? video_content = null; + if (video) { + // Create video content + Parameters video_content_parameters = new Parameters(this, "video", yield get_supported_payloads("video")); + video_content_parameters.local_crypto = generate_local_crypto(); + video_content_parameters.header_extensions.add_all(get_suggested_header_extensions("video")); + Jingle.Transport? video_transport = yield stream.get_module(Jingle.Module.IDENTITY).select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); + if (video_transport == null) { + throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable video transports"); + } + Jingle.TransportParameters video_transport_params = video_transport.create_transport_parameters(stream, content_type.required_components, my_jid, receiver_full_jid); + video_content = new Jingle.Content.initiate_sent("webcam", Jingle.Senders.BOTH, + content_type, video_content_parameters, + video_transport, video_transport_params, + null, null, + my_jid, receiver_full_jid); + contents.add(video_content); + } + + // Create session + try { + Jingle.Session session = yield jingle_module.create_session(stream, contents, receiver_full_jid, sid); + return session; + } catch (Jingle.Error e) { + throw new Jingle.Error.GENERAL(@"Couldn't create Jingle session: $(e.message)"); + } + } + + public async Jingle.Content add_outgoing_video_content(XmppStream stream, Jingle.Session session) throws Jingle.Error { + Jid my_jid = session.local_full_jid; + Jid receiver_full_jid = session.peer_full_jid; + + Jingle.Content? content = null; + foreach (Jingle.Content c in session.contents) { + Parameters? parameters = c.content_params as Parameters; + if (parameters == null) continue; + + if (parameters.media == "video") { + content = c; + break; + } + } + + if (content == null) { + // Content for video does not yet exist -> create it + Parameters video_content_parameters = new Parameters(this, "video", yield get_supported_payloads("video")); + video_content_parameters.local_crypto = generate_local_crypto(); + video_content_parameters.header_extensions.add_all(get_suggested_header_extensions("video")); + Jingle.Transport? video_transport = yield stream.get_module(Jingle.Module.IDENTITY).select_transport(stream, content_type.required_transport_type, content_type.required_components, receiver_full_jid, Set.empty()); + if (video_transport == null) { + throw new Jingle.Error.NO_SHARED_PROTOCOLS("No suitable video transports"); + } + Jingle.TransportParameters video_transport_params = video_transport.create_transport_parameters(stream, content_type.required_components, my_jid, receiver_full_jid); + content = new Jingle.Content.initiate_sent("webcam", + session.we_initiated ? Jingle.Senders.INITIATOR : Jingle.Senders.RESPONDER, + content_type, video_content_parameters, + video_transport, video_transport_params, + null, null, + my_jid, receiver_full_jid); + + session.add_content.begin(content); + } else { + // Content for video already exists -> modify senders + bool we_initiated = session.we_initiated; + Jingle.Senders want_sender = we_initiated ? Jingle.Senders.INITIATOR : Jingle.Senders.RESPONDER; + if (content.senders == Jingle.Senders.BOTH || content.senders == want_sender) { + warning("want to add video but senders is already both/target"); + } else if (content.senders == Jingle.Senders.NONE) { + content.modify(want_sender); + } else { + content.modify(Jingle.Senders.BOTH); + } + } + + return content; + } + + public override void attach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI_AUDIO); + stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI_VIDEO); + stream.get_module(Jingle.Module.IDENTITY).register_content_type(content_type); + stream.get_module(Jingle.Module.IDENTITY).register_session_info_type(session_info_type); + } + + public override void detach(XmppStream stream) { + stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); + stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI_AUDIO); + stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI_VIDEO); + } + + public async bool is_available(XmppStream stream, Jid full_jid) { + bool? has_feature = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).has_entity_feature(stream, full_jid, NS_URI); + if (has_feature == null || !(!)has_feature) { + return false; + } + return yield stream.get_module(Jingle.Module.IDENTITY).is_available(stream, content_type.required_transport_type, content_type.required_components, full_jid); + } + + public override string get_ns() { return NS_URI; } + public override string get_id() { return IDENTITY.id; } +} + +public class Crypto { + public const string AES_CM_128_HMAC_SHA1_80 = "AES_CM_128_HMAC_SHA1_80"; + public const string AES_CM_128_HMAC_SHA1_32 = "AES_CM_128_HMAC_SHA1_32"; + public const string F8_128_HMAC_SHA1_80 = "F8_128_HMAC_SHA1_80"; + + public string crypto_suite { get; private set; } + public string key_params { get; private set; } + public string? session_params { get; private set; } + public string tag { get; private set; } + + public uint8[]? key_and_salt { owned get { + if (!key_params.has_prefix("inline:")) return null; + int endIndex = key_params.index_of("|"); + if (endIndex < 0) endIndex = key_params.length; + string sub = key_params.substring(7, endIndex - 7); + return Base64.decode(sub); + }} + + public string? lifetime { owned get { + if (!key_params.has_prefix("inline:")) return null; + int firstIndex = key_params.index_of("|"); + if (firstIndex < 0) return null; + int endIndex = key_params.index_of("|", firstIndex + 1); + if (endIndex < 0) { + if (key_params.index_of(":", firstIndex) > 0) return null; // Is MKI + endIndex = key_params.length; + } + return key_params.substring(firstIndex + 1, endIndex); + }} + + public int mki { get { + if (!key_params.has_prefix("inline:")) return -1; + int firstIndex = key_params.index_of("|"); + if (firstIndex < 0) return -1; + int splitIndex = key_params.index_of(":", firstIndex); + if (splitIndex < 0) return -1; + int secondIndex = key_params.index_of("|", firstIndex + 1); + if (secondIndex < 0) { + return int.parse(key_params.substring(firstIndex + 1, splitIndex)); + } else if (splitIndex > secondIndex) { + return int.parse(key_params.substring(secondIndex + 1, splitIndex)); + } + return -1; + }} + + public int mki_length { get { + if (!key_params.has_prefix("inline:")) return -1; + int firstIndex = key_params.index_of("|"); + if (firstIndex < 0) return -1; + int splitIndex = key_params.index_of(":", firstIndex); + if (splitIndex < 0) return -1; + int secondIndex = key_params.index_of("|", firstIndex + 1); + if (secondIndex < 0 || splitIndex > secondIndex) { + return int.parse(key_params.substring(splitIndex + 1, key_params.length)); + } + return -1; + }} + + public bool is_valid { get { + switch(crypto_suite) { + case AES_CM_128_HMAC_SHA1_80: + case AES_CM_128_HMAC_SHA1_32: + case F8_128_HMAC_SHA1_80: + return key_and_salt != null && key_and_salt.length == 30; + } + return false; + }} + + public uint8[]? key { owned get { + uint8[]? key_and_salt = key_and_salt; + switch(crypto_suite) { + case AES_CM_128_HMAC_SHA1_80: + case AES_CM_128_HMAC_SHA1_32: + case F8_128_HMAC_SHA1_80: + if (key_and_salt != null && key_and_salt.length >= 16) return key_and_salt[0:16]; + break; + } + return null; + }} + + public uint8[]? salt { owned get { + uint8[]? key_and_salt = key_and_salt; + switch(crypto_suite) { + case AES_CM_128_HMAC_SHA1_80: + case AES_CM_128_HMAC_SHA1_32: + case F8_128_HMAC_SHA1_80: + if (key_and_salt != null && key_and_salt.length >= 30) return key_and_salt[16:30]; + break; + } + return null; + }} + + public static Crypto create(string crypto_suite, uint8[] key_and_salt, string? session_params = null, string tag = "1") { + Crypto crypto = new Crypto(); + crypto.crypto_suite = crypto_suite; + crypto.key_params = "inline:" + Base64.encode(key_and_salt); + crypto.session_params = session_params; + crypto.tag = tag; + return crypto; + } + + public Crypto rekey(uint8[] key_and_salt) { + Crypto crypto = new Crypto(); + crypto.crypto_suite = crypto_suite; + crypto.key_params = "inline:" + Base64.encode(key_and_salt); + crypto.session_params = session_params; + crypto.tag = tag; + return crypto; + } + + public static Crypto parse(StanzaNode node) { + Crypto crypto = new Crypto(); + crypto.crypto_suite = node.get_attribute("crypto-suite"); + crypto.key_params = node.get_attribute("key-params"); + crypto.session_params = node.get_attribute("session-params"); + crypto.tag = node.get_attribute("tag"); + return crypto; + } + + public StanzaNode to_xml() { + StanzaNode node = new StanzaNode.build("crypto", NS_URI) + .put_attribute("crypto-suite", crypto_suite) + .put_attribute("key-params", key_params) + .put_attribute("tag", tag); + if (session_params != null) node.put_attribute("session-params", session_params); + return node; + } +} + +} diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala new file mode 100644 index 00000000..faba38c9 --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala @@ -0,0 +1,99 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; + +public class Xmpp.Xep.JingleRtp.PayloadType { + public const string NAME = "payload-type"; + + public uint8 id { get; set; } + public string? name { get; set; } + public uint8 channels { get; set; default = 1; } + public uint32 clockrate { get; set; } + public uint32 maxptime { get; set; } + public uint32 ptime { get; set; } + public Map<string, string> parameters = new HashMap<string, string>(); + public Gee.List<RtcpFeedback> rtcp_fbs = new ArrayList<RtcpFeedback>(); + + public static PayloadType parse(StanzaNode node) { + PayloadType payloadType = new PayloadType(); + payloadType.channels = (uint8) node.get_attribute_uint("channels", payloadType.channels); + payloadType.clockrate = node.get_attribute_uint("clockrate"); + payloadType.id = (uint8) node.get_attribute_uint("id"); + payloadType.maxptime = node.get_attribute_uint("maxptime"); + payloadType.name = node.get_attribute("name"); + payloadType.ptime = node.get_attribute_uint("ptime"); + foreach (StanzaNode parameter in node.get_subnodes("parameter")) { + payloadType.parameters[parameter.get_attribute("name")] = parameter.get_attribute("value"); + } + foreach (StanzaNode subnode in node.get_subnodes(RtcpFeedback.NAME, RtcpFeedback.NS_URI)) { + payloadType.rtcp_fbs.add(RtcpFeedback.parse(subnode)); + } + return payloadType; + } + + public StanzaNode to_xml() { + StanzaNode node = new StanzaNode.build(NAME, NS_URI) + .put_attribute("id", id.to_string()); + if (channels != 1) node.put_attribute("channels", channels.to_string()); + if (clockrate != 0) node.put_attribute("clockrate", clockrate.to_string()); + if (maxptime != 0) node.put_attribute("maxptime", maxptime.to_string()); + if (name != null) node.put_attribute("name", name); + if (ptime != 0) node.put_attribute("ptime", ptime.to_string()); + foreach (string parameter in parameters.keys) { + node.put_node(new StanzaNode.build("parameter", NS_URI) + .put_attribute("name", parameter) + .put_attribute("value", parameters[parameter])); + } + foreach (RtcpFeedback rtcp_fb in rtcp_fbs) { + node.put_node(rtcp_fb.to_xml()); + } + return node; + } + + public PayloadType clone() { + PayloadType clone = new PayloadType(); + clone.id = id; + clone.name = name; + clone.channels = channels; + clone.clockrate = clockrate; + clone.maxptime = maxptime; + clone.ptime = ptime; + clone.parameters.set_all(parameters); + clone.rtcp_fbs.add_all(rtcp_fbs); + return clone; + } + + public static bool equals_func(PayloadType a, PayloadType b) { + return a.id == b.id && + a.name == b.name && + a.channels == b.channels && + a.clockrate == b.clockrate && + a.maxptime == b.maxptime && + a.ptime == b.ptime; + } +} + +public class Xmpp.Xep.JingleRtp.RtcpFeedback { + public const string NS_URI = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; + public const string NAME = "rtcp-fb"; + + public string type_ { get; private set; } + public string? subtype { get; private set; } + + public RtcpFeedback(string type, string? subtype = null) { + this.type_ = type; + this.subtype = subtype; + } + + public static RtcpFeedback parse(StanzaNode node) { + return new RtcpFeedback(node.get_attribute("type"), node.get_attribute("subtype")); + } + + public StanzaNode to_xml() { + StanzaNode node = new StanzaNode.build(NAME, NS_URI) + .add_self_xmlns() + .put_attribute("type", type_); + if (subtype != null) node.put_attribute("subtype", subtype); + return node; + } +}
\ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala new file mode 100644 index 00000000..32cd9016 --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala @@ -0,0 +1,67 @@ +using Gee; +using Xmpp; +using Xmpp.Xep; + +namespace Xmpp.Xep.JingleRtp { + + public enum CallSessionInfo { + ACTIVE, + HOLD, + UNHOLD, + MUTE, + UNMUTE, + RINGING + } + + public class SessionInfoType : Jingle.SessionInfoNs, Object { + public const string NS_URI = "urn:xmpp:jingle:apps:rtp:info:1"; + public string ns_uri { get { return NS_URI; } } + + public signal void info_received(Jingle.Session session, CallSessionInfo info); + public signal void mute_update_received(Jingle.Session session, bool mute, string name); + + public void handle_content_session_info(XmppStream stream, Jingle.Session session, StanzaNode info, Iq.Stanza iq) throws Jingle.IqError { + switch (info.name) { + case "active": + info_received(session, CallSessionInfo.ACTIVE); + break; + case "hold": + info_received(session, CallSessionInfo.HOLD); + break; + case "unhold": + info_received(session, CallSessionInfo.UNHOLD); + break; + case "mute": + string? name = info.get_attribute("name"); + mute_update_received(session, true, name); + info_received(session, CallSessionInfo.MUTE); + break; + case "unmute": + string? name = info.get_attribute("name"); + mute_update_received(session, false, name); + info_received(session, CallSessionInfo.UNMUTE); + break; + case "ringing": + info_received(session, CallSessionInfo.RINGING); + break; + } + } + + public void send_mute(Jingle.Session session, bool mute, string media) { + string node_name = mute ? "mute" : "unmute"; + + foreach (Jingle.Content content in session.contents) { + Parameters? parameters = content.content_params as Parameters; + if (parameters != null && parameters.media == media) { + StanzaNode session_info_content = new StanzaNode.build(node_name, NS_URI).add_self_xmlns().put_attribute("name", content.content_name); + session.send_session_info(session_info_content); + } + } + } + + public void send_ringing(Jingle.Session session) { + StanzaNode session_info_content = new StanzaNode.build("ringing", NS_URI).add_self_xmlns(); + session.send_session_info(session_info_content); + } + } +} diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala new file mode 100644 index 00000000..65be8a0a --- /dev/null +++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala @@ -0,0 +1,76 @@ +public abstract class Xmpp.Xep.JingleRtp.Stream : Object { + + public Jingle.Content content { get; protected set; } + + public string name { get { + return content.content_name; + }} + public string? media { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).media; + } + return null; + }} + public JingleRtp.PayloadType? payload_type { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).agreed_payload_type; + } + return null; + }} + public JingleRtp.Crypto? local_crypto { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).local_crypto; + } + return null; + }} + public JingleRtp.Crypto? remote_crypto { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).remote_crypto; + } + return null; + }} + public Gee.List<JingleRtp.HeaderExtension>? header_extensions { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).header_extensions; + } + return null; + }} + public bool sending { get { + return content.session.senders_include_us(content.senders); + }} + public bool receiving { get { + return content.session.senders_include_counterpart(content.senders); + }} + public bool rtcp_mux { get { + var content_params = content.content_params; + if (content_params is Parameters) { + return ((Parameters)content_params).rtcp_mux; + } + return false; + }} + + protected Stream(Jingle.Content content) { + this.content = content; + } + + public signal void on_send_rtp_data(Bytes bytes); + public signal void on_send_rtcp_data(Bytes bytes); + + public abstract void on_recv_rtp_data(Bytes bytes); + public abstract void on_recv_rtcp_data(Bytes bytes); + + public abstract void on_rtp_ready(); + public abstract void on_rtcp_ready(); + + public abstract void create(); + public abstract void destroy(); + + public string to_string() { + return @"$name/$media stream in $(content.session.sid)"; + } +}
\ No newline at end of file |