git » sdk » commit c4d72fc

Inbound call UI working

author Stephen Paul Weber
2023-09-28 01:36:32 UTC
committer Stephen Paul Weber
2023-09-28 01:59:15 UTC
parent a109683261438a40b3e777b1f734f475c8443c30

Inbound call UI working

xmpp/Chat.hx +27 -0
xmpp/Client.hx +78 -2
xmpp/PeerConnection.js.hx +0 -0
xmpp/jingle/PeerConnection.js.hx +1 -0
xmpp/jingle/Session.hx +123 -1

diff --git a/xmpp/Chat.hx b/xmpp/Chat.hx
index bac2e93..2343135 100644
--- a/xmpp/Chat.hx
+++ b/xmpp/Chat.hx
@@ -8,6 +8,7 @@ import xmpp.Chat;
 import xmpp.GenericStream;
 import xmpp.queries.MAMQuery;
 import xmpp.Color;
+using Lambda;
 
 enum ChatType {
 	ChatTypeDirect;
@@ -24,6 +25,7 @@ abstract class Chat {
 	private var trusted:Bool = false;
 	public var chatId(default, null):String;
 	public var type(default, null):Null<ChatType>;
+	public var jingleSessions: Map<String, xmpp.jingle.Session> = [];
 
 	private function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, type:ChatType) {
 		this.client = client;
@@ -62,6 +64,31 @@ abstract class Chat {
 		return this.trusted;
 	}
 
+	public function acceptCall() {
+		for (session in jingleSessions) {
+			session.accept();
+		}
+	}
+
+	public function hangup() {
+		for (session in jingleSessions) {
+			session.hangup();
+			jingleSessions.remove(session.sid);
+		}
+	}
+
+	public function callStatus() {
+		for (session in jingleSessions) {
+			return session.callStatus();
+		}
+
+		return "none";
+	}
+
+	public function videoTracks() {
+		return jingleSessions.flatMap((session) -> session.videoTracks());
+	}
+
 	public function onMessage(handler:ChatMessage->Void):Void {
 		this.stream.on("message", function(event) {
 			final stanza:Stanza = event.stanza;
diff --git a/xmpp/Client.hx b/xmpp/Client.hx
index c2401e9..587a437 100644
--- a/xmpp/Client.hx
+++ b/xmpp/Client.hx
@@ -10,6 +10,7 @@ import xmpp.EventEmitter;
 import xmpp.EventHandler;
 import xmpp.PubsubEvent;
 import xmpp.Stream;
+import xmpp.jingle.Session;
 import xmpp.queries.DiscoInfoGet;
 import xmpp.queries.ExtDiscoGet;
 import xmpp.queries.GenericQuery;
@@ -27,7 +28,22 @@ class Client extends xmpp.EventEmitter {
 	public var jid(default,null):String;
 	private var chats: ChatList = [];
 	private var persistence: Persistence;
-	private final caps = new Caps("https://sdk.snikket.org", [], ["urn:xmpp:avatar:metadata+notify"]);
+	private final caps = new Caps(
+		"https://sdk.snikket.org",
+		[],
+		[
+			"http://jabber.org/protocol/disco#info",
+			"http://jabber.org/protocol/caps",
+			"urn:xmpp:avatar:metadata+notify",
+			"urn:xmpp:jingle-message:0",
+			"urn:xmpp:jingle:1",
+			"urn:xmpp:jingle:apps:dtls:0",
+			"urn:xmpp:jingle:apps:rtp:1",
+			"urn:xmpp:jingle:apps:rtp:audio",
+			"urn:xmpp:jingle:apps:rtp:video",
+			"urn:xmpp:jingle:transports:ice-udp:1"
+		]
+	);
 
 	public function new(jid: String, persistence: Persistence) {
 		super();
@@ -59,6 +75,27 @@ class Client extends xmpp.EventEmitter {
 		}
 		this.stream.on("message", function(event) {
 			final stanza:Stanza = event.stanza;
+			final from = stanza.attr.get("from") == null ? null : JID.parse(stanza.attr.get("from"));
+
+			final jmiP = stanza.getChild("propose", "urn:xmpp:jingle-message:0");
+			if (jmiP != null && jmiP.attr.get("id") != null) {
+				final session = new IncomingProposedSession(this, from, jmiP.attr.get("id"));
+				final chat = getDirectChat(from.asBare().asString());
+				chat.jingleSessions.set(session.sid, session);
+				chatActivity(chat);
+				session.ring();
+			}
+
+			final jmiR = stanza.getChild("retract", "urn:xmpp:jingle-message:0");
+			if (jmiR != null && jmiR.attr.get("id") != null) {
+				final chat = getDirectChat(from.asBare().asString());
+				final session = chat.jingleSessions.get(jmiR.attr.get("id"));
+				if (session != null) {
+					session.retract();
+					chat.jingleSessions.remove(session.sid);
+				}
+			}
+
 			final chatMessage = ChatMessage.fromStanza(stanza, jid);
 			if (chatMessage != null) {
 				var chat = getDirectChat(chatMessage.conversation());
@@ -107,6 +144,41 @@ class Client extends xmpp.EventEmitter {
 
 		this.stream.on("iq", function(event) {
 			final stanza:Stanza = event.stanza;
+			final from = stanza.attr.get("from") == null ? null : JID.parse(stanza.attr.get("from"));
+
+			final jingle = stanza.getChild("jingle", "urn:xmpp:jingle:1");
+			if (stanza.attr.get("type") == "set" && jingle != null) {
+				// First, jingle requires useless replies to every iq
+				sendStanza(new Stanza("iq", { type: "result", to: stanza.attr.get("from"), id: stanza.attr.get("id") }));
+				final chat = getDirectChat(from.asBare().asString());
+				final session = chat.jingleSessions.get(jingle.attr.get("sid"));
+
+				if (jingle.attr.get("action") == "session-initiate") {
+					final newSession = new xmpp.jingle.InitiatedSession(this, stanza);
+					if (session != null) {
+						final nextSession = session.initiate(newSession);
+						if (nextSession == null) {
+							chat.jingleSessions.remove(session.sid);
+						} else {
+							chat.jingleSessions.set(session.sid, nextSession);
+						}
+					} else {
+						chat.jingleSessions.set(session.sid, newSession);
+						chatActivity(chat);
+						newSession.ring();
+					}
+				}
+
+				if (session != null && jingle.attr.get("action") == "session-terminate") {
+					session.terminate();
+					chat.jingleSessions.remove(jingle.attr.get("sid"));
+				}
+
+				if (session != null && jingle.attr.get("action") == "transport-info") {
+					session.transportInfo(stanza);
+				}
+				return EventHandled;
+			}
 
 			if (stanza.attr.get("type") == "get" && stanza.getChild("query", "http://jabber.org/protocol/disco#info") != null) {
 				stream.sendStanza(caps.discoReply(stanza));
@@ -178,7 +250,7 @@ class Client extends xmpp.EventEmitter {
 		rosterGet();
 		sync(() -> {
 			// Set self to online
-			stream.sendStanza(caps.addC(new Stanza("presence")));
+			sendPresence();
 			this.trigger("status/online", {});
 		});
 
@@ -270,6 +342,10 @@ class Client extends xmpp.EventEmitter {
 		stream.sendStanza(stanza);
 	}
 
+	public function sendPresence(?to: String) {
+		sendStanza(caps.addC(new Stanza("presence", to == null ? {} : { to: to })));
+	}
+
 	#if js
 	public function subscribePush(reg: js.html.ServiceWorkerRegistration, push_service: String, vapid_key: { publicKey: js.html.CryptoKey, privateKey: js.html.CryptoKey}) {
 		js.Browser.window.crypto.subtle.exportKey("raw", vapid_key.publicKey).then((vapid_public_raw) -> {
diff --git a/xmpp/PeerConnection.js.hx b/xmpp/PeerConnection.js.hx
new file mode 100644
index 0000000..e69de29
diff --git a/xmpp/jingle/PeerConnection.js.hx b/xmpp/jingle/PeerConnection.js.hx
index ae07cae..1480ec8 100644
--- a/xmpp/jingle/PeerConnection.js.hx
+++ b/xmpp/jingle/PeerConnection.js.hx
@@ -6,3 +6,4 @@ typedef PeerConnection = js.html.rtc.PeerConnection;
 typedef SdpType = js.html.rtc.SdpType;
 typedef Promise<T> = js.lib.Promise<T>;
 typedef MediaStream = js.html.MediaStream;
+typedef MediaStreamTrack = js.html.MediaStreamTrack;
diff --git a/xmpp/jingle/Session.hx b/xmpp/jingle/Session.hx
index 0105d2d..ce7bad9 100644
--- a/xmpp/jingle/Session.hx
+++ b/xmpp/jingle/Session.hx
@@ -1,10 +1,93 @@
 package xmpp.jingle;
 
+import xmpp.ID;
 import xmpp.jingle.PeerConnection;
 import xmpp.jingle.SessionDescription;
 using Lambda;
 
-class Session {
+interface Session {
+	public var sid (get, null): String;
+	public function initiate(session: InitiatedSession): Null<InitiatedSession>;
+	public function accept(): Void;
+	public function hangup(): Void;
+	public function retract(): Void;
+	public function terminate(): Void;
+	public function transportInfo(stanza: Stanza): Promise<Void>;
+	public function callStatus():String;
+	public function videoTracks():Array<MediaStreamTrack>;
+}
+
+class IncomingProposedSession implements Session {
+	public var sid (get, null): String;
+	private final client: Client;
+	private final from: JID;
+	private final _sid: String;
+	private var accepted: Bool = false;
+
+	public function new(client: Client, from: JID, sid: String) {
+		this.client = client;
+		this.from = from;
+		this._sid = sid;
+	}
+
+	public function ring() {
+		// XEP-0353 says to send <ringing/> but that leaks presence if not careful
+		client.trigger("call/ring", { chatId: from.asBare().asString(), session: this });
+	}
+
+	public function hangup() {
+		// XEP-0353 says to send <reject/> but that leaks presence if not careful
+		// It also tells all other devices to stop ringing, which you may or may not want
+		client.getDirectChat(from.asBare().asString(), false).jingleSessions.remove(sid);
+	}
+
+	public function retract() {
+		// Other side retracted, stop ringing
+		client.trigger("call/retract", { chatId: from.asBare().asString() });
+	}
+
+	public function terminate() {
+		trace("Tried to terminate before session-inititate: " + sid, this);
+	}
+
+	public function transportInfo(_) {
+		trace("Got transport-info before session-inititate: " + sid, this);
+		return Promise.resolve(null);
+	}
+
+	public function accept() {
+		if (accepted) return;
+		accepted = true;
+		client.sendPresence(from.asString());
+		client.sendStanza(
+			new Stanza("message", { to: from.asString(), type: "chat" })
+				.tag("proceed", { xmlns: "urn:xmpp:jingle-message:0", id: sid }).up()
+				.tag("store", { xmlns: "urn:xmpp:hints" })
+		);
+	}
+
+	public function initiate(session: InitiatedSession) {
+		// TODO: check if new session has corrent media
+		if (session.sid != sid) return null;
+		if (!accepted) return null;
+		session.accept();
+		return session;
+	}
+
+	public function callStatus() {
+		return "incoming";
+	}
+
+	public function videoTracks() {
+		return [];
+	}
+
+	private function get_sid() {
+		return this._sid;
+	}
+}
+
+class InitiatedSession implements Session {
 	public var sid (get, null): String;
 	private final client: Client;
 	private final sessionInitiate: Stanza;
@@ -13,6 +96,7 @@ class Session {
 	private var pc: PeerConnection = null;
 	private final queuedInboundTransportInfo: Array<Stanza> = [];
 	private final queuedOutboundCandidate: Array<{ candidate: String, sdpMid: String, usernameFragment: String }> = [];
+	private var accepted: Bool = false;
 
 	public function new(client: Client, sessionInitiate: Stanza) {
 		this.client = client;
@@ -25,10 +109,36 @@ class Session {
 		return jingle.attr.get("sid");
 	}
 
+	public function ring() {
+		client.trigger("call/ring", { chatId: JID.parse(sessionInitiate.attr.get("from")).asBare().asString(), session: this });
+	}
+
+	public function retract() {
+		trace("Tried to retract session in wrong state: " + sid, this);
+	}
+
 	public function accept() {
+		if (accepted) return;
+		accepted = true;
 		client.trigger("call/media", { session: this });
 	}
 
+	public function hangup() {
+		client.sendStanza(
+			new Stanza("iq", { to: sessionInitiate.attr.get("from"), type: "set", id: ID.medium() })
+				.tag("jingle", { xmlns: "urn:xmpp:jingle:1", action: "session-terminate", sid: sid })
+				.tag("reason").tag("success")
+				.up().up().up()
+		);
+		terminate();
+		client.trigger("call/retract", { chatId: JID.parse(sessionInitiate.attr.get("from")).asBare().asString() });
+	}
+
+	public function initiate(session: InitiatedSession) {
+		trace("Trying to inititate already initiated session: " + sid);
+		return null;
+	}
+
 	public function terminate() {
 		if (pc == null) return;
 		pc.close();
@@ -37,6 +147,7 @@ class Session {
 				tranceiver.sender.track.stop();
 			}
 		}
+		pc = null;
 	}
 
 	public function transportInfo(stanza: Stanza) {
@@ -56,6 +167,17 @@ class Session {
 		})).then((_) -> {});
 	}
 
+	public function callStatus() {
+		return "ongoing";
+	}
+
+	public function videoTracks() {
+		if (pc == null) return [];
+		return pc.getTransceivers()
+			.filter((t) -> t.receiver != null && t.receiver.track != null && t.receiver.track.kind == "video" && !t.receiver.track.muted)
+			.map((t) -> t.receiver.track);
+	}
+
 	private function sendIceCandidate(candidate: { candidate: String, sdpMid: String, usernameFragment: String }) {
 		if (candidate == null) return; // All candidates received now
 		if (candidate.candidate == "") return; // All candidates received now