diff libervia/backend/plugins/plugin_xep_0167/__init__.py @ 4240:79c8a70e1813

backend, frontend: prepare remote control: This is a series of changes necessary to prepare the implementation of remote control feature: - XEP-0166: add a `priority` attribute to `ApplicationData`: this is needed when several applications are working in a same session, to know which one must be handled first. Will be used to make Remote Control have precedence over Call content. - XEP-0166: `_call_plugins` is now async and is not used with `DeferredList` anymore: the benefit to have methods called in parallels is very low, and it cause a lot of trouble as we can't predict order. Methods are now called sequentially so workflow can be predicted. - XEP-0167: fix `senders` XMPP attribute <=> SDP mapping - XEP-0234: preflight acceptance key is now `pre-accepted` instead of `file-accepted`, so the same key can be used with other jingle applications. - XEP-0167, XEP-0343: move some method to XEP-0167 - XEP-0353: use new `priority` feature to call preflight methods of applications according to it. - frontend (webrtc): refactor the sources/sink handling with a more flexible mechanism based on Pydantic models. It is now possible to have has many Data Channel as necessary, to have them in addition to A/V streams, to specify manually GStreamer sources and sinks, etc. - frontend (webrtc): rework of the pipeline to reduce latency. - frontend: new `portal_desktop` method. Screenshare portal handling has been moved there, and RemoteDesktop portal has been added. - frontend (webrtc): fix `extract_ufrag_pwd` method. rel 436
author Goffi <goffi@goffi.org>
date Sat, 11 May 2024 13:52:41 +0200
parents e11b13418ba6
children a7d4007a8fa5
line wrap: on
line diff
--- a/libervia/backend/plugins/plugin_xep_0167/__init__.py	Sat May 11 13:25:45 2024 +0200
+++ b/libervia/backend/plugins/plugin_xep_0167/__init__.py	Sat May 11 13:52:41 2024 +0200
@@ -71,9 +71,12 @@
     "unmute",
     "ringing",
 )
+ANSWER_SDP_SENT_KEY = "answer_sdp_sent"
 
 
 class XEP_0167(BaseApplicationHandler):
+    namespace = NS_JINGLE_RTP
+
     def __init__(self, host):
         log.info(f'Plugin "{PLUGIN_INFO[C.PI_NAME]}" initialization')
         self.host = host
@@ -147,7 +150,7 @@
         metadata = call_data.get("metadata") or {}
 
         if "sdp" in call_data:
-            sdp_data = mapping.parse_sdp(call_data["sdp"])
+            sdp_data = mapping.parse_sdp(call_data["sdp"], self._j.ROLE_INITIATOR)
             to_delete = set()
             for media, data in sdp_data.items():
                 if media not in ("audio", "video", "application"):
@@ -186,6 +189,44 @@
 
         return metadata
 
+    def get_contents(self, call_data: dict, metadata: dict) -> list[dict]:
+        """Generate call related contents.
+
+        @param call_data: Call data after being parsed by [parse_call_data]
+        @param metadata: Metadata as returned by [parse_call_data]
+        @return: List of contents to be used with [jingle.initiate].
+
+        """
+        contents = []
+        seen_names = set()
+
+        for media, media_data in call_data.items():
+            if media not in ("audio", "video"):
+                continue
+            content = {
+                "app_ns": NS_JINGLE_RTP,
+                "senders": media_data["senders"],
+                "transport_type": self._j.TRANSPORT_DATAGRAM,
+                "app_kwargs": {"media": media, "media_data": media_data},
+                "transport_data": {
+                    "local_ice_data": {
+                        "ufrag": metadata["ice-ufrag"],
+                        "pwd": metadata["ice-pwd"],
+                        "candidates": media_data.pop("ice-candidates"),
+                        "fingerprint": media_data.pop("fingerprint", {}),
+                    }
+                },
+            }
+            if "id" in media_data:
+                name = media_data.pop("id")
+                if name in seen_names:
+                    raise exceptions.DataError(
+                        f"Content name (mid) seen multiple times: {name}"
+                    )
+                content["name"] = name
+            contents.append(content)
+        return contents
+
     async def call_start(
         self,
         client: SatXMPPEntity,
@@ -211,36 +252,8 @@
         @raises exceptions.DataError: If media data is invalid or duplicate content name
             (mid) is found.
         """
-        sid = str(uuid.uuid4())
         metadata = self.parse_call_data(call_data)
-        contents = []
-        seen_names = set()
-
-        for media, media_data in call_data.items():
-            if media not in ("audio", "video"):
-                continue
-            content = {
-                "app_ns": NS_JINGLE_RTP,
-                "senders": "both",
-                "transport_type": self._j.TRANSPORT_DATAGRAM,
-                "app_kwargs": {"media": media, "media_data": media_data},
-                "transport_data": {
-                    "local_ice_data": {
-                        "ufrag": metadata["ice-ufrag"],
-                        "pwd": metadata["ice-pwd"],
-                        "candidates": media_data.pop("ice-candidates"),
-                        "fingerprint": media_data.pop("fingerprint", {}),
-                    }
-                },
-            }
-            if "id" in media_data:
-                name = media_data.pop("id")
-                if name in seen_names:
-                    raise exceptions.DataError(
-                        f"Content name (mid) seen multiple times: {name}"
-                    )
-                content["name"] = name
-            contents.append(content)
+        contents = self.get_contents(call_data, metadata)
         if not contents:
             raise exceptions.DataError("no valid media data found: {call_data}")
 
@@ -249,17 +262,14 @@
             else C.META_SUBTYPE_CALL_AUDIO
         )
 
-        defer.ensureDeferred(
-            self._j.initiate(
+        sid = await self._j.initiate(
                 client,
                 peer_jid,
                 contents,
-                sid=sid,
                 call_type=call_type,
                 metadata=metadata,
                 peer_metadata={},
             )
-        )
         return sid
 
     def _call_answer_sdp(self, session_id: str, answer_sdp: str, profile: str) -> None:
@@ -338,7 +348,7 @@
         accepted = not resp_data.get("cancelled", False)
 
         if accepted:
-            session["call_accepted"] = True
+            session["pre_accepted"] = True
 
         return accepted
 
@@ -357,7 +367,7 @@
 
         @raises exceptions.CancelError: If the user doesn't accept the incoming call.
         """
-        if session.get("call_accepted", False):
+        if session.get("pre_accepted", False):
             # the call is already accepted, nothing to do
             return
 
@@ -474,7 +484,7 @@
             # is requested only once for audio and video contents.
             return True
 
-        if not session.get("call_accepted", False):
+        if not session.get("pre_accepted", False):
             if any(
                 c["desc_elt"].getAttribute("media") == "video"
                 for c in session["contents"].values()
@@ -505,20 +515,47 @@
 
         answer_sdp = await answer_sdp_d
 
-        parsed_answer = mapping.parse_sdp(answer_sdp)
+        parsed_answer = mapping.parse_sdp(answer_sdp, session["role"])
         session["metadata"].update(parsed_answer["metadata"])
-        for media in ("audio", "video"):
-            for content in session["contents"].values():
-                if content["desc_elt"].getAttribute("media") == media:
-                    media_data = parsed_answer[media]
-                    application_data = content["application_data"]
-                    application_data["local_data"] = media_data["application_data"]
-                    transport_data = content["transport_data"]
-                    local_ice_data = media_data["transport_data"]
-                    transport_data["local_ice_data"] = local_ice_data
+        self.propagate_data(session, parsed_answer)
 
         return True
 
+    def propagate_data(self, session: dict, parsed_answer: dict) -> None:
+        """Propagate local SDP data to other contents"""
+        for media in ("audio", "video", "application"):
+            for content in session["contents"].values():
+                try:
+                    application_data = content["application_data"]
+                    content_media = application_data["media"]
+                except KeyError:
+                    pass
+                else:
+                    if content_media == media:
+                        media_data = parsed_answer[media]
+                        application_data["local_data"] = media_data["application_data"]
+                        transport_data = content["transport_data"]
+                        local_ice_data = media_data["transport_data"]
+                        transport_data["local_ice_data"] = local_ice_data
+
+    def send_answer_sdp(self, client: SatXMPPEntity, session: dict) -> None:
+        """Send answer SDP to frontend"""
+        if not session.get(ANSWER_SDP_SENT_KEY, False):
+            # we only send the signal once, as it means that the whole session is
+            # accepted
+            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,
+            )
+            session[ANSWER_SDP_SENT_KEY] = True
+
     async def jingle_handler(self, client, action, session, content_name, desc_elt):
         content_data = session["contents"][content_name]
         application_data = content_data["application_data"]
@@ -542,20 +579,7 @@
         elif action == self._j.A_PREPARE_INITIATOR:
             application_data["peer_data"] = mapping.parse_description(desc_elt)
         elif action == self._j.A_SESSION_ACCEPT:
-            if content_name == next(iter(session["contents"])):
-                # we only send the signal for first content, as it means that the whole
-                # session is accepted
-                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,
-                )
+            pass # self.send_answer_sdp(client, session)
         else:
             log.warning(f"FIXME: unmanaged action {action}")