From 50eeb2f16dfa4342d50086404da068f5426cbe3c Mon Sep 17 00:00:00 2001 From: Marvin W Date: Wed, 9 Apr 2025 20:02:55 +0200 Subject: RTP: Tear down most of GStreamer while not in a call. This includes #1679 and works around an upstream bug in pipewire causing #1685 --- main/src/ui/call_window/call_window.vala | 4 +- .../src/ui/call_window/call_window_controller.vala | 39 ++++++++----- main/src/ui/call_window/participant_widget.vala | 6 ++ plugins/rtp/src/device.vala | 5 +- plugins/rtp/src/plugin.vala | 66 +++++++++++++++++----- plugins/rtp/src/stream.vala | 6 +- plugins/rtp/src/video_widget.vala | 19 +++++++ 7 files changed, 112 insertions(+), 33 deletions(-) diff --git a/main/src/ui/call_window/call_window.vala b/main/src/ui/call_window/call_window.vala index 08ea4b21..54e290a7 100644 --- a/main/src/ui/call_window/call_window.vala +++ b/main/src/ui/call_window/call_window.vala @@ -18,7 +18,6 @@ namespace Dino.Ui { public HeaderBar header_bar = new HeaderBar() { valign=Align.START, halign=Align.END, show_title_buttons=true, opacity=0.0 }; public Revealer header_bar_revealer = new Revealer() { halign=Align.END, valign=Align.START, transition_type=RevealerTransitionType.SLIDE_LEFT, transition_duration=200, reveal_child=false }; public Box own_video_box = new Box(Orientation.HORIZONTAL, 0) { halign=Align.END, valign=Align.END }; - private Widget? own_video = null; private HashMap participant_widgets = new HashMap(); private ArrayList participants = new ArrayList(); @@ -98,6 +97,7 @@ namespace Dino.Ui { } public void remove_participant(string participant) { + participant_widgets[participant].set_video(new Adw.Bin()); participants.remove(participant); grid.remove(participant_widgets[participant]); participant_widgets.unset(participant); @@ -167,7 +167,7 @@ namespace Dino.Ui { public void set_own_video(Widget? widget_) { unset_own_video(); - own_video = widget_; + Widget? own_video = widget_; if (own_video == null) { own_video = new Box(Orientation.HORIZONTAL, 0); } diff --git a/main/src/ui/call_window/call_window_controller.vala b/main/src/ui/call_window/call_window_controller.vala index 6e3eac6d..2543569d 100644 --- a/main/src/ui/call_window/call_window_controller.vala +++ b/main/src/ui/call_window/call_window_controller.vala @@ -210,18 +210,25 @@ public class Dino.Ui.CallWindowController : Object { bool show_keys = true; Plugins.Registry registry = Dino.Application.get_default().plugin_registry; if (((has_audio && audio_encryption != null) || (has_video && video_encryption != null)) && (!has_audio || !has_video || (audio_encryption != null && video_encryption != null && audio_encryption.encryption_ns == video_encryption.encryption_ns))) { - Plugins.CallEncryptionEntry? encryption_entry = audio_encryption != null ? registry.call_encryption_entries[audio_encryption.encryption_ns] : null; - if (encryption_entry != null) { - Plugins.CallEncryptionWidget? audio_encryption_widgets = encryption_entry.get_widget(call.account, audio_encryption); - Plugins.CallEncryptionWidget? video_encryption_widgets = encryption_entry.get_widget(call.account, video_encryption); - if (audio_encryption_widgets != null && video_encryption_widgets != null) { - if (audio_encryption_widgets.get_title() == video_encryption_widgets.get_title()) - title = audio_encryption_widgets.get_title(); - if (audio_encryption_widgets.get_icon_name() == video_encryption_widgets.get_icon_name()) - icon_name = audio_encryption_widgets.get_icon_name(); - if (audio_encryption_widgets.show_keys() == video_encryption_widgets.show_keys()) - show_keys = audio_encryption_widgets.show_keys(); - } + Plugins.CallEncryptionEntry? audio_encryption_entry = audio_encryption != null ? registry.call_encryption_entries[audio_encryption.encryption_ns] : null; + Plugins.CallEncryptionEntry? video_encryption_entry = video_encryption != null ? registry.call_encryption_entries[video_encryption.encryption_ns] : null; + Plugins.CallEncryptionWidget? audio_encryption_widgets = audio_encryption_entry != null ? audio_encryption_entry.get_widget(call.account, audio_encryption) : null; + Plugins.CallEncryptionWidget? video_encryption_widgets = video_encryption_entry != null ? video_encryption_entry.get_widget(call.account, video_encryption) : null; + if (audio_encryption_widgets != null && !has_video) { + title = audio_encryption_widgets.get_title(); + icon_name = audio_encryption_widgets.get_icon_name(); + show_keys = audio_encryption_widgets.show_keys(); + } else if (video_encryption_widgets != null && !has_audio) { + title = video_encryption_widgets.get_title(); + icon_name = video_encryption_widgets.get_icon_name(); + show_keys = video_encryption_widgets.show_keys(); + } else if (audio_encryption_widgets != null && video_encryption_widgets != null) { + if (audio_encryption_widgets.get_title() == video_encryption_widgets.get_title()) + title = audio_encryption_widgets.get_title(); + if (audio_encryption_widgets.get_icon_name() == video_encryption_widgets.get_icon_name()) + icon_name = audio_encryption_widgets.get_icon_name(); + if (audio_encryption_widgets.show_keys() == video_encryption_widgets.show_keys()) + show_keys = audio_encryption_widgets.show_keys(); } } @@ -376,7 +383,13 @@ public class Dino.Ui.CallWindowController : Object { } call_window_handler_ids = bottom_bar_handler_ids = new ulong[0]; - own_video.detach(); + if (own_video != null) { + if (this.call_window.bottom_bar.video_enabled) { + own_video.detach(); + call_window.unset_own_video(); + } + own_video = null; + } base.dispose(); } } diff --git a/main/src/ui/call_window/participant_widget.vala b/main/src/ui/call_window/participant_widget.vala index 0940bc9e..ba06f27a 100644 --- a/main/src/ui/call_window/participant_widget.vala +++ b/main/src/ui/call_window/participant_widget.vala @@ -139,5 +139,11 @@ namespace Dino.Ui { header_bar.opacity = controls_active ? 1.0 : 0.0; invite_button.visible = may_show_invite_button && is_highest_row && is_start_row; } + + public override void dispose() { + main_widget = null; + overlay = null; + base.dispose(); + } } } diff --git a/plugins/rtp/src/device.vala b/plugins/rtp/src/device.vala index 7fee7307..734d32d9 100644 --- a/plugins/rtp/src/device.vala +++ b/plugins/rtp/src/device.vala @@ -19,6 +19,7 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object { public string id { owned get { return device_name; }} public string display_name { owned get { return device_display_name; }} public string? detail_name { owned get { + if (device.properties == null) return null; if (device.properties.has_field("alsa.card_name")) return device.properties.get_string("alsa.card_name"); if (device.properties.has_field("alsa.name")) return device.properties.get_string("alsa.name"); if (device.properties.has_field("alsa.id")) return device.properties.get_string("alsa.id"); @@ -39,13 +40,15 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object { public Gst.Pipeline pipe { get { return plugin.pipe; }} public bool is_source { get { return device.has_classes("Source"); }} public bool is_sink { get { return device.has_classes("Sink"); }} - public bool is_monitor { get { return device.properties.get_string("device.class") == "monitor" || (protocol == DeviceProtocol.PIPEWIRE && device.has_classes("Stream")); } } + public bool is_monitor { get { return (device.properties != null && device.properties.get_string("device.class") == "monitor") || (protocol == DeviceProtocol.PIPEWIRE && device.has_classes("Stream")); } } public bool is_default { get { + if (device.properties == null) return false; bool ret; device.properties.get_boolean("is-default", out ret); return ret; }} public DeviceProtocol protocol { get { + if (device.properties == null) return DeviceProtocol.OTHER; if (device.properties.has_name("pulse-proplist")) return DeviceProtocol.PULSEAUDIO; if (device.properties.has_name("pipewire-proplist")) return DeviceProtocol.PIPEWIRE; if (device.properties.has_name("v4l2deviceprovider")) return DeviceProtocol.V4L2; diff --git a/plugins/rtp/src/plugin.vala b/plugins/rtp/src/plugin.vala index 58ce871c..a94c0ba0 100644 --- a/plugins/rtp/src/plugin.vala +++ b/plugins/rtp/src/plugin.vala @@ -12,12 +12,12 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { private Gee.List streams = new ArrayList(); private Gee.List devices = new ArrayList(); + private int64 last_devices_refresh = 0; // private Gee.List participants = new ArrayList(); public void registered(Dino.Application app) { this.app = app; this.codec_util = new CodecUtil(); - app.startup.connect(startup); app.add_option_group(Gst.init_get_option_group()); app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => { list.add(new Module(this)); @@ -42,22 +42,54 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { if (pause_count < 0) warning("Pause count below zero!"); } - private void init_device_monitor() { + private void handle_existing_devices(Gst.DeviceMonitor device_monitor) { + var new_devices = new ArrayList(); + foreach (Gst.Device device in device_monitor.get_devices()) { + if (device.properties == null) continue; + if (device.properties.has_name("pipewire-proplist") && device.has_classes("Audio")) continue; + if (device.properties.get_string("device.class") == "monitor") continue; + var pre_device = devices.first_match((it) => it.matches(device)); + if (pre_device != null) { + if (!new_devices.contains(pre_device)) new_devices.add(pre_device); + continue; + } + var new_device = new Device(this, device); + devices.add(new_device); + new_devices.add(new_device); + } + devices.retain_all(new_devices); + } + + private void refresh_devices() { + if (device_monitor != null) return; // No manual refresh needed, we're monitoring actively. + if (last_devices_refresh > 0 && last_devices_refresh + 5000000 > get_monotonic_time()) return; // Already current + var device_monitor = new Gst.DeviceMonitor(); + device_monitor.show_all = true; + device_monitor.start(); + handle_existing_devices(device_monitor); + device_monitor.stop(); + last_devices_refresh = get_monotonic_time(); + } + + private void start_device_monitor() { if (device_monitor != null) return; device_monitor = new Gst.DeviceMonitor(); device_monitor.show_all = true; device_monitor.get_bus().add_watch(Priority.DEFAULT, on_device_monitor_message); device_monitor.start(); - foreach (Gst.Device device in device_monitor.get_devices()) { - if (device.properties.has_name("pipewire-proplist") && device.has_classes("Audio")) continue; - if (device.properties.get_string("device.class") == "monitor") continue; - if (devices.any_match((it) => it.matches(device))) continue; - devices.add(new Device(this, device)); - } + handle_existing_devices(device_monitor); + } + + private void stop_device_monitor() { + if (device_monitor == null) return; + device_monitor.stop(); + device_monitor = null; } private void init_call_pipe() { if (pipe != null) return; + debug("Creating call pipe."); + start_device_monitor(); pipe = new Gst.Pipeline(null); // RTP @@ -90,6 +122,12 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { pipe.set_state(Gst.State.PLAYING); } + public void destroy_call_pipe_if_unused() { + if (streams.is_empty && !VideoWidget.has_instances()) { + destroy_call_pipe(); + } + } + private void destroy_call_pipe() { if (pipe == null) return; pipe.set_state(Gst.State.NULL); @@ -98,10 +136,8 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { echoprobe = null; #endif pipe = null; - } - - public void startup() { - init_device_monitor(); + stop_device_monitor(); + debug("Call pipe destroyed"); } private static Gst.Caps? request_pt_map(Gst.Element rtpbin, uint session, uint pt, Plugin plugin) { @@ -269,12 +305,11 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { public void close_stream(Stream stream) { streams.remove(stream); stream.destroy(); + destroy_call_pipe_if_unused(); } public void shutdown() { - if (device_monitor != null) { - device_monitor.stop(); - } + stop_device_monitor(); destroy_call_pipe(); Gst.deinit(); } @@ -292,6 +327,7 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { } public Gee.List get_devices(string media, bool incoming) { + refresh_devices(); Gee.List devices; if (media == "video" && !incoming) { devices = get_video_sources(); diff --git a/plugins/rtp/src/stream.vala b/plugins/rtp/src/stream.vala index 94549856..aa2b080d 100644 --- a/plugins/rtp/src/stream.vala +++ b/plugins/rtp/src/stream.vala @@ -454,7 +454,8 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { } send_rtp.set_locked_state(true); send_rtp.set_state(Gst.State.NULL); - pipe.remove(send_rtp); + // This happens async, so pipe might be gone by now. + if (pipe != null) pipe.remove(send_rtp); send_rtp = null; debug("Stopped sending RTP for %u", rtpid); } @@ -462,7 +463,8 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { private void on_send_rtcp_eos() { send_rtcp.set_locked_state(true); send_rtcp.set_state(Gst.State.NULL); - pipe.remove(send_rtcp); + // This happens async, so pipe might be gone by now. + if (pipe != null) pipe.remove(send_rtcp); send_rtcp = null; debug("Stopped sending RTCP for %u", rtpid); } diff --git a/plugins/rtp/src/video_widget.vala b/plugins/rtp/src/video_widget.vala index 05cc5a6c..5557ec76 100644 --- a/plugins/rtp/src/video_widget.vala +++ b/plugins/rtp/src/video_widget.vala @@ -176,6 +176,22 @@ public class Dino.Plugins.Rtp.VideoWidget : Gtk.Widget, Dino.Plugins.VideoCallWi private Sink sink; private Gtk.Picture widget; + private static uint active_widgets = 0; + + public static bool has_instances() { + return active_widgets > 0; + } + + private static void notify_weak(Object widget_object) { + if (active_widgets > 0) { + debug("Video widget %p destroyed. left=%u", widget_object, active_widgets); + active_widgets--; + } + if (active_widgets == 0 && widget_object is VideoWidget) { + ((VideoWidget)widget_object).plugin.destroy_call_pipe_if_unused(); + } + } + public VideoWidget(Plugin plugin) { this.plugin = plugin; this.layout_manager = new Gtk.BinLayout(); @@ -184,6 +200,9 @@ public class Dino.Plugins.Rtp.VideoWidget : Gtk.Widget, Dino.Plugins.VideoCallWi sink = new Sink() { async = false, sync = true }; widget = new Gtk.Picture.for_paintable(sink.paintable); widget.insert_after(this, null); + active_widgets++; + debug("Video widget %p created. total=%u", this, active_widgets); + this.weak_ref(notify_weak); } public void input_caps_changed(GLib.Object pad, ParamSpec spec) { -- cgit v1.3