comparison libervia/frontends/tools/webrtc.py @ 4209:fe29fbdabce6

frontends (tools/webrtc): add a options to merge video for PiP and to specify size: - the `merge_pip` option is now used to indicate if local feedback and remote video streams must be merged with local feedback being a PiP of remote video. By default, it's done for `SINKS_AUTO`, but it can be manually specified to use it with `SINKS_APP` or to split ``SINKS_AUTO`` in 2 windows. - target size of the compositor used with `merge_pip` can be specified. By default, 720p is used. rel 428
author Goffi <goffi@goffi.org>
date Fri, 16 Feb 2024 18:46:02 +0100
parents 17a8168966f9
children d01b8d002619
comparison
equal deleted inserted replaced
4208:fd9e78b7a0cd 4209:fe29fbdabce6
67 67
68 68
69 @dataclass 69 @dataclass
70 class AppSinkData: 70 class AppSinkData:
71 local_video_cb: Callable 71 local_video_cb: Callable
72 remote_video_cb: Callable 72 remote_video_cb: Callable|None
73 73
74 74
75 class DesktopPortal: 75 class DesktopPortal:
76 76
77 def __init__(self, webrtc: "WebRTC"): 77 def __init__(self, webrtc: "WebRTC"):
253 profile: str, 253 profile: str,
254 sources: str = SOURCES_AUTO, 254 sources: str = SOURCES_AUTO,
255 sinks: str = SINKS_AUTO, 255 sinks: str = SINKS_AUTO,
256 appsink_data: AppSinkData | None = None, 256 appsink_data: AppSinkData | None = None,
257 reset_cb: Callable | None = None, 257 reset_cb: Callable | None = None,
258 merge_pip: bool|None = None,
259 target_size: tuple[int, int]|None = None,
258 ) -> None: 260 ) -> None:
261 """Initializes a new WebRTC instance.
262
263 @param bridge: An instance of backend bridge.
264 @param profile: Libervia profile.
265 @param sources: Which kind of source to use.
266 @param sinks: Which kind of sinks to use.
267 @param appsink_data: configuration data for appsink (when SINKS_APP is used). Must
268 not be used for other sinks.
269 @param reset_cb: An optional Callable that is triggered on reset events. Can be
270 used to reset UI data on new calls.
271 @param merge_pip: A boolean flag indicating whether Picture-in-Picture mode is
272 enabled. When PiP is used, local feedback is merged to remote video stream.
273 Only one video stream is then produced (the local one).
274 If None, PiP mode is selected automatically according to selected sink (it's
275 used for SINKS_AUTO only for now).
276 @param target_size: Expected size of the final sink stream. Mainly use by composer
277 when ``merge_pip`` is set.
278 None to autodetect (not real autodetection implemeted yet, default to
279 (1280,720)).
280 """
259 self.main_loop = asyncio.get_event_loop() 281 self.main_loop = asyncio.get_event_loop()
260 self.bridge = bridge 282 self.bridge = bridge
261 self.profile = profile 283 self.profile = profile
262 self.pipeline = None 284 self.pipeline = None
263 self._audio_muted = False 285 self._audio_muted = False
264 self._video_muted = False 286 self._video_muted = False
265 self._desktop_sharing = False 287 self._desktop_sharing = False
266 self.desktop_sharing_data = None 288 self.desktop_sharing_data = None
267 self.sources = sources 289 self.sources = sources
268 self.sinks = sinks 290 self.sinks = sinks
291 if target_size is None:
292 target_size=(1280, 720)
293 self.target_width, self.target_height = target_size
294 if merge_pip is None:
295 merge_pip = sinks == SINKS_AUTO
296 self.merge_pip = merge_pip
269 if sinks == SINKS_APP: 297 if sinks == SINKS_APP:
298 if (
299 merge_pip
300 and appsink_data is not None
301 and appsink_data.remote_video_cb is not None
302 ):
303 raise ValueError("Remote_video_cb can't be used when merge_pip is used!")
270 self.appsink_data = appsink_data 304 self.appsink_data = appsink_data
271 elif appsink_data is not None: 305 elif appsink_data is not None:
272 raise exceptions.InternalError( 306 raise exceptions.InternalError(
273 "appsink_data can only be used for SINKS_APP sinks" 307 "appsink_data can only be used for SINKS_APP sinks"
274 ) 308 )
574 video_source_elt = "videotestsrc is-live=true pattern=ball" 608 video_source_elt = "videotestsrc is-live=true pattern=ball"
575 audio_source_elt = "audiotestsrc" 609 audio_source_elt = "audiotestsrc"
576 else: 610 else:
577 raise exceptions.InternalError(f'Unknown "sources" value: {self.sources!r}') 611 raise exceptions.InternalError(f'Unknown "sources" value: {self.sources!r}')
578 612
579 extra_elt = ""
580 613
581 if self.sinks == SINKS_APP: 614 if self.sinks == SINKS_APP:
582 local_video_sink_elt = ( 615 local_video_sink_elt = (
583 "appsink name=local_video_sink emit-signals=true drop=true max-buffers=1 " 616 "appsink name=local_video_sink emit-signals=true drop=true max-buffers=1 "
584 "sync=True" 617 "sync=True"
585 ) 618 )
586 elif self.sinks == SINKS_AUTO: 619 elif self.sinks == SINKS_AUTO:
587 extra_elt = "compositor name=compositor ! autovideosink" 620 local_video_sink_elt = "autovideosink"
588 local_video_sink_elt = """compositor.sink_1"""
589 else: 621 else:
590 raise exceptions.InternalError(f"Unknown sinks value {self.sinks!r}") 622 raise exceptions.InternalError(f"Unknown sinks value {self.sinks!r}")
623
624 if self.merge_pip:
625 extra_elt = (
626 "compositor name=compositor background=black "
627 f"! video/x-raw,width={self.target_width},height={self.target_height},"
628 "framerate=30/1 "
629 f"! {local_video_sink_elt}"
630 )
631 local_video_sink_elt = "compositor.sink_1"
632 else:
633 extra_elt = ""
591 634
592 self.gst_pipe_desc = f""" 635 self.gst_pipe_desc = f"""
593 webrtcbin latency=100 name=sendrecv bundle-policy=max-bundle 636 webrtcbin latency=100 name=sendrecv bundle-policy=max-bundle
594 637
595 input-selector name=video_selector 638 input-selector name=video_selector
627 """ 670 """
628 671
629 log.debug(f"Gstreamer pipeline: {self.gst_pipe_desc}") 672 log.debug(f"Gstreamer pipeline: {self.gst_pipe_desc}")
630 673
631 # Create the pipeline 674 # Create the pipeline
632 self.pipeline = Gst.parse_launch(self.gst_pipe_desc) 675 try:
676 self.pipeline = Gst.parse_launch(self.gst_pipe_desc)
677 except Exception:
678 log.exception("Can't parse pipeline")
679 self.pipeline = None
633 if not self.pipeline: 680 if not self.pipeline:
634 raise exceptions.InternalError("Failed to create Gstreamer pipeline.") 681 raise exceptions.InternalError("Failed to create Gstreamer pipeline.")
635 682
636 self.webrtcbin = self.pipeline.get_by_name("sendrecv") 683 self.webrtcbin = self.pipeline.get_by_name("sendrecv")
637 self.video_src = self.pipeline.get_by_name("video_src") 684 self.video_src = self.pipeline.get_by_name("video_src")
817 log.debug("===> VIDEO OK") 864 log.debug("===> VIDEO OK")
818 865
819 self._remote_video_pad = pad 866 self._remote_video_pad = pad
820 867
821 # Check and log the original size of the video 868 # Check and log the original size of the video
822 width = s.get_int("width").value 869 width = self.target_width
823 height = s.get_int("height").value 870 height = self.target_height
824 log.info(f"Original video size: {width}x{height}") 871 log.info(f"Original video size: {width}x{height}")
825 872
826 # This is a fix for an issue found with Movim on desktop: a non standard 873 # This is a fix for an issue found with Movim on desktop: a non standard
827 # resolution is used (990x557) resulting in bad alignement and no color in 874 # resolution is used (990x557) resulting in bad alignement and no color in
828 # rendered image 875 # rendered image
832 width = (width + 3) // 4 * 4 879 width = (width + 3) // 4 * 4
833 height = (height + 3) // 4 * 4 880 height = (height + 3) // 4 * 4
834 log.info(f"Adjusted video size: {width}x{height}") 881 log.info(f"Adjusted video size: {width}x{height}")
835 882
836 conv = Gst.ElementFactory.make("videoconvert") 883 conv = Gst.ElementFactory.make("videoconvert")
837 if self.sinks == SINKS_APP: 884 if self.merge_pip:
885 # with ``merge_pip`` set, we plug the remote stream to the composer
886 compositor = self.pipeline.get_by_name("compositor")
887
888 sink1_pad = compositor.get_static_pad("sink_1")
889
890 local_width, local_height = self.scaled_dimensions(
891 sink1_pad.get_property("width"),
892 sink1_pad.get_property("height"),
893 width // 3,
894 height // 3,
895 )
896
897 sink1_pad.set_property("xpos", width - local_width)
898 sink1_pad.set_property("ypos", height - local_height)
899 sink1_pad.set_property("width", local_width)
900 sink1_pad.set_property("height", local_height)
901 sink1_pad.set_property("sizing-policy", 1)
902 sink1_pad.set_property("zorder", 1)
903
904 # Request a new pad for the remote stream
905 sink_pad_template = compositor.get_pad_template("sink_%u")
906 remote_video_sink = compositor.request_pad(sink_pad_template, None, None)
907 remote_video_sink.set_property("zorder", 0)
908 remote_video_sink.set_property("width", width)
909 remote_video_sink.set_property("height", height)
910 remote_video_sink.set_property("sizing-policy", 1)
911 elif self.sinks == SINKS_APP:
912 # ``app`` sink without ``self.merge_pip`` set, be create the sink and
913 # connect it to the ``remote_video_cb``.
838 assert self.appsink_data is not None 914 assert self.appsink_data is not None
839 remote_video_sink = Gst.ElementFactory.make("appsink") 915 remote_video_sink = Gst.ElementFactory.make("appsink")
840 916
841 appsink_caps = Gst.Caps.from_string("video/x-raw,format=RGB") 917 remote_video_caps = Gst.Caps.from_string("video/x-raw,format=RGB")
842 remote_video_sink.set_property("caps", appsink_caps) 918 remote_video_sink.set_property("caps", remote_video_caps)
843 919
844 remote_video_sink.set_property("emit-signals", True) 920 remote_video_sink.set_property("emit-signals", True)
845 remote_video_sink.set_property("drop", True) 921 remote_video_sink.set_property("drop", True)
846 remote_video_sink.set_property("max-buffers", 1) 922 remote_video_sink.set_property("max-buffers", 1)
847 remote_video_sink.set_property("sync", True) 923 remote_video_sink.set_property("sync", True)
848 remote_video_sink.connect("new-sample", self.appsink_data.remote_video_cb) 924 remote_video_sink.connect("new-sample", self.appsink_data.remote_video_cb)
849 self.pipeline.add(remote_video_sink) 925 self.pipeline.add(remote_video_sink)
850 elif self.sinks == SINKS_AUTO: 926 elif self.sinks == SINKS_AUTO:
851 compositor = self.pipeline.get_by_name("compositor") 927 # if ``self.merge_pip`` is not set, we create a dedicated
852 928 # ``autovideosink`` for remote stream.
853 sink1_pad = compositor.get_static_pad("sink_1") 929 remote_video_sink = Gst.ElementFactory.make("autovideosink")
854 930 self.pipeline.add(remote_video_sink)
855 local_width, local_height = self.scaled_dimensions(
856 sink1_pad.get_property("width"),
857 sink1_pad.get_property("height"),
858 width // 3,
859 height // 3,
860 )
861
862 sink1_pad.set_property("xpos", width - local_width)
863 sink1_pad.set_property("ypos", height - local_height)
864 sink1_pad.set_property("width", local_width)
865 sink1_pad.set_property("height", local_height)
866 sink1_pad.set_property("zorder", 1)
867
868 # Request a new pad for the remote stream
869 sink_pad_template = compositor.get_pad_template("sink_%u")
870 remote_video_sink = compositor.request_pad(sink_pad_template, None, None)
871 remote_video_sink.set_property("zorder", 0)
872
873 else: 931 else:
874 raise exceptions.InternalError(f'Unhandled "sinks" value: {self.sinks!r}') 932 raise exceptions.InternalError(f'Unhandled "sinks" value: {self.sinks!r}')
875 933
876 if adjust_resolution: 934 if adjust_resolution:
877 videoscale = Gst.ElementFactory.make("videoscale") 935 videoscale = Gst.ElementFactory.make("videoscale")