Hello KDE community,
I’m developing a POC of pipewire screen capture and input capture using dbus RemoteDesktop and ScreenCast calls and testing the python script on Plasma on Wayland I got a behavior change between version 5.24 and 5.27+. On 5.24 when I run the script I got a screen selection dialog that I can select the screen with the input sharing permissions checkbox, but on 5.27+ only appear a dialog saying that will share screens and input, without checkbox or screen selection. So on 5.27+ it auto selected the monitor/screen that will be shared.
This behavior start to be a problem when you are using 2 or more monitors with the ScreenCast.selectSources multiple option as false, in this case, the selected auto shared monitor is the option “Full workspace” as in the regular screen share portal. And will end up with 2 or more streams showing the full workspace on each stream instead one monitor/screen on each stream.
I have 2 scripts(in the details bellow), one just with the share screen(dbus_screen_cast.py), and another with the Remote Desktop + Share screen(dbus_rd_screen_cast.py). To reproduce the problem you can start the script dbus_screen_cast.py and select to create a virtual display. So now you have 2 monitors. Run the dbus_rd_screen_cast.py script and you will receive 2 streams with the “full workspace” shared screens on kde 5.27+(Kubuntu 23.10. Fedora 39) and the dialog that you can select on kde 5.24+(Kubuntu 22.04, Ubuntu 22.04 with kde-standard)
Is this a bug or a planned behavior change?
Edit 1: I also created a bug report for it, Bug 484996.
dbus_screen_cast.py
#!/usr/bin/python3
import re
import signal
import dbus
from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop
import gi
gi.require_version('Gst', '1.0')
from gi.repository import Gst
DBusGMainLoop(set_as_default=True)
Gst.init(None)
loop = GLib.MainLoop()
bus = dbus.SessionBus()
request_iface = 'org.freedesktop.portal.Request'
screen_cast_iface = 'org.freedesktop.portal.ScreenCast'
pipeline = None
def terminate():
if pipeline is not None:
self.player.set_state(Gst.State.NULL)
loop.quit()
request_token_counter = 0
session_token_counter = 0
sender_name = re.sub(r'\.', r'_', bus.get_unique_name()[1:])
def new_request_path():
global request_token_counter
request_token_counter = request_token_counter + 1
token = 'u%d'%request_token_counter
path = '/org/freedesktop/portal/desktop/request/%s/%s'%(sender_name, token)
return (path, token)
def new_session_path():
global session_token_counter
session_token_counter = session_token_counter + 1
token = 'u%d'%session_token_counter
path = '/org/freedesktop/portal/desktop/session/%s/%s'%(sender_name, token)
return (path, token)
def screen_cast_call(method, callback, *args, options={}):
(request_path, request_token) = new_request_path()
bus.add_signal_receiver(callback,
'Response',
request_iface,
'org.freedesktop.portal.Desktop',
request_path)
options['handle_token'] = request_token
method(*(args + (options, )),
dbus_interface=screen_cast_iface)
def on_gst_message(bus, message):
type = message.type
if type == Gst.MessageType.EOS or type == Gst.MessageType.ERROR:
terminate()
def play_pipewire_stream(node_id):
empty_dict = dbus.Dictionary(signature="sv")
fd_object = portal.OpenPipeWireRemote(session, empty_dict,
dbus_interface=screen_cast_iface)
fd = fd_object.take()
pipeline = Gst.parse_launch('pipewiresrc fd=%d path=%u ! videoconvert ! xvimagesink'%(fd, node_id))
pipeline.set_state(Gst.State.PLAYING)
pipeline.get_bus().connect('message', on_gst_message)
def on_start_response(response, results):
if response != 0:
print("Failed to start: %s"%response)
terminate()
return
print("streams:")
for (node_id, stream_properties) in results['streams']:
print("stream {}".format(node_id))
play_pipewire_stream(node_id)
def on_select_sources_response(response, results):
if response != 0:
print("Failed to select sources: %d"%response)
terminate()
return
print("sources selected")
global session
screen_cast_call(portal.Start, on_start_response,
session, '')
def on_create_session_response(response, results):
if response != 0:
print("Failed to create session: %d"%response)
terminate()
return
global session
session = results['session_handle']
print("session %s created"%session)
screen_cast_call(portal.SelectSources, on_select_sources_response,
session,
options={ 'multiple': False,
'types': dbus.UInt32(1|2) })
portal = bus.get_object('org.freedesktop.portal.Desktop',
'/org/freedesktop/portal/desktop')
(session_path, session_token) = new_session_path()
screen_cast_call(portal.CreateSession, on_create_session_response,
options={ 'session_handle_token': session_token })
try:
loop.run()
except KeyboardInterrupt:
terminate()
dbus_rd_screen_cast.py
#!/usr/bin/python3
import re
import signal
import dbus
from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop
import gi
gi.require_version('Gst', '1.0')
from gi.repository import Gst
DBusGMainLoop(set_as_default=True)
Gst.init(None)
loop = GLib.MainLoop()
bus = dbus.SessionBus()
request_iface = 'org.freedesktop.portal.Request'
screen_cast_iface = 'org.freedesktop.portal.ScreenCast'
remote_desktop_iface = 'org.freedesktop.portal.RemoteDesktop'
pipeline = None
def terminate():
if pipeline is not None:
self.player.set_state(Gst.State.NULL)
loop.quit()
request_token_counter = 0
session_token_counter = 0
sender_name = re.sub(r'\.', r'_', bus.get_unique_name()[1:])
def new_request_path():
global request_token_counter
request_token_counter = request_token_counter + 1
token = 'u%d'%request_token_counter
path = '/org/freedesktop/portal/desktop/request/%s/%s'%(sender_name, token)
return (path, token)
def new_session_path():
global session_token_counter
session_token_counter = session_token_counter + 1
token = 'u%d'%session_token_counter
path = '/org/freedesktop/portal/desktop/session/%s/%s'%(sender_name, token)
return (path, token)
def remote_desktop_call(method, callback, *args, options={}):
(request_path, request_token) = new_request_path()
bus.add_signal_receiver(callback,
'Response',
request_iface,
'org.freedesktop.portal.Desktop',
request_path)
options['handle_token'] = request_token
method(*(args + (options, )),
dbus_interface=remote_desktop_iface)
def screen_cast_call(method, callback, *args, options={}):
(request_path, request_token) = new_request_path()
bus.add_signal_receiver(callback,
'Response',
request_iface,
'org.freedesktop.portal.Desktop',
request_path)
options['handle_token'] = request_token
method(*(args + (options, )),
dbus_interface=screen_cast_iface)
def on_gst_message(bus, message):
type = message.type
if type == Gst.MessageType.EOS or type == Gst.MessageType.ERROR:
terminate()
def play_pipewire_stream(node_id):
empty_dict = dbus.Dictionary(signature="sv")
fd_object = portal.OpenPipeWireRemote(session, empty_dict,
dbus_interface=screen_cast_iface)
fd = fd_object.take()
pipeline = Gst.parse_launch('pipewiresrc fd=%d path=%u ! videoconvert ! xvimagesink'%(fd, node_id))
pipeline.set_state(Gst.State.PLAYING)
pipeline.get_bus().connect('message', on_gst_message)
def on_start_response(response, results):
if response != 0:
print("Failed to start: %s"%response)
terminate()
return
print("streams:")
for (node_id, stream_properties) in results['streams']:
print("stream {}".format(node_id))
play_pipewire_stream(node_id)
def on_rd_select_sources_response(response, results):
if response != 0:
print("Failed to select sources: %d"%response)
terminate()
return
print("sources selected", results)
global session
remote_desktop_call(portal.Start, on_start_response,
session, '')
def on_create_session_response(response, results):
if response != 0:
print("Failed to create session: %d"%response)
terminate()
return
global session
print(results)
print("session %s created"%session)
screen_cast_call(portal.SelectSources, on_rd_select_sources_response,
session,
options={ 'multiple': False,
'types': dbus.UInt32(1) })
def on_create_rd_session_response(response, results):
if response != 0:
print("Failed to create session: %d"%response)
terminate()
return
global session
session = results['session_handle']
print("session %s created"%session)
remote_desktop_call(portal.SelectDevices, on_create_session_response,
session,
options={ 'types': dbus.UInt32(7) })
portal = bus.get_object('org.freedesktop.portal.Desktop',
'/org/freedesktop/portal/desktop')
(session_path, session_token) = new_session_path()
remote_desktop_call(portal.CreateSession, on_create_rd_session_response,
options={ 'session_handle_token': session_token })
try:
loop.run()
except KeyboardInterrupt:
terminate()