From 369d0c79d7272b4059c39ecedb10a62121bfbe56 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 12 Feb 2022 14:35:44 +0100 Subject: [PATCH] Calls: Fix device selector for multi-party calls, allow picking device before call started --- libdino/src/plugin/interfaces.vala | 14 +++-- libdino/src/service/call_state.vala | 59 ++++++++++++++++--- .../call_window/audio_settings_popover.vala | 24 ++++---- .../call_window/call_window_controller.vala | 12 ++-- .../call_window/video_settings_popover.vala | 12 ++-- plugins/rtp/src/device.vala | 15 +++-- plugins/rtp/src/plugin.vala | 23 ++++---- plugins/rtp/src/stream.vala | 16 ++--- plugins/rtp/src/video_widget.vala | 4 +- 9 files changed, 114 insertions(+), 65 deletions(-) diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index b6955a6b..c7c2c375 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -103,16 +103,17 @@ public abstract interface VideoCallPlugin : Object { // Devices public signal void devices_changed(string media, bool incoming); public abstract Gee.List get_devices(string media, bool incoming); - public abstract MediaDevice? get_device(Xmpp.Xep.JingleRtp.Stream stream, bool incoming); - public abstract void set_pause(Xmpp.Xep.JingleRtp.Stream stream, bool pause); - public abstract void set_device(Xmpp.Xep.JingleRtp.Stream stream, MediaDevice? device); + public abstract MediaDevice? get_preferred_device(string media, bool incoming); + public abstract MediaDevice? get_device(Xmpp.Xep.JingleRtp.Stream? stream, bool incoming); + public abstract void set_pause(Xmpp.Xep.JingleRtp.Stream? stream, bool pause); + public abstract void set_device(Xmpp.Xep.JingleRtp.Stream? stream, MediaDevice? device); public abstract void dump_dot(); } public abstract interface VideoCallWidget : Object { public signal void resolution_changed(uint width, uint height); - public abstract void display_stream(Xmpp.Xep.JingleRtp.Stream stream, Jid jid); + public abstract void display_stream(Xmpp.Xep.JingleRtp.Stream? stream, Jid jid); public abstract void display_device(MediaDevice device); public abstract void detach(); } @@ -120,7 +121,10 @@ public abstract interface VideoCallWidget : Object { public abstract interface MediaDevice : Object { public abstract string id { owned get; } public abstract string display_name { owned get; } - public abstract string detail_name { owned get; } + public abstract string? detail_name { owned get; } + + public abstract string? media { owned get; } + public abstract bool incoming { get; } } public abstract interface NotificationPopulator : Object { diff --git a/libdino/src/service/call_state.vala b/libdino/src/service/call_state.vala index c403fc6a..c1f0522d 100644 --- a/libdino/src/service/call_state.vala +++ b/libdino/src/service/call_state.vala @@ -26,6 +26,10 @@ public class Dino.CallState : Object { public HashMap peers = new HashMap(Jid.hash_func, Jid.equals_func); + private Plugins.MediaDevice selected_microphone_device; + private Plugins.MediaDevice selected_speaker_device; + private Plugins.MediaDevice selected_video_device; + public CallState(Call call, StreamInteractor stream_interactor) { this.call = call; this.stream_interactor = stream_interactor; @@ -80,6 +84,15 @@ public class Dino.CallState : Object { peer_joined(peer.jid, peer); } + internal void on_peer_stream_created(PeerState peer, string media) { + if (media == "audio") { + call_plugin.set_device(peer.get_audio_stream(), get_microphone_device()); + call_plugin.set_device(peer.get_audio_stream(), get_speaker_device()); + } else if (media == "video") { + call_plugin.set_device(peer.get_video_stream(), get_video_device()); + } + } + public void accept() { accepted = true; call.state = Call.State.ESTABLISHING; @@ -206,30 +219,57 @@ public class Dino.CallState : Object { } public Plugins.MediaDevice? get_microphone_device() { - if (peers.is_empty) return null; - var audio_stream = peers.values.to_array()[0].get_audio_stream(); - return call_plugin.get_device(audio_stream, false); + if (selected_microphone_device == null) { + if (!peers.is_empty) { + var audio_stream = peers.values.to_array()[0].get_audio_stream(); + selected_microphone_device = call_plugin.get_device(audio_stream, false); + } + if (selected_microphone_device == null) { + selected_microphone_device = call_plugin.get_preferred_device("audio", false); + } + } + return selected_microphone_device; } public Plugins.MediaDevice? get_speaker_device() { - if (peers.is_empty) return null; - var audio_stream = peers.values.to_array()[0].get_audio_stream(); - return call_plugin.get_device(audio_stream, true); + if (selected_speaker_device == null) { + if (!peers.is_empty) { + var audio_stream = peers.values.to_array()[0].get_audio_stream(); + selected_speaker_device = call_plugin.get_device(audio_stream, true); + } + if (selected_speaker_device == null) { + selected_speaker_device = call_plugin.get_preferred_device("audio", true); + } + } + return selected_speaker_device; } public Plugins.MediaDevice? get_video_device() { - if (peers.is_empty) return null; - var video_stream = peers.values.to_array()[0].get_video_stream(); - return call_plugin.get_device(video_stream, false); + if (selected_video_device == null) { + if (!peers.is_empty) { + var video_stream = peers.values.to_array()[0].get_video_stream(); + selected_video_device = call_plugin.get_device(video_stream, false); + } + if (selected_video_device == null) { + selected_video_device = call_plugin.get_preferred_device("video", false); + } + } + return selected_video_device; } public void set_audio_device(Plugins.MediaDevice? device) { + if (device.incoming) { + selected_speaker_device = device; + } else { + selected_microphone_device = device; + } foreach (PeerState peer_state in peers.values) { call_plugin.set_device(peer_state.get_audio_stream(), device); } } public void set_video_device(Plugins.MediaDevice? device) { + selected_video_device = device; foreach (PeerState peer_state in peers.values) { call_plugin.set_device(peer_state.get_video_stream(), device); } @@ -270,6 +310,7 @@ public class Dino.CallState : Object { this.bind_property("we-should-send-video", peer_state, "we-should-send-video", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); this.bind_property("group-call", peer_state, "group-call", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + peer_state.stream_created.connect((peer, media) => { on_peer_stream_created(peer, media); }); peer_state.session_terminated.connect((we_terminated, reason_name, reason_text) => { debug("[%s] Peer left %s: %s %s (%i peers remaining)", call.account.bare_jid.to_string(), reason_text ?? "", reason_name ?? "", peer_state.jid.to_string(), peers.size); handle_peer_left(peer_state, we_terminated, reason_name, reason_text); diff --git a/main/src/ui/call_window/audio_settings_popover.vala b/main/src/ui/call_window/audio_settings_popover.vala index 3b5dff3f..f7e490cd 100644 --- a/main/src/ui/call_window/audio_settings_popover.vala +++ b/main/src/ui/call_window/audio_settings_popover.vala @@ -37,10 +37,6 @@ public class Dino.Ui.AudioSettingsPopover : Gtk.Popover { micro_frame.add(micro_list_box); foreach (Plugins.MediaDevice device in devices) { Label display_name_label = new Label(device.display_name) { xalign=0, visible=true }; - Label detail_name_label = new Label(device.detail_name) { xalign=0, visible=true }; - detail_name_label.get_style_context().add_class("dim-label"); - detail_name_label.attributes = new Pango.AttrList(); - detail_name_label.attributes.insert(Pango.attr_scale_new(0.8)); Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true }; if (current_microphone_device == null || current_microphone_device.id != device.id) { image.opacity = 0; @@ -56,7 +52,13 @@ public class Dino.Ui.AudioSettingsPopover : Gtk.Popover { device_box.add(image); Box label_box = new Box(Orientation.VERTICAL, 0) { visible = true }; label_box.add(display_name_label); - label_box.add(detail_name_label); + if (device.detail_name != null) { + Label detail_name_label = new Label(device.detail_name) { xalign=0, visible=true }; + detail_name_label.get_style_context().add_class("dim-label"); + detail_name_label.attributes = new Pango.AttrList(); + detail_name_label.attributes.insert(Pango.attr_scale_new(0.8)); + label_box.add(detail_name_label); + } device_box.add(label_box); ListBoxRow list_box_row = new ListBoxRow() { visible=true }; list_box_row.add(device_box); @@ -94,10 +96,6 @@ public class Dino.Ui.AudioSettingsPopover : Gtk.Popover { speaker_frame.add(speaker_list_box); foreach (Plugins.MediaDevice device in devices) { Label display_name_label = new Label(device.display_name) { xalign=0, visible=true }; - Label detail_name_label = new Label(device.detail_name) { xalign=0, visible=true }; - detail_name_label.get_style_context().add_class("dim-label"); - detail_name_label.attributes = new Pango.AttrList(); - detail_name_label.attributes.insert(Pango.attr_scale_new(0.8)); Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true }; if (current_speaker_device == null || current_speaker_device.id != device.id) { image.opacity = 0; @@ -113,7 +111,13 @@ public class Dino.Ui.AudioSettingsPopover : Gtk.Popover { device_box.add(image); Box label_box = new Box(Orientation.VERTICAL, 0) { visible = true }; label_box.add(display_name_label); - label_box.add(detail_name_label); + if (device.detail_name != null) { + Label detail_name_label = new Label(device.detail_name) { xalign=0, visible=true }; + detail_name_label.get_style_context().add_class("dim-label"); + detail_name_label.attributes = new Pango.AttrList(); + detail_name_label.attributes.insert(Pango.attr_scale_new(0.8)); + label_box.add(detail_name_label); + } device_box.add(label_box); ListBoxRow list_box_row = new ListBoxRow() { visible=true }; list_box_row.add(device_box); diff --git a/main/src/ui/call_window/call_window_controller.vala b/main/src/ui/call_window/call_window_controller.vala index 445f88ea..e929aedc 100644 --- a/main/src/ui/call_window/call_window_controller.vala +++ b/main/src/ui/call_window/call_window_controller.vala @@ -115,6 +115,9 @@ public class Dino.Ui.CallWindowController : Object { call_window.menu_dump_dot.connect(() => { call_plugin.dump_dot(); }); update_own_video(); + + update_audio_device_choices(); + update_video_device_choices(); } private void invite_button_clicked() { @@ -135,13 +138,6 @@ public class Dino.Ui.CallWindowController : Object { Jid peer_jid = peer_state.jid; peer_states[peer_id] = peer_state; - peer_state.stream_created.connect((media) => { - if (media == "audio") { - update_audio_device_choices(); - } else if (media == "video") { - update_video_device_choices(); - } - }); peer_state.connection_ready.connect(() => { call_window.set_status(peer_state.internal_id, ""); if (participant_widgets.size == 1) { @@ -331,7 +327,7 @@ public class Dino.Ui.CallWindowController : Object { } else { Widget widget = (Widget) own_video; call_window.set_own_video(widget); - own_video.display_device(devices.first()); + own_video.display_device(call_state.get_video_device()); } } else { own_video.detach(); diff --git a/main/src/ui/call_window/video_settings_popover.vala b/main/src/ui/call_window/video_settings_popover.vala index 48ab3cab..7dd5ec9f 100644 --- a/main/src/ui/call_window/video_settings_popover.vala +++ b/main/src/ui/call_window/video_settings_popover.vala @@ -33,10 +33,6 @@ public class Dino.Ui.VideoSettingsPopover : Gtk.Popover { frame.add(list_box); foreach (Plugins.MediaDevice device in devices) { Label display_name_label = new Label(device.display_name) { xalign=0, visible=true }; - Label detail_name_label = new Label(device.detail_name) { xalign=0, visible=true }; - detail_name_label.get_style_context().add_class("dim-label"); - detail_name_label.attributes = new Pango.AttrList(); - detail_name_label.attributes.insert(Pango.attr_scale_new(0.8)); Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true }; if (current_device == null || current_device.id != device.id) { image.opacity = 0; @@ -52,7 +48,13 @@ public class Dino.Ui.VideoSettingsPopover : Gtk.Popover { device_box.add(image); Box label_box = new Box(Orientation.VERTICAL, 0) { visible = true }; label_box.add(display_name_label); - label_box.add(detail_name_label); + if (device.detail_name != null) { + Label detail_name_label = new Label(device.detail_name) { xalign=0, visible=true }; + detail_name_label.get_style_context().add_class("dim-label"); + detail_name_label.attributes = new Pango.AttrList(); + detail_name_label.attributes.insert(Pango.attr_scale_new(0.8)); + label_box.add(detail_name_label); + } device_box.add(label_box); ListBoxRow list_box_row = new ListBoxRow() { visible=true }; list_box_row.add(device_box); diff --git a/plugins/rtp/src/device.vala b/plugins/rtp/src/device.vala index aca97578..d4eca09a 100644 --- a/plugins/rtp/src/device.vala +++ b/plugins/rtp/src/device.vala @@ -18,12 +18,14 @@ 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 { - return device.properties.get_string("alsa.card_name") ?? device.properties.get_string("alsa.name") ?? device.properties.get_string("alsa.id") ?? device.properties.get_string("api.v4l2.cap.card") ?? id; + public string? detail_name { owned get { + 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"); + if (device.properties.has_field("api.v4l2.cap.card")) return device.properties.get_string("api.v4l2.cap.card"); + return null; }} - - public Gst.Pipeline pipe { get { return plugin.pipe; }} - public string? media { get { + public string? media { owned get { if (device.has_classes("Audio")) { return "audio"; } else if (device.has_classes("Video")) { @@ -32,6 +34,9 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object { return null; } }} + public bool incoming { get { return is_sink; } } + + 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")); } } diff --git a/plugins/rtp/src/plugin.vala b/plugins/rtp/src/plugin.vala index 2469a967..3a4f6ce1 100644 --- a/plugins/rtp/src/plugin.vala +++ b/plugins/rtp/src/plugin.vala @@ -388,7 +388,7 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { return fps; } - public Device? get_preferred_device(string media, bool incoming) { + public MediaDevice? get_preferred_device(string media, bool incoming) { Gee.List devices = new ArrayList(); foreach (MediaDevice media_device in get_devices(media, incoming)) { if (media_device is Device) devices.add((Device)media_device); @@ -427,14 +427,11 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { } } - public MediaDevice? get_device(Xmpp.Xep.JingleRtp.Stream stream, bool incoming) { - Stream plugin_stream = stream as Stream; + public MediaDevice? get_device(Xmpp.Xep.JingleRtp.Stream? stream, bool incoming) { + Stream? plugin_stream = stream as Stream?; if (plugin_stream == null) return null; - if (incoming) { - return plugin_stream.output_device ?? get_preferred_device(stream.media, incoming); - } else { - return plugin_stream.input_device ?? get_preferred_device(stream.media, incoming); - } + MediaDevice? device = incoming ? plugin_stream.output_device : plugin_stream.input_device; + return device ?? get_preferred_device(stream.media, incoming); } public void dump_dot() { @@ -444,8 +441,8 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { print(@"Stored pipe details as $name\n"); } - public void set_pause(Xmpp.Xep.JingleRtp.Stream stream, bool pause) { - Stream plugin_stream = stream as Stream; + public void set_pause(Xmpp.Xep.JingleRtp.Stream? stream, bool pause) { + Stream? plugin_stream = stream as Stream?; if (plugin_stream == null) return; if (pause) { plugin_stream.pause(); @@ -454,9 +451,9 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { } } - public void set_device(Xmpp.Xep.JingleRtp.Stream stream, MediaDevice? device) { - Device real_device = device as Device; - Stream plugin_stream = stream as Stream; + public void set_device(Xmpp.Xep.JingleRtp.Stream? stream, MediaDevice? device) { + Device? real_device = device as Device?; + Stream? plugin_stream = stream as Stream?; if (real_device == null || plugin_stream == null) return; if (real_device.is_source) { plugin_stream.input_device = real_device; diff --git a/plugins/rtp/src/stream.vala b/plugins/rtp/src/stream.vala index 6bd92e3f..abdda776 100644 --- a/plugins/rtp/src/stream.vala +++ b/plugins/rtp/src/stream.vala @@ -27,7 +27,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { private Device _input_device; public Device input_device { get { return _input_device; } set { - if (!paused) { + if (sending && !paused) { var input = this.input; set_input(value != null ? value.link_source(payload_type, our_ssrc, next_seqnum_offset, next_timestamp_offset) : null); if (this._input_device != null) this._input_device.unlink(input); @@ -37,7 +37,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { private Device _output_device; public Device output_device { get { return _output_device; } set { if (output != null) remove_output(output); - if (value != null) add_output(value.link_sink()); + if (value != null && receiving) add_output(value.link_sink()); this._output_device = value; }} @@ -74,10 +74,10 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { public void on_senders_changed() { if (sending && input == null) { - input_device = plugin.get_preferred_device(media, false); + input_device = input_device; } if (receiving && output == null) { - output_device = plugin.get_preferred_device(media, true); + output_device = output_device; } } @@ -86,11 +86,11 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { // Create i/o if needed - if (input == null && input_device == null && sending) { - input_device = plugin.get_preferred_device(media, false); + if (input == null && sending) { + input_device = input_device; } - if (output == null && output_device == null && receiving && media == "audio") { - output_device = plugin.get_preferred_device(media, true); + if (output == null && receiving && media == "audio") { + output_device = output_device; } // Create app elements diff --git a/plugins/rtp/src/video_widget.vala b/plugins/rtp/src/video_widget.vala index 664d45ee..82693b09 100644 --- a/plugins/rtp/src/video_widget.vala +++ b/plugins/rtp/src/video_widget.vala @@ -130,11 +130,11 @@ public class Dino.Plugins.Rtp.VideoWidget : Gtk.Bin, Dino.Plugins.VideoCallWidge return false; } - public void display_stream(Xmpp.Xep.JingleRtp.Stream stream, Xmpp.Jid jid) { + public void display_stream(Xmpp.Xep.JingleRtp.Stream? stream, Xmpp.Jid jid) { if (sink == null) return; detach(); if (stream.media != "video") return; - connected_stream = stream as Stream; + connected_stream = stream as Stream?; if (connected_stream == null) return; plugin.pause(); pipe.add(sink);