plugin XEP-0167, XEP-0176: transport-info and ICE candidate sending are delayed if session is not active yet
This commit is contained in:
parent
09d88cdfe2
commit
d60b79b18f
4 changed files with 125 additions and 108 deletions
|
|
@ -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 <iq/> 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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <transport> 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"<transport> 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)]
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue