frontends (tools/webrtc): implement X11 desktop sharing:

rel 433
This commit is contained in:
Goffi 2024-01-16 10:42:00 +01:00
commit 27ae8655d7

View file

@ -41,7 +41,13 @@ from typing import Callable
from urllib.parse import quote_plus
from libervia.backend.tools.common import data_format
from libervia.frontends.tools import aio
from libervia.frontends.tools import aio, display_servers
current_server = display_servers.detect()
if current_server == display_servers.X11:
# GSTreamer's ximagesrc documentation asks to run this function
import ctypes
ctypes.CDLL('libX11.so.6').XInitThreads()
log = logging.getLogger(__name__)
@ -83,6 +89,8 @@ class WebRTC:
self.pipeline = None
self._audio_muted = False
self._video_muted = False
self._desktop_sharing = False
self.desktop_sharing_data = None
self.sources = sources
self.sinks = sinks
if sinks == SINKS_APP:
@ -114,6 +122,16 @@ class WebRTC:
self._video_muted = muted
self.on_video_mute(muted)
@property
def desktop_sharing(self):
return self._desktop_sharing
@desktop_sharing.setter
def desktop_sharing(self, active: bool) -> None:
if active != self._desktop_sharing:
self._desktop_sharing = active
self.on_desktop_switch(active)
@property
def sdp_set(self):
return self._sdp_set
@ -384,7 +402,7 @@ class WebRTC:
{extra_elt}
{video_source_elt} name=video_src ! queue leaky=downstream ! video_selector.
videotestsrc is-live=true pattern=black ! queue leaky=downstream ! video_selector.
videotestsrc name=muted_src is-live=true pattern=black ! queue leaky=downstream ! video_selector.
t.
! queue max-size-buffers=5 max-size-time=0 max-size-bytes=0 leaky=downstream
@ -419,6 +437,7 @@ class WebRTC:
self.webrtcbin = self.pipeline.get_by_name("sendrecv")
self.video_src = self.pipeline.get_by_name("video_src")
self.muted_src = self.pipeline.get_by_name("muted_src")
self.video_selector = self.pipeline.get_by_name("video_selector")
self.audio_valve = self.pipeline.get_by_name("audio_valve")
@ -888,22 +907,123 @@ class WebRTC:
log.info("End of stream")
def on_audio_mute(self, muted: bool) -> None:
"""Handles (un)muting of audio.
@param muted: True if audio is muted.
"""
if self.audio_valve is not None:
self.audio_valve.set_property("drop", muted)
state = "muted" if muted else "unmuted"
log.info(f"audio is now {state}")
def on_video_mute(self, muted: bool) -> None:
"""Handles (un)muting of video.
@param muted: True if video is muted.
"""
if self.video_selector is not None:
# when muted, we switch to a black image and deactivate the camera
if not muted:
self.video_src.set_state(Gst.State.PLAYING)
pad = self.video_selector.get_static_pad("sink_1" if muted else "sink_0")
self.video_selector.props.active_pad = pad
if muted:
self.video_src.set_state(Gst.State.NULL)
current_source = None if muted else "desktop" if self.desktop_sharing else "camera"
self.switch_video_source(current_source)
state = "muted" if muted else "unmuted"
log.info(f"video is now {state}")
log.info(f"Video is now {state}")
def on_desktop_switch(self, desktop_active: bool) -> None:
"""Switches the video source between desktop and camera.
@param desktop_active: True if desktop must be active. False for camera.
"""
if self.video_muted:
# Update the active source state but do not switch
self.desktop_sharing = desktop_active
return
source = "desktop" if desktop_active else "camera"
self.switch_video_source(source)
self.desktop_sharing = desktop_active
def switch_video_source(self, source: str|None) -> None:
"""Activates the specified source while deactivating the others.
@param source: 'desktop', 'camera', 'muted' or None for muted source.
"""
if source is None:
source = "muted"
if source not in ["camera", "muted", "desktop"]:
raise ValueError(
f"Invalid source: {source!r}, use one of {'camera', 'muted', 'desktop'}"
)
self.pipeline.set_state(Gst.State.PAUSED)
# Create a new desktop source if necessary
if source == "desktop":
self._setup_desktop_source(self.desktop_sharing_data)
# Activate the chosen source and deactivate the others
for src_name in ["camera", "muted", "desktop"]:
src_element = self.pipeline.get_by_name(f"{src_name}_src")
if src_name == source:
if src_element:
src_element.set_state(Gst.State.PLAYING)
else:
if src_element:
src_element.set_state(Gst.State.NULL)
if src_name == "desktop":
self._remove_desktop_source(src_element)
# Set the video_selector active pad
pad_name = f"sink_{['camera', 'muted', 'desktop'].index(source)}"
pad = self.video_selector.get_static_pad(pad_name)
self.video_selector.props.active_pad = pad
self.pipeline.set_state(Gst.State.PLAYING)
def _setup_desktop_source(self, properties: dict[str, object]|None) -> None:
"""Set up a new desktop source.
@param properties: The properties to set on the desktop source.
"""
desktop_src = Gst.ElementFactory.make("ximagesrc", "desktop_src")
if properties is None:
properties = {}
for key, value in properties.items():
log.debug(f"setting ximagesrc property: {key!r}={value!r}")
desktop_src.set_property(key, value)
video_convert = Gst.ElementFactory.make("videoconvert", "desktop_videoconvert")
queue = Gst.ElementFactory.make("queue", "desktop_queue")
queue.set_property("leaky", "downstream")
self.pipeline.add(desktop_src)
self.pipeline.add(video_convert)
self.pipeline.add(queue)
desktop_src.link(video_convert)
video_convert.link(queue)
sink_pad_template = self.video_selector.get_pad_template("sink_%u")
sink_pad = self.video_selector.request_pad(sink_pad_template, None, None)
queue_src_pad = queue.get_static_pad("src")
queue_src_pad.link(sink_pad)
desktop_src.sync_state_with_parent()
video_convert.sync_state_with_parent()
queue.sync_state_with_parent()
def _remove_desktop_source(self, desktop_src: Gst.Element) -> None:
"""Remove the desktop source from the pipeline.
@param desktop_src: The desktop source to remove.
"""
# Remove elements for the desktop source
video_convert = self.pipeline.get_by_name("desktop_videoconvert")
queue = self.pipeline.get_by_name("desktop_queue")
if video_convert:
video_convert.set_state(Gst.State.NULL)
desktop_src.unlink(video_convert)
self.pipeline.remove(video_convert)
if queue:
queue.set_state(Gst.State.NULL)
self.pipeline.remove(queue)
self.pipeline.remove(desktop_src)
async def end_call(self) -> None:
"""Stop streaming and clean instance"""