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()