Mercurial > libervia-backend
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):