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