Mercurial > libervia-backend
comparison libervia/cli/call_simple.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 | |
children | d01b8d002619 |
comparison
equal
deleted
inserted
replaced
4209:fe29fbdabce6 | 4210:9218d4331bb2 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # Libervia CLI | |
4 # Copyright (C) 2009-2024 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 import asyncio | |
20 from functools import partial | |
21 import os | |
22 | |
23 from prompt_toolkit.input import create_input | |
24 from prompt_toolkit.keys import Keys | |
25 from rich.align import Align | |
26 from rich.columns import Columns | |
27 from rich.console import group | |
28 from rich.live import Live | |
29 from rich.panel import Panel | |
30 from rich.text import Text | |
31 | |
32 from .call_webrtc import CallData, WebRTCCall | |
33 | |
34 | |
35 class BaseAVTUI: | |
36 def __init__(self, host, webrtc=None, align: str = "left"): | |
37 self.host = host | |
38 self.webrtc = webrtc | |
39 self.align = align | |
40 self.input = create_input() | |
41 self.done = asyncio.Event() | |
42 self.target_size: tuple[int, int] | None = None | |
43 | |
44 @staticmethod | |
45 def parse_output_opts(parent) -> dict: | |
46 """Parse output options. | |
47 | |
48 This method should be called in a loop checking all output options. | |
49 It will set the relevant attributes. | |
50 @return: keyword argument to use to instanciate WebRTCCall | |
51 """ | |
52 kwargs = {} | |
53 for oo in parent.args.output_opts: | |
54 if oo.startswith("size="): | |
55 try: | |
56 width_s, height_s = oo[5:].lower().strip().split("x", 1) | |
57 width, height = int(width_s), int(height_s) | |
58 except ValueError: | |
59 parent.parser.error( | |
60 "Invalid size; it must be in the form widthxheight " | |
61 "(e.g., 640x360)." | |
62 ) | |
63 else: | |
64 kwargs["target_size"] = (width, height) | |
65 return kwargs | |
66 | |
67 def styled_shortcut_key(self, word: str, key: str | None = None) -> Text: | |
68 """Return a word with the specified key or the first letter underlined.""" | |
69 if key is None: | |
70 key = word[0] | |
71 index = word.find(key) | |
72 before, keyword, after = word[:index], word[index], word[index + 1 :] | |
73 return Text(before) + Text(keyword, style="shortcut") + Text(after) | |
74 | |
75 def get_micro_display(self): | |
76 assert self.webrtc is not None | |
77 if self.webrtc.audio_muted: | |
78 return Panel(Text("🔇 ") + self.styled_shortcut_key("Muted"), expand=False) | |
79 else: | |
80 return Panel( | |
81 Text("🎤 ") + self.styled_shortcut_key("Unmuted", "m"), expand=False | |
82 ) | |
83 | |
84 def get_video_display(self): | |
85 assert self.webrtc is not None | |
86 if self.webrtc.video_muted: | |
87 return Panel(Text("❌ ") + self.styled_shortcut_key("Video Off"), expand=False) | |
88 else: | |
89 return Panel(Text("🎥 ") + self.styled_shortcut_key("Video On"), expand=False) | |
90 | |
91 def get_phone_display(self): | |
92 return Panel(Text("📞 ") + self.styled_shortcut_key("Hang up"), expand=False) | |
93 | |
94 @group() | |
95 def generate_control_bar(self): | |
96 """Return the full interface display.""" | |
97 assert self.webrtc is not None | |
98 yield Align( | |
99 Columns( | |
100 [ | |
101 Text(), | |
102 self.get_micro_display(), | |
103 self.get_video_display(), | |
104 self.get_phone_display(), | |
105 ], | |
106 expand=False, | |
107 title="Calling [bold center]{}[/]".format(self.webrtc.callee), | |
108 ), | |
109 align=self.align, | |
110 ) | |
111 | |
112 def keys_ready(self, live=None): | |
113 assert self.webrtc is not None | |
114 for key_press in self.input.read_keys(): | |
115 char = key_press.key.lower() | |
116 if char == "m": | |
117 # audio mute | |
118 self.webrtc.audio_muted = not self.webrtc.audio_muted | |
119 if live is not None: | |
120 live.update(self.generate_control_bar(), refresh=True) | |
121 elif char == "v": | |
122 # video mute | |
123 self.webrtc.video_muted = not self.webrtc.video_muted | |
124 if live is not None: | |
125 live.update(self.generate_control_bar(), refresh=True) | |
126 elif char == "h" or key_press.key == Keys.ControlC: | |
127 # Hang up | |
128 self.done.set() | |
129 elif char == "d": | |
130 # generate dot file for debugging. Only available if | |
131 # ``GST_DEBUG_DUMP_DOT_DIR`` is set. Filename is "pipeline.dot" with a | |
132 # timestamp. | |
133 if os.getenv("GST_DEBUG_DUMP_DOT_DIR"): | |
134 self.webrtc.generate_dot_file() | |
135 self.host.disp("Dot file generated.") | |
136 | |
137 | |
138 class AVCallUI(BaseAVTUI): | |
139 def __init__(self, host, webrtc): | |
140 super().__init__(host, webrtc) | |
141 | |
142 async def start(self): | |
143 assert self.webrtc is not None | |
144 with Live( | |
145 self.generate_control_bar(), console=self.host.console, auto_refresh=False | |
146 ) as live: | |
147 with self.input.raw_mode(): | |
148 with self.input.attach(partial(self.keys_ready, live)): | |
149 await self.done.wait() | |
150 | |
151 await self.webrtc.end_call() | |
152 await self.host.a_quit() | |
153 | |
154 @classmethod | |
155 async def run(cls, parent, call_data: CallData) -> None: | |
156 kwargs = cls.parse_output_opts(parent) | |
157 merge_pip = False if "split" in parent.args.output_opts else None | |
158 | |
159 webrtc_call = await WebRTCCall.make_webrtc_call( | |
160 parent.host, | |
161 parent.profile, | |
162 call_data, | |
163 merge_pip=merge_pip, | |
164 **kwargs, | |
165 ) | |
166 if not parent.args.no_ui: | |
167 ui = cls(parent.host, webrtc_call.webrtc) | |
168 await ui.start() |