| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-10-05 03:27:41 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-10-05 03:28:01 UTC |
| parent | 333e9ad9b2a06b6954c6dba4172dc51cc3bd83a6 |
| 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; }