git » sdk » commit cbd0f38

Support inbound content-add

author Stephen Paul Weber
2023-10-05 03:27:41 UTC
committer Stephen Paul Weber
2023-10-05 03:28:01 UTC
parent 333e9ad9b2a06b6954c6dba4172dc51cc3bd83a6

Support inbound content-add

xmpp/Client.hx +4 -0
xmpp/jingle/Session.hx +77 -13
xmpp/jingle/SessionDescription.hx +86 -14

diff --git a/xmpp/Client.hx b/xmpp/Client.hx
index 41a3ad6..39ea31c 100644
--- a/xmpp/Client.hx
+++ b/xmpp/Client.hx
@@ -206,6 +206,10 @@ class Client extends xmpp.EventEmitter {
 					chat.jingleSessions.remove(jingle.attr.get("sid"));
 				}
 
+				if (session != null && jingle.attr.get("action") == "content-add") {
+					session.contentAdd(stanza);
+				}
+
 				if (session != null && jingle.attr.get("action") == "transport-info") {
 					session.transportInfo(stanza);
 				}
diff --git a/xmpp/jingle/Session.hx b/xmpp/jingle/Session.hx
index c6967ea..a01f667 100644
--- a/xmpp/jingle/Session.hx
+++ b/xmpp/jingle/Session.hx
@@ -12,6 +12,7 @@ interface Session {
 	public function hangup(): Void;
 	public function retract(): Void;
 	public function terminate(): Void;
+	public function contentAdd(stanza: Stanza): Void;
 	public function transportInfo(stanza: Stanza): Promise<Void>;
 	public function callStatus():String;
 	public function videoTracks():Array<MediaStreamTrack>;
@@ -50,6 +51,10 @@ class IncomingProposedSession implements Session {
 		trace("Tried to terminate before session-initiate: " + sid, this);
 	}
 
+	public function contentAdd(_) {
+		trace("Got content-add before session-initiate: " + sid, this);
+	}
+
 	public function transportInfo(_) {
 		trace("Got transport-info before session-initiate: " + sid, this);
 		return Promise.resolve(null);
@@ -139,6 +144,10 @@ class OutgoingProposedSession implements Session {
 		trace("Tried to terminate before session-initiate: " + sid, this);
 	}
 
+	public function contentAdd(_) {
+		trace("Got content-add before session-initiate: " + sid, this);
+	}
+
 	public function transportInfo(_) {
 		trace("Got transport-info before session-initiate: " + sid, this);
 		return Promise.resolve(null);
@@ -179,15 +188,19 @@ class InitiatedSession implements Session {
 	private var remoteDescription: Null<SessionDescription> = null;
 	private var localDescription: Null<SessionDescription> = null;
 	private var pc: PeerConnection = null;
+	private var peerDtlsSetup: String = "actpass";
 	private final queuedInboundTransportInfo: Array<Stanza> = [];
 	private final queuedOutboundCandidate: Array<{ candidate: String, sdpMid: String, usernameFragment: String }> = [];
 	private var accepted: Bool = false;
+	private var afterMedia: Null<()->Void> = null;
+	private final initiator: Bool;
 
 	public function new(client: Client, counterpart: JID, sid: String, remoteDescription: Null<SessionDescription>) {
 		this.client = client;
 		this.counterpart = counterpart;
 		this._sid = sid;
 		this.remoteDescription = remoteDescription;
+		this.initiator = remoteDescription == null;
 	}
 
 	public static function fromSessionInitiate(client: Client, stanza: Stanza): InitiatedSession {
@@ -249,6 +262,29 @@ class InitiatedSession implements Session {
 		pc = null;
 	}
 
+	public function contentAdd(stanza: Stanza) {
+		if (remoteDescription == null) throw "Got content-add before session-accept";
+
+		final addThis = SessionDescription.fromStanza(stanza, initiator, remoteDescription);
+		var video = false;
+		var audio = false;
+		for (m in addThis.media) {
+			if (m.attributes.exists((attr) -> attr.key == "sendrecv" || attr.key == "sendonly")) {
+				if (m.media == "video") video = true;
+				if (m.media == "audio") audio = true;
+			}
+			m.attributes.push(new Attribute("setup", peerDtlsSetup));
+		}
+		remoteDescription = remoteDescription.addContent(addThis);
+		pc.setRemoteDescription({ type: SdpType.OFFER, sdp: remoteDescription.toSdp() }).then((_) -> {
+			afterMedia = () -> {
+				setupLocalDescription("content-accept", addThis.media.map((m) -> m.mid));
+				afterMedia = null;
+			};
+			client.trigger("call/media", { session: this, audio: audio, video: video });
+		});
+	}
+
 	public function transportInfo(stanza: Stanza) {
 		if (pc == null) {
 			queuedInboundTransportInfo.push(stanza);
@@ -301,13 +337,36 @@ class InitiatedSession implements Session {
 				media.formats
 			),
 			sid
-		).toStanza(false);
+		).toStanza(initiator);
 		transportInfo.attr.set("to", counterpart.asString());
 		transportInfo.attr.set("id", ID.medium());
 		client.sendStanza(transportInfo);
 	}
 
 	public function supplyMedia(streams: Array<MediaStream>) {
+		setupPeerConnection(() -> {
+			for (stream in streams) {
+				for (track in stream.getTracks()) {
+					pc.addTrack(track, stream);
+				}
+			}
+
+			if (afterMedia == null) {
+				onPeerConnection().catchError((e) -> {
+					trace("supplyMedia error", e);
+					pc.close();
+				});
+			} else {
+				afterMedia();
+			}
+		});
+	}
+
+	private function setupPeerConnection(callback: ()->Void) {
+		if (pc != null) {
+			callback();
+			return;
+		}
 		client.getIceServers((servers) -> {
 			pc = new PeerConnection({ iceServers: servers });
 			pc.addEventListener("track", (event) -> {
@@ -317,23 +376,24 @@ class InitiatedSession implements Session {
 			pc.addEventListener("icecandidate", (event) -> {
 				sendIceCandidate(event.candidate);
 			});
-			for (stream in streams) {
-				for (track in stream.getTracks()) {
-					pc.addTrack(track, stream);
-				}
-			}
-
-			onPeerConnection().catchError((e) -> {
-				trace("supplyMedia error", e);
-				pc.close();
-			});
+			callback();
 		});
 	}
 
-	private function setupLocalDescription(type: String) {
+	private function setupLocalDescription(type: String, ?filterMedia: Array<String>) {
 		return pc.setLocalDescription(null).then((_) -> {
 			localDescription = SessionDescription.parse(pc.localDescription.sdp);
-			final sessionAccept = localDescription.toStanza(type, sid, false);
+			var descriptionToSend = localDescription;
+			if (filterMedia != null) {
+				descriptionToSend = new SessionDescription(
+					descriptionToSend.version,
+					descriptionToSend.name,
+					descriptionToSend.media.filter((m) -> filterMedia.contains(m.mid)),
+					descriptionToSend.attributes,
+					descriptionToSend.identificationTags
+				);
+			}
+			final sessionAccept = descriptionToSend.toStanza(type, sid, initiator);
 			sessionAccept.attr.set("to", counterpart.asString());
 			sessionAccept.attr.set("id", ID.medium());
 			client.sendStanza(sessionAccept);
@@ -355,6 +415,9 @@ class InitiatedSession implements Session {
 		})
 		.then((_) -> {
 			setupLocalDescription("session-accept");
+		}).then((_) -> {
+			peerDtlsSetup = localDescription.getDtlsSetup() == "active" ? "passive" : "active";
+			return;
 		});
 	}
 }
@@ -370,6 +433,7 @@ class OutgoingSession extends InitiatedSession {
 
 	public override function initiate(stanza: Stanza) {
 		remoteDescription = SessionDescription.fromStanza(stanza, true);
+		peerDtlsSetup = remoteDescription.getDtlsSetup();
 		pc.setRemoteDescription({ type: SdpType.ANSWER, sdp: remoteDescription.toSdp() })
 		  .then((_) -> transportInfo(stanza));
 		return this;
diff --git a/xmpp/jingle/SessionDescription.hx b/xmpp/jingle/SessionDescription.hx
index cf07a29..c23fe33 100644
--- a/xmpp/jingle/SessionDescription.hx
+++ b/xmpp/jingle/SessionDescription.hx
@@ -96,11 +96,11 @@ class SessionDescription {
 		return new SessionDescription(version, name, media, attributes, tags);
 	}
 
-	public static function fromStanza(iq: Stanza, initiator: Bool) {
+	public static function fromStanza(iq: Stanza, initiator: Bool, ?existingDescription: SessionDescription) {
 		final attributes: Array<Attribute> = [];
 		final jingle = iq.getChild("jingle", "urn:xmpp:jingle:1");
 		final group = jingle.getChild("group", "urn:xmpp:jingle:apps:grouping:0");
-		final media = jingle.allTags("content").map((el) -> Media.fromElement(el, initiator, group != null));
+		final media = jingle.allTags("content").map((el) -> Media.fromElement(el, initiator, group != null, existingDescription));
 
 		var tags: Array<String>;
 		if (group != null) {
@@ -115,6 +115,62 @@ class SessionDescription {
 		return new SessionDescription(0, "-", media, attributes, tags);
 	}
 
+	public function getUfragPwd() {
+		var ufragPwd = null;
+		for (m in media) {
+			final mUfragPwd = m.getUfragPwd();
+			if (ufragPwd != null && mUfragPwd.ufrag != ufragPwd.ufrag) throw "ufrag not unique";
+			if (ufragPwd != null && mUfragPwd.pwd != ufragPwd.pwd) throw "pwd not unique";
+			ufragPwd = mUfragPwd;
+		}
+
+		if (ufragPwd == null) throw "no ufrag or pwd found";
+		return ufragPwd;
+	}
+
+	public function getFingerprint() {
+		var fingerprint = attributes.find((attr) -> attr.key == "fingerprint");
+		if (fingerprint != null) return fingerprint;
+
+		for (m in media) {
+			final mFingerprint = m.attributes.find((attr) -> attr.key == "fingerprint");
+			if (fingerprint != null && mFingerprint != null && fingerprint.value != mFingerprint.value) throw "fingerprint not unique";
+			fingerprint = mFingerprint;
+		}
+
+		if (fingerprint == null) throw "no fingerprint found";
+		return fingerprint;
+	}
+
+	public function getDtlsSetup() {
+		var setup = attributes.find((attr) -> attr.key == "setup");
+		if (setup != null) return setup.value;
+
+		for (m in media) {
+			final mSetup = m.attributes.find((attr) -> attr.key == "setup");
+			if (setup != null && mSetup != null && setup.value != mSetup.value) throw "setup not unique";
+			setup = mSetup;
+		}
+
+		if (setup == null) throw "no setup found";
+		return setup.value;
+	}
+
+	public function addContent(newDescription: SessionDescription) {
+		for (newM in newDescription.media) {
+			if (media.find((m) -> m.mid == newM.mid) != null) {
+				throw "Media with id " + newM.mid + " already exists!";
+			}
+		}
+		return new SessionDescription(
+			version,
+			name,
+			media.concat(newDescription.media),
+			attributes.filter((attr) -> attr.key != "group").concat(newDescription.attributes.filter((attr) -> attr.key == "group")),
+			newDescription.identificationTags
+		);
+	}
+
 	public function toSdp() {
 		return
 			"v=" + version + "\r\n" +
@@ -177,23 +233,33 @@ class Media {
 		this.formats = formats;
 	}
 
-	public static function fromElement(content: Stanza, initiator: Bool, hasGroup: Bool) {
+	public static function fromElement(content: Stanza, initiator: Bool, hasGroup: Bool, ?existingDescription: SessionDescription) {
 		final mediaAttributes: Array<Attribute> = [];
 		final mediaFormats: Array<Int> = [];
 		final mid = content.attr.get("name");
 		final transport = content.getChild("transport", "urn:xmpp:jingle:transports:ice-udp:1");
 		if (transport == null) throw "ice-udp transport is missing";
 
-		if (transport.attr.get("ufrag") == null) throw "transport is missing ufrag";
-		final ufrag = transport.attr.get("ufrag");
+		var ufrag = transport.attr.get("ufrag");
+		var pwd = transport.attr.get("pwd");
+		if ((ufrag == null || pwd == null) && existingDescription != null) {
+			final ufragPwd = existingDescription.getUfragPwd();
+			ufrag = ufragPwd.ufrag;
+			pwd = ufragPwd.pwd;
+		}
+		if (ufrag == null) throw "transport is missing ufrag";
 		mediaAttributes.push(new Attribute("ice-ufrag", ufrag));
 
-		if (transport.attr.get("pwd") == null) throw "transport is missing pwd";
-		mediaAttributes.push(new Attribute("ice-pwd", transport.attr.get("pwd")));
+		if (pwd == null) throw "transport is missing pwd";
+		mediaAttributes.push(new Attribute("ice-pwd", pwd));
 		mediaAttributes.push(new Attribute("ice-options", "trickle"));
 
 		final fingerprint = transport.getChild("fingerprint", "urn:xmpp:jingle:apps:dtls:0");
-		if (fingerprint != null) {
+		if (fingerprint == null) {
+			if (existingDescription != null) {
+				mediaAttributes.push(existingDescription.getFingerprint());
+			}
+		} else {
 			mediaAttributes.push(new Attribute("fingerprint", fingerprint.attr.get("hash") + " " + fingerprint.getText()));
 			if (fingerprint.attr.get("setup") != null) {
 				mediaAttributes.push(new Attribute("setup", fingerprint.attr.get("setup")));
@@ -299,7 +365,7 @@ class Media {
 	}
 
 	public function contentElement(initiator: Bool) {
-			final attrs: DynamicAccess<String> = { xmlns: "urn:xmpp:jingle:1", creator: "initiator", name: mid };
+		final attrs: DynamicAccess<String> = { xmlns: "urn:xmpp:jingle:1", creator: "initiator", name: mid };
 		if (attributes.exists((attr) -> attr.key == "inactive")) {
 			attrs.set("senders", "none");
 		} else if (attributes.exists((attr) -> attr.key == "sendonly")) {
@@ -399,12 +465,18 @@ class Media {
 		return content;
 	}
 
-	public function toTransportElement(sessionAttributes: Array<Attribute>) {
-		final transportAttr: DynamicAccess<String> = { xmlns: "urn:xmpp:jingle:transports:ice-udp:1" };
+	public function getUfragPwd() {
 		final ufrag = attributes.find((attr) -> attr.key == "ice-ufrag");
-		if (ufrag != null) transportAttr.set("ufrag", ufrag.value);
 		final pwd = attributes.find((attr) -> attr.key == "ice-pwd");
-		if (pwd != null) transportAttr.set("pwd", pwd.value);
+		if (ufrag == null || pwd == null) throw "transport is missing ufrag or pwd";
+		return { ufrag: ufrag.value, pwd: pwd.value };
+	}
+
+	public function toTransportElement(sessionAttributes: Array<Attribute>) {
+		final transportAttr: DynamicAccess<String> = { xmlns: "urn:xmpp:jingle:transports:ice-udp:1" };
+		final ufragPwd = getUfragPwd();
+		transportAttr.set("ufrag", ufragPwd.ufrag);
+		transportAttr.set("pwd", ufragPwd.pwd);
 		final transport = new Stanza("transport", transportAttr);
 		final fingerprint = (attributes.concat(sessionAttributes)).find((attr) -> attr.key == "fingerprint");
 		final setup = (attributes.concat(sessionAttributes)).find((attr) -> attr.key == "setup");
@@ -412,7 +484,7 @@ class Media {
 			final pos = fingerprint.value.indexOf(" ");
 			transport.textTag("fingerprint", fingerprint.value.substr(pos + 1), { xmlns: "urn:xmpp:jingle:apps:dtls:0", hash: fingerprint.value.substr(0, pos), setup: setup.value });
 		}
-		transport.addChildren(attributes.filter((attr) -> attr.key == "candidate").map((attr) -> IceCandidate.parse(attr.value, mid, ufrag.value).toElement()));
+		transport.addChildren(attributes.filter((attr) -> attr.key == "candidate").map((attr) -> IceCandidate.parse(attr.value, mid, ufragPwd.ufrag).toElement()));
 		transport.up();
 		return transport;
 	}