aboutsummaryrefslogtreecommitdiff
path: root/xmpp-vala/src/module/xep/0167_jingle_rtp
diff options
context:
space:
mode:
Diffstat (limited to 'xmpp-vala/src/module/xep/0167_jingle_rtp')
-rw-r--r--xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala231
-rw-r--r--xmpp-vala/src/module/xep/0167_jingle_rtp/content_type.vala23
-rw-r--r--xmpp-vala/src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala290
-rw-r--r--xmpp-vala/src/module/xep/0167_jingle_rtp/payload_type.vala99
-rw-r--r--xmpp-vala/src/module/xep/0167_jingle_rtp/session_info_type.vala67
-rw-r--r--xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala76
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