diff libervia/backend/plugins/plugin_xep_0167/__init__.py @ 4231:e11b13418ba6

plugin XEP-0353, XEP-0234, jingle: WebRTC data channel signaling implementation: Implement XEP-0343: Signaling WebRTC Data Channels in Jingle. The current version of the XEP (0.3.1) has no implementation and contains some flaws. After discussing this on xsf@, Daniel (from Conversations) mentioned that they had a sprint with Larma (from Dino) to work on another version and provided me with this link: https://gist.github.com/iNPUTmice/6c56f3e948cca517c5fb129016d99e74 . I have used it for my implementation. This implementation reuses work done on Jingle A/V call (notably XEP-0176 and XEP-0167 plugins), with adaptations. When used, XEP-0234 will not handle the file itself as it normally does. This is because WebRTC has several implementations (browser for web interface, GStreamer for others), and file/data must be handled directly by the frontend. This is particularly important for web frontends, as the file is not sent from the backend but from the end-user's browser device. Among the changes, there are: - XEP-0343 implementation. - `file_send` bridge method now use serialised dict as output. - New `BaseTransportHandler.is_usable` method which get content data and returns a boolean (default to `True`) to tell if this transport can actually be used in this context (when we are initiator). Used in webRTC case to see if call data are available. - Support of `application` media type, and everything necessary to handle data channels. - Better confirmation message, with file name, size and description when available. - When file is accepted in preflight, it is specified in following `action_new` signal for actual file transfer. This way, frontend can avoid the display or 2 confirmation messages. - XEP-0166: when not specified, default `content` name is now its index number instead of a UUID. This follows the behaviour of browsers. - XEP-0353: better handling of events such as call taken by another device. - various other updates. rel 441
author Goffi <goffi@goffi.org>
date Sat, 06 Apr 2024 12:57:23 +0200
parents 832a7bdb3aea
children 79c8a70e1813
line wrap: on
line diff
--- a/libervia/backend/plugins/plugin_xep_0167/__init__.py	Sat Apr 06 12:21:04 2024 +0200
+++ b/libervia/backend/plugins/plugin_xep_0167/__init__.py	Sat Apr 06 12:57:23 2024 +0200
@@ -81,6 +81,7 @@
         mapping.host = host
         self._j = host.plugins["XEP-0166"]
         self._j.register_application(NS_JINGLE_RTP, self)
+        host.register_namespace("jingle-rtp", NS_JINGLE_RTP)
         host.bridge.add_method(
             "call_start",
             ".plugin",
@@ -141,6 +142,50 @@
             )
         )
 
+    def parse_call_data(self, call_data: dict) -> dict:
+        """Parse ``call_data`` and return corresponding contents end metadata"""
+        metadata = call_data.get("metadata") or {}
+
+        if "sdp" in call_data:
+            sdp_data = mapping.parse_sdp(call_data["sdp"])
+            to_delete = set()
+            for media, data in sdp_data.items():
+                if media not in ("audio", "video", "application"):
+                    continue
+                to_delete.add(media)
+                media_type, media_data = media, data
+                call_data[media_type] = media_data["application_data"]
+                transport_data = media_data["transport_data"]
+                try:
+                    call_data[media_type]["fingerprint"] = transport_data["fingerprint"]
+                except KeyError:
+                    log.warning("fingerprint is missing")
+                    pass
+                try:
+                    call_data[media_type]["id"] = media_data["id"]
+                except KeyError:
+                    log.warning(f"no media ID found for {media_type}: {media_data}")
+                # FIXME: the 2 values below are linked to XEP-0343, they should be added
+                #   there instead, maybe with some new trigger?
+                for key in ("sctp-port","max-message-size"):
+                    value = transport_data.get(key)
+                    if value is not None:
+                        metadata[key] = value
+                try:
+                    call_data[media_type]["ice-candidates"] = transport_data.get(
+                        "candidates", []
+                    )
+                    metadata["ice-ufrag"] = transport_data["ufrag"]
+                    metadata["ice-pwd"] = transport_data["pwd"]
+                except KeyError:
+                    log.warning("ICE data are missing from SDP")
+                    continue
+            for media in to_delete:
+                del sdp_data[media]
+            metadata.update(sdp_data.get("metadata", {}))
+
+        return metadata
+
     async def call_start(
         self,
         client: SatXMPPEntity,
@@ -166,46 +211,9 @@
         @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 = []
-        metadata = call_data.get("metadata") or {}
-
-        if "sdp" in call_data:
-            sdp_data = mapping.parse_sdp(call_data["sdp"])
-            to_delete = set()
-            for media, data in sdp_data.items():
-                if media not in ("audio", "video"):
-                    continue
-                to_delete.add(media)
-                media_type, media_data = media, data
-                call_data[media_type] = media_data["application_data"]
-                transport_data = media_data["transport_data"]
-                try:
-                    call_data[media_type]["fingerprint"] = transport_data["fingerprint"]
-                except KeyError:
-                    log.warning("fingerprint is missing")
-                    pass
-                try:
-                    call_data[media_type]["id"] = media_data["id"]
-                except KeyError:
-                    log.warning(f"no media ID found for {media_type}: {media_data}")
-                try:
-                    call_data[media_type]["ice-candidates"] = transport_data.get(
-                        "candidates", []
-                    )
-                    metadata["ice-ufrag"] = transport_data["ufrag"]
-                    metadata["ice-pwd"] = transport_data["pwd"]
-                except KeyError:
-                    log.warning("ICE data are missing from SDP")
-                    continue
-            for media in to_delete:
-                del sdp_data[media]
-            metadata.update(sdp_data.get("metadata", {}))
-
-        call_type = (
-            C.META_SUBTYPE_CALL_VIDEO
-            if "video" in call_data
-            else C.META_SUBTYPE_CALL_AUDIO
-        )
         seen_names = set()
 
         for media, media_data in call_data.items():
@@ -235,7 +243,12 @@
             contents.append(content)
         if not contents:
             raise exceptions.DataError("no valid media data found: {call_data}")
-        sid = str(uuid.uuid4())
+
+        call_type = (
+            C.META_SUBTYPE_CALL_VIDEO if "video" in call_data
+            else C.META_SUBTYPE_CALL_AUDIO
+        )
+
         defer.ensureDeferred(
             self._j.initiate(
                 client,
@@ -295,7 +308,7 @@
 
         @param client: The client entity.
         @param session: The Jingle session.
-        @param media_type: Type of media (audio or video).
+        @param call_type: Type of media (audio or video).
 
         @return: True if the call has been accepted
         """
@@ -393,15 +406,13 @@
         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
-        # contents
+        # call_ended is used to send the signal only once even if there are audio and
+        # video contents
         call_ended = session.get("call_ended", False)
         if call_ended:
             return
-        data = {"reason": getattr(cancel_error, "reason", "cancelled")}
-        text = getattr(cancel_error, "text", None)
-        if text:
-            data["text"] = text
+        data = {"reason": getattr(cancel_error, "reason", None) or "cancelled"}
+        data["text"] = str(cancel_error)
         self.host.bridge.call_ended(
             session["id"], data_format.serialise(data), client.profile
         )