diff --git a/libervia/backend/plugins/plugin_xep_0166/__init__.py b/libervia/backend/plugins/plugin_xep_0166/__init__.py
index 16e08ad3..e13ece2f 100644
--- a/libervia/backend/plugins/plugin_xep_0166/__init__.py
+++ b/libervia/backend/plugins/plugin_xep_0166/__init__.py
@@ -55,9 +55,6 @@ IQ_SET : Final = '/iq[@type="set"]'
NS_JINGLE : Final = "urn:xmpp:jingle:1"
NS_JINGLE_ERROR : Final = "urn:xmpp:jingle:errors:1"
JINGLE_REQUEST : Final = f'{IQ_SET}/jingle[@xmlns="{NS_JINGLE}"]'
-STATE_PENDING : Final = "PENDING"
-STATE_ACTIVE : Final = "ACTIVE"
-STATE_ENDED : Final = "ENDED"
CONFIRM_TXT : Final = D_(
"{entity} want to start a jingle session with you, do you accept ?"
)
@@ -89,6 +86,10 @@ class XEP_0166:
REASON_FAILED_TRANSPORT : Final = "failed-transport"
REASON_CONNECTIVITY_ERROR : Final = "connectivity-error"
+ STATE_PENDING : Final = "PENDING"
+ STATE_ACTIVE : Final = "ACTIVE"
+ STATE_ENDED : Final = "ENDED"
+
# standard actions
A_SESSION_INITIATE : Final = "session-initiate"
@@ -194,7 +195,7 @@ class XEP_0166:
session_data = {
"id": sid,
- "state": STATE_PENDING,
+ "state": XEP_0166.STATE_PENDING,
"initiator": client.jid if role == XEP_0166.ROLE_INITIATOR else peer_jid,
"role": role,
"local_jid": local_jid or client.jid,
@@ -502,7 +503,8 @@ class XEP_0166:
content_elt["creator"] = content_data["creator"]
if context_elt is not None:
- pass
+ if context_elt.parent is None:
+ content_elt.addChild(context_elt)
elif action == XEP_0166.A_TRANSPORT_INFO:
context_elt = transport_elt = content_elt.addElement(
"transport", content_data["transport"].namespace
@@ -1191,7 +1193,7 @@ class XEP_0166:
d_list.addCallback(lambda __: iq_elt.send())
def change_state(__, session):
- session["state"] = STATE_ACTIVE
+ session["state"] = XEP_0166.STATE_ACTIVE
d_list.addCallback(change_state, session)
d_list.addCallback(
@@ -1279,7 +1281,7 @@ class XEP_0166:
# at this point we can send the result to confirm reception of the request
client.send(xmlstream.toResponse(request, "result"))
# and change the state
- session["state"] = STATE_ACTIVE
+ session["state"] = XEP_0166.STATE_ACTIVE
session["jingle_elt"] = jingle_elt
await defer.DeferredList(self._call_plugins(
diff --git a/libervia/backend/plugins/plugin_xep_0167/__init__.py b/libervia/backend/plugins/plugin_xep_0167/__init__.py
index ac9fb961..4a1bf56c 100644
--- a/libervia/backend/plugins/plugin_xep_0167/__init__.py
+++ b/libervia/backend/plugins/plugin_xep_0167/__init__.py
@@ -115,19 +115,13 @@ class XEP_0167(BaseApplicationHandler):
# args: session_id, serialised setup data (dict with keys "role" and "sdp"),
# profile
- host.bridge.add_signal(
- "call_setup", ".plugin", signature="sss"
- )
+ host.bridge.add_signal("call_setup", ".plugin", signature="sss")
# args: session_id, data, profile
- host.bridge.add_signal(
- "call_ended", ".plugin", signature="sss"
- )
+ host.bridge.add_signal("call_ended", ".plugin", signature="sss")
# args: session_id, info_type, extra, profile
- host.bridge.add_signal(
- "call_info", ".plugin", signature="ssss"
- )
+ host.bridge.add_signal("call_info", ".plugin", signature="ssss")
def get_handler(self, client):
return XEP_0167_handler()
@@ -194,7 +188,9 @@ class XEP_0167(BaseApplicationHandler):
except KeyError:
log.warning(f"no media ID found for {media_type}: {media_data}")
try:
- call_data[media_type]["ice-candidates"] = transport_data["candidates"]
+ call_data[media_type]["ice-candidates"] = transport_data.get(
+ "candidates", []
+ )
metadata["ice-ufrag"] = transport_data["ufrag"]
metadata["ice-pwd"] = transport_data["pwd"]
except KeyError:
@@ -250,12 +246,7 @@ class XEP_0167(BaseApplicationHandler):
)
return sid
- def _call_answer_sdp(
- self,
- session_id: str,
- answer_sdp: str,
- profile: str
- ) -> None:
+ def _call_answer_sdp(self, session_id: str, answer_sdp: str, profile: str) -> None:
client = self.host.get_client(profile)
session = self._j.get_session(client, session_id)
try:
@@ -274,9 +265,7 @@ class XEP_0167(BaseApplicationHandler):
):
client = self.host.get_client(profile_key)
return defer.ensureDeferred(
- self.call_end(
- client, session_id, data_format.deserialise(data_s)
- )
+ self.call_end(client, session_id, data_format.deserialise(data_s))
)
async def call_end(
@@ -297,10 +286,7 @@ class XEP_0167(BaseApplicationHandler):
# jingle callbacks
async def confirm_incoming_call(
- self,
- client: SatXMPPEntity,
- session: dict,
- call_type: str
+ self, client: SatXMPPEntity, session: dict, call_type: str
) -> bool:
"""Prompt the user for a call confirmation.
@@ -341,10 +327,7 @@ class XEP_0167(BaseApplicationHandler):
return accepted
async def jingle_preflight(
- self,
- client: SatXMPPEntity,
- session: dict,
- description_elt: domish.Element
+ self, client: SatXMPPEntity, session: dict, description_elt: domish.Element
) -> None:
"""Perform preflight checks for an incoming call session.
@@ -380,7 +363,7 @@ class XEP_0167(BaseApplicationHandler):
self.host.bridge.call_ended(
session["id"],
data_format.serialise({"reason": "retracted"}),
- client.profile
+ client.profile,
)
raise e
@@ -392,13 +375,11 @@ class XEP_0167(BaseApplicationHandler):
client: SatXMPPEntity,
session: dict,
info_type: str,
- info_data: dict|None = None
+ info_data: dict | None = None,
) -> None:
if info_type == "ringing":
if not session.get("ringing", False):
- self.host.bridge.call_info(
- session["id"], "ringing", "", client.profile
- )
+ self.host.bridge.call_info(session["id"], "ringing", "", client.profile)
# we indicate that the ringing has started, to avoid sending several times
# the signal
session["ringing"] = True
@@ -406,10 +387,7 @@ class XEP_0167(BaseApplicationHandler):
log.warning(f"Unknown preflight info type: {info_type!r}")
async def jingle_preflight_cancel(
- self,
- client: SatXMPPEntity,
- session: dict,
- cancel_error: exceptions.CancelError
+ self, client: SatXMPPEntity, session: dict, cancel_error: exceptions.CancelError
) -> None:
"""The call has been rejected"""
# call_ended is use to send the signal only once even if there are audio and video
@@ -417,20 +395,15 @@ class XEP_0167(BaseApplicationHandler):
call_ended = session.get("call_ended", False)
if call_ended:
return
- data = {
- "reason": getattr(cancel_error, "reason", "cancelled")
- }
+ data = {"reason": getattr(cancel_error, "reason", "cancelled")}
text = getattr(cancel_error, "text", None)
if text:
data["text"] = text
self.host.bridge.call_ended(
- session["id"],
- data_format.serialise(data),
- client.profile
+ session["id"], data_format.serialise(data), client.profile
)
session["call_ended"] = True
-
def jingle_session_init(
self,
client: SatXMPPEntity,
@@ -507,11 +480,13 @@ class XEP_0167(BaseApplicationHandler):
self.host.bridge.call_setup(
session["id"],
- data_format.serialise({
- "role": session["role"],
- "sdp": sdp,
- }),
- client.profile
+ data_format.serialise(
+ {
+ "role": session["role"],
+ "sdp": sdp,
+ }
+ ),
+ client.profile,
)
answer_sdp = await answer_sdp_d
@@ -559,11 +534,13 @@ class XEP_0167(BaseApplicationHandler):
answer_sdp = mapping.generate_sdp_from_session(session)
self.host.bridge.call_setup(
session["id"],
- data_format.serialise({
- "role": session["role"],
- "sdp": answer_sdp,
- }),
- client.profile
+ data_format.serialise(
+ {
+ "role": session["role"],
+ "sdp": answer_sdp,
+ }
+ ),
+ client.profile,
)
else:
log.warning(f"FIXME: unmanaged action {action}")
@@ -635,9 +612,7 @@ class XEP_0167(BaseApplicationHandler):
reason_elt: domish.Element,
) -> None:
reason, text = self._j.parse_reason_elt(reason_elt)
- data = {
- "reason": reason
- }
+ data = {"reason": reason}
if text:
data["text"] = text
self.host.bridge.call_ended(
diff --git a/libervia/backend/plugins/plugin_xep_0167/mapping.py b/libervia/backend/plugins/plugin_xep_0167/mapping.py
index b343a32e..2979e62e 100644
--- a/libervia/backend/plugins/plugin_xep_0167/mapping.py
+++ b/libervia/backend/plugins/plugin_xep_0167/mapping.py
@@ -439,12 +439,11 @@ def parse_sdp(sdp: str) -> dict:
try:
ice_candidates = first_content["transport_data"]["candidates"]
except KeyError:
- log.warning("missing candidates in SDP")
- else:
- for idx, content in enumerate(all_media.values()):
- if idx == 0:
- continue
- content["transport_data"].setdefault("candidates", ice_candidates)
+ ice_candidates = []
+ for idx, content in enumerate(all_media.values()):
+ if idx == 0:
+ continue
+ content["transport_data"].setdefault("candidates", ice_candidates)
return call_data
diff --git a/libervia/backend/plugins/plugin_xep_0176.py b/libervia/backend/plugins/plugin_xep_0176.py
index 69c52210..1e0112de 100644
--- a/libervia/backend/plugins/plugin_xep_0176.py
+++ b/libervia/backend/plugins/plugin_xep_0176.py
@@ -36,7 +36,7 @@ from .plugin_xep_0166 import BaseTransportHandler
log = getLogger(__name__)
-NS_JINGLE_ICE_UDP= "urn:xmpp:jingle:transports:ice-udp:1"
+NS_JINGLE_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"
PLUGIN_INFO = {
C.PI_NAME: "Jingle ICE-UDP Transport Method",
@@ -53,7 +53,6 @@ PLUGIN_INFO = {
class XEP_0176(BaseTransportHandler):
-
def __init__(self, host):
log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
self.host = host
@@ -86,11 +85,13 @@ class XEP_0176(BaseTransportHandler):
profile_key: str,
):
client = self.host.get_client(profile_key)
- return defer.ensureDeferred(self.ice_candidates_add(
- client,
- session_id,
- data_format.deserialise(media_ice_data_s),
- ))
+ return defer.ensureDeferred(
+ self.ice_candidates_add(
+ client,
+ session_id,
+ data_format.deserialise(media_ice_data_s),
+ )
+ )
def build_transport(self, ice_data: dict) -> domish.Element:
"""Generate element from ICE data
@@ -119,14 +120,13 @@ class XEP_0176(BaseTransportHandler):
try:
ufrag: str = ice_data["ufrag"]
pwd: str = ice_data["pwd"]
- candidates: List[dict] = ice_data["candidates"]
except KeyError as e:
raise exceptions.DataError(f"ICE {e} must be provided")
+ candidates: List[dict] = ice_data.get("candidates", [])
candidates.sort(key=lambda c: int(c.get("priority", 0)), reverse=True)
transport_elt = domish.Element(
- (NS_JINGLE_ICE_UDP, "transport"),
- attribs={"ufrag": ufrag, "pwd": pwd}
+ (NS_JINGLE_ICE_UDP, "transport"), attribs={"ufrag": ufrag, "pwd": pwd}
)
for candidate in candidates:
@@ -162,10 +162,7 @@ class XEP_0176(BaseTransportHandler):
@return: ICE data (as in [build_transport])
"""
try:
- ice_data = {
- "ufrag": transport_elt["ufrag"],
- "pwd": transport_elt["pwd"]
- }
+ ice_data = {"ufrag": transport_elt["ufrag"], "pwd": transport_elt["pwd"]}
except KeyError as e:
raise exceptions.DataError(
f" is missing mandatory attribute {e}: {transport_elt.toXml()}"
@@ -256,11 +253,36 @@ class XEP_0176(BaseTransportHandler):
peer_ice_data = self.parse_transport(transport_elt)
transport_data["peer_ice_data"] = peer_ice_data
- elif action in (self._j.A_ACCEPTED_ACK, self._j.A_PREPARE_RESPONDER):
+ elif action == self._j.A_ACCEPTED_ACK:
+ buffer = session.pop("XEP-0176_handler_buffer", None)
+ if buffer:
+ log.debug("replaying buffered events")
+ for args in buffer:
+ await self.jingle_handler(*args)
+ elif action == self._j.A_PREPARE_RESPONDER:
pass
elif action == self._j.A_SESSION_ACCEPT:
- pass
+ # we check if we have any buffered ICE candidates, and send them if it's the
+ # case
+ media_type = content_data["application_data"].get("media")
+ try:
+ buffer = session["XEP-0176_buffer"]
+ buffered_ice_data = buffer.pop(media_type)
+ except KeyError:
+ pass
+ else:
+ if not buffer:
+ del session["XEP-0176_buffer"]
+ transport_elt = self.build_transport(buffered_ice_data)
+ iq_elt, __ = self._j.build_action(
+ client,
+ self._j.A_TRANSPORT_INFO,
+ session,
+ content_name,
+ context_elt=transport_elt,
+ )
+ await iq_elt.send()
elif action == self._j.A_START:
pass
@@ -269,6 +291,13 @@ class XEP_0176(BaseTransportHandler):
# responder side, we give our candidates
transport_elt = self.build_transport(transport_data["local_ice_data"])
elif action == self._j.A_TRANSPORT_INFO:
+ if session["state"] == self._j.STATE_PENDING:
+ # Session is not yet active; we buffer the arguments to replay them
+ # when the session becomes active. This makes the frontend's life easier.
+ log.debug("session is not active yet, buffering transport-info element")
+ buffer = session.setdefault("XEP-0176_handler_buffer", [])
+ buffer.append([client, action, session, content_name, transport_elt])
+ return transport_elt
media_type = content_data["application_data"].get("media")
new_ice_data = self.parse_transport(transport_elt)
@@ -283,10 +312,10 @@ class XEP_0176(BaseTransportHandler):
self.host.bridge.ice_candidates_new(
session["id"],
data_format.serialise({media_type: new_ice_data}),
- client.profile
+ client.profile,
)
elif action == self._j.A_DESTROY:
- pass
+ pass
else:
log.warning("FIXME: unmanaged action {}".format(action))
@@ -303,10 +332,7 @@ class XEP_0176(BaseTransportHandler):
log.debug("ICE-UDP session terminated")
def update_candidates(
- self,
- transport_data: dict,
- new_ice_data: dict,
- local: bool
+ self, transport_data: dict, new_ice_data: dict, local: bool
) -> bool:
"""Update ICE candidates when new one are received
@@ -320,9 +346,7 @@ class XEP_0176(BaseTransportHandler):
try:
ice_data = transport_data[key]
except KeyError:
- log.warning(
- f"no {key} available"
- )
+ log.warning(f"no {key} available")
transport_data[key] = new_ice_data
else:
if (
@@ -336,28 +360,45 @@ class XEP_0176(BaseTransportHandler):
return False
async def ice_candidates_add(
- self,
- client: SatXMPPEntity,
- session_id: str,
- media_ice_data: Dict[str, dict]
+ self, client: SatXMPPEntity, session_id: str, media_ice_data: Dict[str, dict]
) -> None:
"""Called when a new ICE candidates are available for a session
@param session_id: Session ID
- @param candidates: a map from media type (audio, video) to ICE data
+ @param media_ice_data: a map from media type (audio, video) to ICE data
ICE data must be in the same format as in [self.parse_transport]
"""
session = self._j.get_session(client, session_id)
iq_elt: Optional[domish.Element] = None
for media_type, new_ice_data in media_ice_data.items():
+ if session["state"] == self._j.STATE_PENDING:
+ log.debug(f"session not active, buffering")
+ buffer = session.setdefault("XEP-0176_buffer", {})
+ media_buffer = buffer.setdefault(media_type, {})
+
+ for key in ["ufrag", "pwd"]:
+ if key not in media_buffer:
+ media_buffer[key] = new_ice_data[key]
+ else:
+ if media_buffer[key] != new_ice_data[key]:
+ log.warning(
+ f"{key} conflict, new value will replace old one\n"
+ f"buffer={media_buffer[key]!r}\n"
+ f"new={new_ice_data[key]!r}"
+ )
+ media_buffer[key] = new_ice_data[key]
+
+ media_buffer.setdefault("candidates", []).extend(
+ new_ice_data["candidates"]
+ )
+ continue
+
for content_name, content_data in session["contents"].items():
if content_data["application_data"].get("media") == media_type:
break
else:
- log.warning(
- "no media of type {media_type} has been found"
- )
+ log.warning(f"no media of type {media_type} has been found")
continue
restart = self.update_candidates(
content_data["transport_data"], new_ice_data, True
@@ -370,8 +411,12 @@ class XEP_0176(BaseTransportHandler):
self.host.bridge.ice_restart(session["id"], "local", client.profile)
transport_elt = self.build_transport(new_ice_data)
iq_elt, __ = self._j.build_action(
- client, self._j.A_TRANSPORT_INFO, session, content_name, iq_elt=iq_elt,
- transport_elt=transport_elt
+ client,
+ self._j.A_TRANSPORT_INFO,
+ session,
+ content_name,
+ iq_elt=iq_elt,
+ context_elt=transport_elt,
)
if iq_elt is not None:
@@ -380,13 +425,9 @@ class XEP_0176(BaseTransportHandler):
except Exception as e:
log.warning(f"Could not send new ICE candidates: {e}")
- else:
- log.error("Could not find any content to apply new ICE candidates")
-
@implementer(iwokkel.IDisco)
class XEP_0176_handler(XMPPHandler):
-
def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
return [disco.DiscoFeature(NS_JINGLE_ICE_UDP)]