diff libervia/web/pages/calls/_browser/__init__.py @ 1602:6feac4a25e60

browser: Remote Control implementation: - Add `cbor-x` JS dependency. - In "Call" page, a Remote Control session can now be started. This is done by clicking on a search item 3 dots menu. Libervia Web will act as a controlling device. The call box is then adapted, and mouse/wheel and keyboard events are sent to remote, touch events are converted to mouse one. - Some Brython 3.12* related changes. rel 436
author Goffi <goffi@goffi.org>
date Sat, 11 May 2024 14:02:22 +0200
parents 0a4433a343a3
children 4a9679369856
line wrap: on
line diff
--- a/libervia/web/pages/calls/_browser/__init__.py	Sat May 11 13:57:49 2024 +0200
+++ b/libervia/web/pages/calls/_browser/__init__.py	Sat May 11 14:02:22 2024 +0200
@@ -4,7 +4,7 @@
 from browser import aio, console as log, document, window
 from cache import cache
 import dialog
-from javascript import JSObject
+from javascript import JSObject, NULL
 from jid import JID
 from jid_search import JidSearch
 import loading
@@ -27,7 +27,8 @@
 )
 AUDIO = "audio"
 VIDEO = "video"
-ALLOWED_CALL_MODES = {AUDIO, VIDEO}
+REMOTE = "remote-control"
+ALLOWED_CALL_MODES = {AUDIO, VIDEO, REMOTE}
 INACTIVE_CLASS = "inactive"
 MUTED_CLASS = "muted"
 SCREEN_OFF_CLASS = "screen-off"
@@ -107,6 +108,9 @@
                     ".click-to-audio": lambda evt, item: aio.run(
                         self.on_entity_action(evt, AUDIO, item)
                     ),
+                    ".click-to-remote-control": lambda evt, item: aio.run(
+                        self.on_entity_action(evt, REMOTE, item)
+                    ),
                 },
             },
         )
@@ -155,13 +159,28 @@
         if mode in ALLOWED_CALL_MODES:
             if self._call_mode == mode:
                 return
+            log.debug("Switching to {mode} call mode.")
             self._call_mode = mode
-            with_video = mode == VIDEO
-            for elt in self.call_box_elt.select(".is-video-only"):
-                if with_video:
+            selector = ".is-video-only, .is-not-remote"
+            for elt in self.call_box_elt.select(selector):
+                if mode == VIDEO:
+                    # In video, all elements are visible.
                     elt.classList.remove("is-hidden")
+                elif mode == AUDIO:
+                    # In audio, we hide video-only elements.
+                    if elt.classList.contains("is-video-only"):
+                        elt.classList.add("is-hidden")
+                    else:
+                        elt.classList.remove("is-hidden")
+                elif mode == REMOTE:
+                    # In remote, we show all video element, except if they are
+                    # `is-not-remote`
+                    if elt.classList.contains("is-not-remote"):
+                        elt.classList.add("is-hidden")
+                    else:
+                        elt.classList.remove("is-hidden")
                 else:
-                    elt.classList.add("is-hidden")
+                    raise Exception("This line should never be reached.")
         else:
             raise ValueError("Invalid call mode")
 
@@ -190,7 +209,10 @@
         @param profile: Profile associated with the action
         """
         action_data = json.loads(action_data_s)
-        if action_data.get("type") == "confirm" and action_data.get("subtype") == "file":
+        if (
+            action_data.get("type") in ("confirm", "not_in_roster_leak")
+            and action_data.get("subtype") == "file"
+        ):
             aio.run(self.on_file_preflight(action_data, action_id))
         elif action_data.get("type") == "file":
             aio.run(self.on_file_proposal(action_data, action_id))
@@ -286,7 +308,7 @@
             # TODO: Check if any other frontend is connected for this profile, and refuse
             # the file if none is.
             return
-        if action_data.get("file_accepted", False):
+        if action_data.get("pre_accepted", False):
             # File proposal has already been accepted in preflight.
             accepted = True
         else:
@@ -472,13 +494,24 @@
             btn_elt.classList.remove(INACTIVE_CLASS, MUTED_CLASS, "is-warning")
             btn_elt.classList.add("is-success")
 
-    async def make_call(self, audio: bool = True, video: bool = True) -> None:
+    async def make_call(
+        self,
+        audio: bool = True,
+        video: bool = True,
+        remote: bool = False
+    ) -> None:
         """Start a WebRTC call
 
         @param audio: True if an audio flux is required
         @param video: True if a video flux is required
+        @param remote: True if this is a Remote Control session.
         """
-        self.call_mode = VIDEO if video else AUDIO
+        if remote:
+            self.call_mode = REMOTE
+        elif video:
+            self.call_mode = VIDEO
+        else:
+            self.call_mode = AUDIO
         try:
             callee_jid = JID(self.search_elt.value.strip())
             if not callee_jid.is_valid:
@@ -495,7 +528,12 @@
         self.set_avatar(callee_jid)
 
         self.switch_mode("call")
-        await self.webrtc.make_call(callee_jid, audio, video)
+        if remote:
+            await self.webrtc.start_remote_control(
+                callee_jid, audio, video
+            )
+        else:
+            await self.webrtc.make_call(callee_jid, audio, video)
 
     async def end_call(self, data: dict) -> None:
         """Stop streaming and clean instance"""
@@ -612,18 +650,17 @@
         @param fullscreen: if set, determine the fullscreen state; otherwise,
             the fullscreen mode will be toggled.
         """
-        do_fullscreen = (
-            document.fullscreenElement is None if fullscreen is None else fullscreen
-        )
+        if fullscreen is None:
+            fullscreen = document.fullscreenElement is NULL
 
         try:
-            if do_fullscreen:
-                if document.fullscreenElement is None:
+            if fullscreen:
+                if document.fullscreenElement is NULL:
                     self.call_box_elt.requestFullscreen()
                     document["full_screen_btn"].classList.add("is-hidden")
                     document["exit_full_screen_btn"].classList.remove("is-hidden")
             else:
-                if document.fullscreenElement is not None:
+                if document.fullscreenElement is not NULL:
                     document.exitFullscreen()
                     document["full_screen_btn"].classList.remove("is-hidden")
                     document["exit_full_screen_btn"].classList.add("is-hidden")
@@ -711,11 +748,15 @@
         evt.stopPropagation()
         if action == "menu":
             evt.currentTarget.parent.classList.toggle("is-active")
-        elif action in (VIDEO, AUDIO):
+        elif action in (VIDEO, AUDIO, REMOTE):
             self.search_elt.value = item["entity"]
             # we want the dropdown to be inactive
             evt.currentTarget.closest(".dropdown").classList.remove("is-active")
-            await self.make_call(video=action == VIDEO)
+            if action == REMOTE:
+                await self.make_call(audio=False, video=True, remote=True)
+
+            else:
+                await self.make_call(video=action == VIDEO)
 
 
 CallUI()