frontends (tools/webrtc): implement X11 desktop sharing:
rel 433
This commit is contained in:
parent
3fecc07a7e
commit
27ae8655d7
1 changed files with 130 additions and 10 deletions
|
|
@ -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"""
|
||||
|
|
|
|||
Loading…
Reference in a new issue