Mercurial > libervia-backend
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/tools/webrtc_screenshare.py Sat Apr 06 13:43:09 2024 +0200 @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 + +# Libervia WebRTC implementation +# Copyright (C) 2009-2024 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from libervia.backend.core import exceptions + +import asyncio +import logging +from random import randint + + +log = logging.getLogger(__name__) + + +SOURCES_AUTO = "auto" +SOURCES_TEST = "test" +SOURCES_DATACHANNEL = "datachannel" +SINKS_APP = "app" +SINKS_AUTO = "auto" +SINKS_TEST = "test" +SINKS_DATACHANNEL = "datachannel" + + +class ScreenshareError(Exception): + pass + + +class DesktopPortal: + + def __init__(self, webrtc): + import dbus + from dbus.mainloop.glib import DBusGMainLoop + # we want monitors + windows, see https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html#org-freedesktop-portal-screencast-availablesourcetypes + self.dbus = dbus + self.webrtc = webrtc + self.sources_type = dbus.UInt32(7) + DBusGMainLoop(set_as_default=True) + self.session_bus = dbus.SessionBus() + portal_object = self.session_bus.get_object( + 'org.freedesktop.portal.Desktop', + '/org/freedesktop/portal/desktop' + ) + self.screencast_interface = dbus.Interface( + portal_object, + 'org.freedesktop.portal.ScreenCast' + ) + self.session_interface = None + self.session_signal = None + self.handle_counter = 0 + self.session_handle = None + self.stream_data: dict|None = None + + @property + def handle_token(self): + self.handle_counter += 1 + return f"libervia{self.handle_counter}" + + def on_session_closed(self, details: dict) -> None: + if self.session_interface is not None: + self.session_interface = None + self.webrtc.desktop_sharing = False + if self.session_signal is not None: + self.session_signal.remove() + self.session_signal = None + + + async def dbus_call(self, method_name: str, *args) -> dict: + """Call a screenshare portal method + + This method handle the signal response. + @param method_name: method to call + @param args: extra args + `handle_token` will be automatically added to the last arg (option dict) + @return: method result + """ + if self.session_handle is not None: + self.end_screenshare() + method = getattr(self.screencast_interface, method_name) + options = args[-1] + reply_fut = asyncio.Future() + signal_fut = asyncio.Future() + # cf. https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html + handle_token = self.handle_token + sender = self.session_bus.get_unique_name().replace(".", "_")[1:] + path = f"/org/freedesktop/portal/desktop/request/{sender}/{handle_token}" + signal_match = None + + def on_signal(response, results): + assert signal_match is not None + signal_match.remove() + if response == 0: + signal_fut.set_result(results) + elif response == 1: + signal_fut.set_exception( + exceptions.CancelError("Cancelled by user.") + ) + else: + signal_fut.set_exception(ScreenshareError( + f"Can't get signal result" + )) + + signal_match = self.session_bus.add_signal_receiver( + on_signal, + signal_name="Response", + dbus_interface="org.freedesktop.portal.Request", + path=path + ) + + options["handle_token"] = handle_token + + method( + *args, + reply_handler=reply_fut.set_result, + error_handler=reply_fut.set_exception + ) + try: + await reply_fut + except Exception as e: + raise ScreenshareError(f"Can't ask screenshare permission: {e}") + return await signal_fut + + async def request_screenshare(self) -> dict: + session_data = await self.dbus_call( + "CreateSession", + { + "session_handle_token": str(randint(1, 2**32)), + } + ) + try: + session_handle = session_data["session_handle"] + except KeyError: + raise ScreenshareError("Can't get session handle") + self.session_handle = session_handle + + + await self.dbus_call( + "SelectSources", + session_handle, + { + "multiple": True, + "types": self.sources_type, + "modal": True + } + ) + screenshare_data = await self.dbus_call( + "Start", + session_handle, + "", + {} + ) + + session_object = self.session_bus.get_object( + 'org.freedesktop.portal.Desktop', + session_handle + ) + self.session_interface = self.dbus.Interface( + session_object, + 'org.freedesktop.portal.Session' + ) + + self.session_signal = self.session_bus.add_signal_receiver( + self.on_session_closed, + signal_name="Closed", + dbus_interface="org.freedesktop.portal.Session", + path=session_handle + ) + + try: + node_id, stream_data = screenshare_data["streams"][0] + source_type = int(stream_data["source_type"]) + except (IndexError, KeyError): + raise ScreenshareError("Can't parse stream data") + self.stream_data = stream_data = { + "session_handle": session_handle, + "node_id": node_id, + "source_type": source_type + } + try: + height = int(stream_data["size"][0]) + weight = int(stream_data["size"][1]) + except (IndexError, KeyError): + pass + else: + stream_data["size"] = (height, weight) + + return self.stream_data + + def end_screenshare(self) -> None: + """Close a running screenshare session, if any.""" + if self.session_interface is None: + return + self.session_interface.Close() + self.on_session_closed({})