aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarvin W <git@larma.de>2025-04-09 20:02:55 +0200
committerMarvin W <git@larma.de>2025-04-09 20:05:13 +0200
commit50eeb2f16dfa4342d50086404da068f5426cbe3c (patch)
tree3738913a2661af3d738fc5ce14d76497c6a29b5e
parent75b5aa91716e962ff38a35f7bb21aab384e35232 (diff)
RTP: Tear down most of GStreamer while not in a call.
This includes #1679 and works around an upstream bug in pipewire causing #1685
-rw-r--r--main/src/ui/call_window/call_window.vala4
-rw-r--r--main/src/ui/call_window/call_window_controller.vala39
-rw-r--r--main/src/ui/call_window/participant_widget.vala6
-rw-r--r--plugins/rtp/src/device.vala5
-rw-r--r--plugins/rtp/src/plugin.vala66
-rw-r--r--plugins/rtp/src/stream.vala6
-rw-r--r--plugins/rtp/src/video_widget.vala19
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<string, ParticipantWidget> participant_widgets = new HashMap<string, ParticipantWidget>();
private ArrayList<string> participants = new ArrayList<string>();
@@ -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<Stream> streams = new ArrayList<Stream>();
private Gee.List<Device> devices = new ArrayList<Device>();
+ private int64 last_devices_refresh = 0;
// private Gee.List<Participant> participants = new ArrayList<Participant>();
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<Device>();
+ 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<MediaDevice> get_devices(string media, bool incoming) {
+ refresh_devices();
Gee.List<MediaDevice> 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) {