Mercurial > libervia-backend
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 (14 months ago) |
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") |