Mercurial > libervia-backend
comparison libervia/frontends/tools/webrtc_screenshare.py @ 4233:d01b8d002619
cli (call, file), frontends: implement webRTC data channel transfer:
- file send/receive commands now supports webRTC transfer. In `send` command, the
`--webrtc` flags is currenty used to activate it.
- WebRTC related code have been factorized and moved to `libervia.frontends.tools.webrtc*`
modules.
rel 442
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 06 Apr 2024 13:43:09 +0200 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
4232:0fbe5c605eb6 | 4233:d01b8d002619 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # Libervia WebRTC implementation | |
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 from libervia.backend.core import exceptions | |
20 | |
21 import asyncio | |
22 import logging | |
23 from random import randint | |
24 | |
25 | |
26 log = logging.getLogger(__name__) | |
27 | |
28 | |
29 SOURCES_AUTO = "auto" | |
30 SOURCES_TEST = "test" | |
31 SOURCES_DATACHANNEL = "datachannel" | |
32 SINKS_APP = "app" | |
33 SINKS_AUTO = "auto" | |
34 SINKS_TEST = "test" | |
35 SINKS_DATACHANNEL = "datachannel" | |
36 | |
37 | |
38 class ScreenshareError(Exception): | |
39 pass | |
40 | |
41 | |
42 class DesktopPortal: | |
43 | |
44 def __init__(self, webrtc): | |
45 import dbus | |
46 from dbus.mainloop.glib import DBusGMainLoop | |
47 # we want monitors + windows, see https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html#org-freedesktop-portal-screencast-availablesourcetypes | |
48 self.dbus = dbus | |
49 self.webrtc = webrtc | |
50 self.sources_type = dbus.UInt32(7) | |
51 DBusGMainLoop(set_as_default=True) | |
52 self.session_bus = dbus.SessionBus() | |
53 portal_object = self.session_bus.get_object( | |
54 'org.freedesktop.portal.Desktop', | |
55 '/org/freedesktop/portal/desktop' | |
56 ) | |
57 self.screencast_interface = dbus.Interface( | |
58 portal_object, | |
59 'org.freedesktop.portal.ScreenCast' | |
60 ) | |
61 self.session_interface = None | |
62 self.session_signal = None | |
63 self.handle_counter = 0 | |
64 self.session_handle = None | |
65 self.stream_data: dict|None = None | |
66 | |
67 @property | |
68 def handle_token(self): | |
69 self.handle_counter += 1 | |
70 return f"libervia{self.handle_counter}" | |
71 | |
72 def on_session_closed(self, details: dict) -> None: | |
73 if self.session_interface is not None: | |
74 self.session_interface = None | |
75 self.webrtc.desktop_sharing = False | |
76 if self.session_signal is not None: | |
77 self.session_signal.remove() | |
78 self.session_signal = None | |
79 | |
80 | |
81 async def dbus_call(self, method_name: str, *args) -> dict: | |
82 """Call a screenshare portal method | |
83 | |
84 This method handle the signal response. | |
85 @param method_name: method to call | |
86 @param args: extra args | |
87 `handle_token` will be automatically added to the last arg (option dict) | |
88 @return: method result | |
89 """ | |
90 if self.session_handle is not None: | |
91 self.end_screenshare() | |
92 method = getattr(self.screencast_interface, method_name) | |
93 options = args[-1] | |
94 reply_fut = asyncio.Future() | |
95 signal_fut = asyncio.Future() | |
96 # cf. https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html | |
97 handle_token = self.handle_token | |
98 sender = self.session_bus.get_unique_name().replace(".", "_")[1:] | |
99 path = f"/org/freedesktop/portal/desktop/request/{sender}/{handle_token}" | |
100 signal_match = None | |
101 | |
102 def on_signal(response, results): | |
103 assert signal_match is not None | |
104 signal_match.remove() | |
105 if response == 0: | |
106 signal_fut.set_result(results) | |
107 elif response == 1: | |
108 signal_fut.set_exception( | |
109 exceptions.CancelError("Cancelled by user.") | |
110 ) | |
111 else: | |
112 signal_fut.set_exception(ScreenshareError( | |
113 f"Can't get signal result" | |
114 )) | |
115 | |
116 signal_match = self.session_bus.add_signal_receiver( | |
117 on_signal, | |
118 signal_name="Response", | |
119 dbus_interface="org.freedesktop.portal.Request", | |
120 path=path | |
121 ) | |
122 | |
123 options["handle_token"] = handle_token | |
124 | |
125 method( | |
126 *args, | |
127 reply_handler=reply_fut.set_result, | |
128 error_handler=reply_fut.set_exception | |
129 ) | |
130 try: | |
131 await reply_fut | |
132 except Exception as e: | |
133 raise ScreenshareError(f"Can't ask screenshare permission: {e}") | |
134 return await signal_fut | |
135 | |
136 async def request_screenshare(self) -> dict: | |
137 session_data = await self.dbus_call( | |
138 "CreateSession", | |
139 { | |
140 "session_handle_token": str(randint(1, 2**32)), | |
141 } | |
142 ) | |
143 try: | |
144 session_handle = session_data["session_handle"] | |
145 except KeyError: | |
146 raise ScreenshareError("Can't get session handle") | |
147 self.session_handle = session_handle | |
148 | |
149 | |
150 await self.dbus_call( | |
151 "SelectSources", | |
152 session_handle, | |
153 { | |
154 "multiple": True, | |
155 "types": self.sources_type, | |
156 "modal": True | |
157 } | |
158 ) | |
159 screenshare_data = await self.dbus_call( | |
160 "Start", | |
161 session_handle, | |
162 "", | |
163 {} | |
164 ) | |
165 | |
166 session_object = self.session_bus.get_object( | |
167 'org.freedesktop.portal.Desktop', | |
168 session_handle | |
169 ) | |
170 self.session_interface = self.dbus.Interface( | |
171 session_object, | |
172 'org.freedesktop.portal.Session' | |
173 ) | |
174 | |
175 self.session_signal = self.session_bus.add_signal_receiver( | |
176 self.on_session_closed, | |
177 signal_name="Closed", | |
178 dbus_interface="org.freedesktop.portal.Session", | |
179 path=session_handle | |
180 ) | |
181 | |
182 try: | |
183 node_id, stream_data = screenshare_data["streams"][0] | |
184 source_type = int(stream_data["source_type"]) | |
185 except (IndexError, KeyError): | |
186 raise ScreenshareError("Can't parse stream data") | |
187 self.stream_data = stream_data = { | |
188 "session_handle": session_handle, | |
189 "node_id": node_id, | |
190 "source_type": source_type | |
191 } | |
192 try: | |
193 height = int(stream_data["size"][0]) | |
194 weight = int(stream_data["size"][1]) | |
195 except (IndexError, KeyError): | |
196 pass | |
197 else: | |
198 stream_data["size"] = (height, weight) | |
199 | |
200 return self.stream_data | |
201 | |
202 def end_screenshare(self) -> None: | |
203 """Close a running screenshare session, if any.""" | |
204 if self.session_interface is None: | |
205 return | |
206 self.session_interface.Close() | |
207 self.on_session_closed({}) |