diff libervia/cli/cmd_call.py @ 4210:9218d4331bb2

cli (call): `tui` output implementation: - Moved original UI to a separated class, and use if with the `simple` output - By default, best output is automatically selected. For now `gui` is selected if possible, and `simple` is used as fallback. - The new `tui` output can be used to have the videos directly embedded in the terminal, either with real videos for compatible terminal emulators, or with Unicode blocks. - Text contrôls are used for both `simple` and `tui` outputs - several options can be used with `--oo` (will be documented in next commit). rel 428
author Goffi <goffi@goffi.org>
date Fri, 16 Feb 2024 18:46:06 +0100
parents 0f8ea0768a3b
children d01b8d002619
line wrap: on
line diff
--- a/libervia/cli/cmd_call.py	Fri Feb 16 18:46:02 2024 +0100
+++ b/libervia/cli/cmd_call.py	Fri Feb 16 18:46:06 2024 +0100
@@ -18,199 +18,22 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
-from argparse import ArgumentParser
-import asyncio
-from dataclasses import dataclass
 from functools import partial
+import importlib
 import logging
-import os
-from pathlib import Path
-from typing import Callable
 
-from prompt_toolkit.input import create_input
-from prompt_toolkit.keys import Keys
 
 from libervia.backend.core.i18n import _
 from libervia.backend.tools.common import data_format
 from libervia.cli.constants import Const as C
-from libervia.frontends.tools import aio, jid
-from rich.columns import Columns
-from rich.console import group
-from rich.live import Live
-from rich.panel import Panel
-from rich.text import Text
+from libervia.frontends.tools import jid
 
 from . import base
+from .call_webrtc import CallData, WebRTCCall
 
 __commands__ = ["Call"]
 
 
-@dataclass
-class CallData:
-    callee: jid.JID
-    sid: str|None = None
-    action_id: str|None = None
-
-
-class WebRTCCall:
-    def __init__(self, host, profile: str, callee: jid.JID, **kwargs):
-        """Create and setup a webRTC instance
-
-        @param profile: profile making or receiving the call
-        @param callee: peer jid
-        @param kwargs: extra kw args to use when instantiating WebRTC
-        """
-        from libervia.frontends.tools import webrtc
-
-        aio.install_glib_asyncio_iteration()
-        self.host = host
-        self.profile = profile
-        self.webrtc = webrtc.WebRTC(host.bridge, profile, **kwargs)
-        self.webrtc.callee = callee
-        host.bridge.register_signal(
-            "ice_candidates_new", self.on_ice_candidates_new, "plugin"
-        )
-        host.bridge.register_signal("call_setup", self.on_call_setup, "plugin")
-        host.bridge.register_signal("call_ended", self.on_call_ended, "plugin")
-
-    @property
-    def sid(self) -> str | None:
-        return self.webrtc.sid
-
-    @sid.setter
-    def sid(self, new_sid: str | None) -> None:
-        self.webrtc.sid = new_sid
-
-    async def on_ice_candidates_new(
-        self, sid: str, candidates_s: str, profile: str
-    ) -> None:
-        if sid != self.webrtc.sid or profile != self.profile:
-            return
-        self.webrtc.on_ice_candidates_new(
-            data_format.deserialise(candidates_s),
-        )
-
-    async def on_call_setup(self, sid: str, setup_data_s: str, profile: str) -> None:
-        if sid != self.webrtc.sid or profile != self.profile:
-            return
-        setup_data = data_format.deserialise(setup_data_s)
-        try:
-            role = setup_data["role"]
-            sdp = setup_data["sdp"]
-        except KeyError:
-            self.host.disp(f"Invalid setup data received: {setup_data}", error=True)
-            return
-        if role == "initiator":
-            self.webrtc.on_accepted_call(sdp, profile)
-        elif role == "responder":
-            await self.webrtc.answer_call(sdp, profile)
-        else:
-            self.host.disp(
-                f"Invalid role received during setup: {setup_data}", error=True
-            )
-        # we want to be sure that call is ended if user presses `Ctrl + c` or anything
-        # else stops the session.
-        self.host.add_on_quit_callback(
-            lambda: self.host.bridge.call_end(sid, "", profile)
-        )
-
-    async def on_call_ended(self, sid: str, data_s: str, profile: str) -> None:
-        if sid != self.webrtc.sid or profile != self.profile:
-            return
-        await self.webrtc.end_call()
-        await self.host.a_quit()
-
-    async def start(self):
-        """Start a call.
-
-        To be used only if we are initiator
-        """
-        await self.webrtc.setup_call("initiator")
-        self.webrtc.start_pipeline()
-
-
-class UI:
-    def __init__(self, host, webrtc):
-        self.host = host
-        self.webrtc = webrtc
-
-    def styled_shortcut_key(self, word: str, key: str | None = None) -> Text:
-        """Return a word with the specified key or the first letter underlined."""
-        if key is None:
-            key = word[0]
-        index = word.find(key)
-        before, keyword, after = word[:index], word[index], word[index + 1 :]
-        return Text(before) + Text(keyword, style="shortcut") + Text(after)
-
-    def get_micro_display(self):
-        if self.webrtc.audio_muted:
-            return Panel(Text("🔇 ") + self.styled_shortcut_key("Muted"), expand=False)
-        else:
-            return Panel(
-                Text("🎤 ") + self.styled_shortcut_key("Unmuted", "m"), expand=False
-            )
-
-    def get_video_display(self):
-        if self.webrtc.video_muted:
-            return Panel(Text("❌ ") + self.styled_shortcut_key("Video Off"), expand=False)
-        else:
-            return Panel(Text("🎥 ") + self.styled_shortcut_key("Video On"), expand=False)
-
-    def get_phone_display(self):
-        return Panel(Text("📞 ") + self.styled_shortcut_key("Hang up"), expand=False)
-
-    @group()
-    def generate_control_bar(self):
-        """Return the full interface display."""
-        yield Columns(
-            [
-                self.get_micro_display(),
-                self.get_video_display(),
-                self.get_phone_display(),
-            ],
-            expand=False,
-            title="Calling [bold center]{}[/]".format(self.webrtc.callee),
-        )
-
-    async def start(self):
-        done = asyncio.Event()
-        input = create_input()
-
-        def keys_ready(live):
-            for key_press in input.read_keys():
-                char = key_press.key.lower()
-                if char == "m":
-                    # audio mute
-                    self.webrtc.audio_muted = not self.webrtc.audio_muted
-                    live.update(self.generate_control_bar(), refresh=True)
-                elif char == "v":
-                    # video mute
-                    self.webrtc.video_muted = not self.webrtc.video_muted
-                    live.update(self.generate_control_bar(), refresh=True)
-                elif char == "h" or key_press.key == Keys.ControlC:
-                    # Hang up
-                    done.set()
-                elif char == "d":
-                    # generate dot file for debugging. Only available if
-                    # ``GST_DEBUG_DUMP_DOT_DIR`` is set. Filename is "pipeline.dot" with a
-                    # timestamp.
-                    if os.getenv("GST_DEBUG_DUMP_DOT_DIR"):
-                        self.webrtc.generate_dot_file()
-                        self.host.disp("Dot file generated.")
-
-        with Live(
-            self.generate_control_bar(),
-            console=self.host.console,
-            auto_refresh=False
-        ) as live:
-            with input.raw_mode():
-                with input.attach(partial(keys_ready, live)):
-                    await done.wait()
-
-        await self.webrtc.end_call()
-        await self.host.a_quit()
-
-
 class Common(base.CommandBase):
 
 
@@ -219,9 +42,14 @@
             *args,
             use_output=C.OUTPUT_CUSTOM,
             extra_outputs={
+                #: automatically select best output for current platform
                 "default": self.auto_output,
-                "simple": self.simple_output,
-                "gui": self.gui_output,
+                #: simple output with GStreamer ``autovideosink``
+                "simple": partial(self.use_output, "simple"),
+                #: Qt GUI
+                "gui": partial(self.use_output, "gui"),
+                #: experimental TUI output
+                "tui": partial(self.use_output, "tui"),
             },
             **kwargs
         )
@@ -243,50 +71,32 @@
         if self.verbosity >= 2:
             root_logger.setLevel(logging.DEBUG)
 
-    async def make_webrtc_call(self, call_data: CallData, **kwargs) -> WebRTCCall:
-        """Create the webrtc_call instance
-
-        @param call_data: Call data of the command
-        @param kwargs: extra args used to instanciate WebRTCCall
-
-        """
-        webrtc_call = WebRTCCall(self.host, self.profile, call_data.callee, **kwargs)
-        if call_data.sid is None:
-            # we are making the call
-            await webrtc_call.start()
+    async def auto_output(self, call_data: CallData) -> None:
+        """Make a guess on the best output to use on current platform"""
+        try:
+            from .call_gui import AVCallUI
+        except ImportError:
+            # we can't import GUI, we may have missing modules
+            await self.use_output("simple", call_data)
         else:
-            # we are receiving the call
-            webrtc_call.sid = call_data.sid
-            if call_data.action_id is not None:
-                await self.host.bridge.action_launch(
-                    call_data.action_id,
-                    data_format.serialise({"cancelled": False}),
-                    self.profile
-                )
-        return webrtc_call
+            if AVCallUI.can_run():
+                await self.use_output("gui", call_data)
+            else:
+                await self.use_output("simple", call_data)
 
-    async def auto_output(self, call_data: CallData):
-        """Make a guess on the best output to use on current platform"""
-        # For now we just use simple output
-        await self.simple_output(call_data)
-
-    async def simple_output(self, call_data: CallData):
-        """Run simple output, with GStreamer ``autovideosink``"""
-        webrtc_call = await self.make_webrtc_call(call_data)
-        if not self.args.no_ui:
-            ui = UI(self.host, webrtc_call.webrtc)
-            await ui.start()
-
-    async def gui_output(self, call_data: CallData):
-        """Run GUI output"""
-        media_dir = Path(await self.host.bridge.config_get("", "media_dir"))
-        icons_path = media_dir / "fonts/fontello/svg"
+    async def use_output(self, output_type, call_data: CallData):
         try:
-            from .call_gui import AVCallGUI
-            await AVCallGUI.run(self, call_data, icons_path)
+            AVCall_module = importlib.import_module(f"libervia.cli.call_{output_type}")
+            AVCallUI = AVCall_module.AVCallUI
         except Exception as e:
-            self.disp(f"Error starting GUI: {e}", error=True)
+            self.disp(f"Error starting {output_type.upper()} UI: {e}", error=True)
             self.host.quit(C.EXIT_ERROR)
+        else:
+            try:
+                await AVCallUI.run(self, call_data)
+            except Exception as e:
+                self.disp(f"Error running {output_type.upper()} UI: {e}", error=True)
+                self.host.quit(C.EXIT_ERROR)
 
 
 class Make(Common):