From 43ea088f646a8b3a5c41699f48cf5f0b4e7d4107 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 8 Feb 2022 21:57:48 +0100 Subject: [PATCH] Calls: Device picker --- libdino/src/plugin/interfaces.vala | 6 +- .../call_window/audio_settings_popover.vala | 30 +++- .../call_window/call_window_controller.vala | 20 +-- .../call_window/video_settings_popover.vala | 15 +- plugins/rtp/src/device.vala | 28 +++- plugins/rtp/src/plugin.vala | 151 +++++++++--------- plugins/rtp/src/stream.vala | 2 - 7 files changed, 139 insertions(+), 113 deletions(-) diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index 23e64373..b6955a6b 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -118,9 +118,9 @@ public abstract interface VideoCallWidget : Object { } public abstract interface MediaDevice : Object { - public abstract string id { get; } - public abstract string display_name { get; } - public abstract string detail_name { get; } + public abstract string id { owned get; } + public abstract string display_name { owned get; } + public abstract string detail_name { owned get; } } public abstract interface NotificationPopulator : Object { diff --git a/main/src/ui/call_window/audio_settings_popover.vala b/main/src/ui/call_window/audio_settings_popover.vala index feb84f32..f13f346b 100644 --- a/main/src/ui/call_window/audio_settings_popover.vala +++ b/main/src/ui/call_window/audio_settings_popover.vala @@ -26,17 +26,21 @@ public class Dino.Ui.AudioSettingsPopover : Gtk.Popover { Gee.List devices = call_plugin.get_devices("audio", false); Box micro_box = new Box(Orientation.VERTICAL, 10) { visible=true }; - micro_box.add(new Label("" + "Microphones" + "") { use_markup=true, xalign=0, visible=true, can_focus=true /* grab initial focus*/ }); + micro_box.add(new Label("" + _("Microphones") + "") { use_markup=true, xalign=0, visible=true, can_focus=true /* grab initial focus*/ }); if (devices.size == 0) { - micro_box.add(new Label("No microphones found.")); + micro_box.add(new Label(_("No microphones found."))); } else { ListBox micro_list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true }; micro_list_box.set_header_func(listbox_header_func); Frame micro_frame = new Frame(null) { visible=true }; micro_frame.add(micro_list_box); foreach (Plugins.MediaDevice device in devices) { - Label label = new Label(device.display_name) { xalign=0, visible=true }; + 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; @@ -50,7 +54,10 @@ public class Dino.Ui.AudioSettingsPopover : Gtk.Popover { }); Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true }; device_box.add(image); - device_box.add(label); + Box label_box = new Box(Orientation.VERTICAL, 0) { visible = true }; + label_box.add(display_name_label); + 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); micro_list_box.add(list_box_row); @@ -73,10 +80,10 @@ public class Dino.Ui.AudioSettingsPopover : Gtk.Popover { Gee.List devices = call_plugin.get_devices("audio", true); Box speaker_box = new Box(Orientation.VERTICAL, 10) { visible=true }; - speaker_box.add(new Label("" + "Speakers" +"") { use_markup=true, xalign=0, visible=true }); + speaker_box.add(new Label("" + _("Speakers") +"") { use_markup=true, xalign=0, visible=true }); if (devices.size == 0) { - speaker_box.add(new Label("No speakers found.")); + speaker_box.add(new Label(_("No speakers found."))); } else { ListBox speaker_list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true }; speaker_list_box.set_header_func(listbox_header_func); @@ -86,7 +93,11 @@ public class Dino.Ui.AudioSettingsPopover : Gtk.Popover { Frame speaker_frame = new Frame(null) { visible=true }; speaker_frame.add(speaker_list_box); foreach (Plugins.MediaDevice device in devices) { - Label label = new Label(device.display_name) { xalign=0, visible=true }; + 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; @@ -100,7 +111,10 @@ public class Dino.Ui.AudioSettingsPopover : Gtk.Popover { }); Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true }; device_box.add(image); - device_box.add(label); + Box label_box = new Box(Orientation.VERTICAL, 0) { visible = true }; + label_box.add(display_name_label); + 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); speaker_list_box.add(list_box_row); diff --git a/main/src/ui/call_window/call_window_controller.vala b/main/src/ui/call_window/call_window_controller.vala index 94d6e890..e482e3aa 100644 --- a/main/src/ui/call_window/call_window_controller.vala +++ b/main/src/ui/call_window/call_window_controller.vala @@ -271,12 +271,9 @@ public class Dino.Ui.CallWindowController : Object { private void update_audio_device_choices() { if (call_plugin.get_devices("audio", true).size == 0 || call_plugin.get_devices("audio", false).size == 0) { call_window.bottom_bar.show_audio_device_error(); - } /*else if (call_plugin.get_devices("audio", true).size == 1 && call_plugin.get_devices("audio", false).size == 1) { + } else if (call_plugin.get_devices("audio", true).size == 1 && call_plugin.get_devices("audio", false).size == 1) { call_window.bottom_bar.show_audio_device_choices(false); return; - }*/ else { - call_window.bottom_bar.show_video_device_choices(false); - return; } AudioSettingsPopover? audio_settings_popover = call_window.bottom_bar.show_audio_device_choices(true); @@ -290,11 +287,6 @@ public class Dino.Ui.CallWindowController : Object { call_state.set_audio_device(device); update_current_audio_device(audio_settings_popover); }); -// calls.stream_created.connect((call, media) => { -// if (media == "audio") { -// update_current_audio_device(audio_settings_popover); -// } -// }); } private void update_current_audio_device(AudioSettingsPopover audio_settings_popover) { @@ -307,10 +299,7 @@ public class Dino.Ui.CallWindowController : Object { if (device_count == 0) { call_window.bottom_bar.show_video_device_error(); - } /*else if (device_count == 1 || call_state.get_video_device() == null) { - call_window.bottom_bar.show_video_device_choices(false); - return; - }*/ else { + } else if (device_count == 1 || call_state.get_video_device() == null) { call_window.bottom_bar.show_video_device_choices(false); return; } @@ -323,11 +312,6 @@ public class Dino.Ui.CallWindowController : Object { update_current_video_device(video_settings_popover); own_video.display_device(device); }); -// call_state.stream_created.connect((call, media) => { -// if (media == "video") { -// update_current_video_device(video_settings_popover); -// } -// }); } private void update_current_video_device(VideoSettingsPopover video_settings_popover) { diff --git a/main/src/ui/call_window/video_settings_popover.vala b/main/src/ui/call_window/video_settings_popover.vala index 332bd917..553fc270 100644 --- a/main/src/ui/call_window/video_settings_popover.vala +++ b/main/src/ui/call_window/video_settings_popover.vala @@ -22,17 +22,21 @@ public class Dino.Ui.VideoSettingsPopover : Gtk.Popover { Gee.List devices = call_plugin.get_devices("video", false); Box camera_box = new Box(Orientation.VERTICAL, 10) { visible=true }; - camera_box.add(new Label("" + "Cameras" + "") { use_markup=true, xalign=0, visible=true, can_focus=true /* grab initial focus*/ }); + camera_box.add(new Label("" + _("Cameras") + "") { use_markup=true, xalign=0, visible=true, can_focus=true /* grab initial focus*/ }); if (devices.size == 0) { - camera_box.add(new Label("No cameras found.") { visible=true }); + camera_box.add(new Label(_("No cameras found.")) { visible=true }); } else { ListBox list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true }; list_box.set_header_func(listbox_header_func); Frame frame = new Frame(null) { visible=true }; frame.add(list_box); foreach (Plugins.MediaDevice device in devices) { - Label label = new Label(device.display_name) { xalign=0, visible=true }; + 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; @@ -46,7 +50,10 @@ public class Dino.Ui.VideoSettingsPopover : Gtk.Popover { }); Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true }; device_box.add(image); - device_box.add(label); + Box label_box = new Box(Orientation.VERTICAL, 0) { visible = true }; + label_box.add(display_name_label); + 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); list_box.add(list_box_row); diff --git a/plugins/rtp/src/device.vala b/plugins/rtp/src/device.vala index 97258d0c..e97a0d04 100644 --- a/plugins/rtp/src/device.vala +++ b/plugins/rtp/src/device.vala @@ -1,6 +1,14 @@ using Xmpp.Xep.JingleRtp; using Gee; +public enum Dino.Plugins.Rtp.DeviceProtocol { + OTHER, + PIPEWIRE, + V4L2, + PULSEAUDIO, + ALSA +} + public class Dino.Plugins.Rtp.Device : MediaDevice, Object { private const int[] common_widths = {320, 360, 400, 480, 640, 960, 1280, 1920, 2560, 3840}; @@ -8,10 +16,10 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object { public CodecUtil codec_util { get { return plugin.codec_util; } } public Gst.Device device { get; private set; } - public string id { get { return device_name; }} - public string display_name { get { return device_display_name; }} - public string detail_name { get { - return device.properties.get_string("alsa.card_name") ?? device.properties.get_string("alsa.id") ?? id; + 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 Gst.Pipeline pipe { get { return plugin.pipe; }} @@ -26,6 +34,18 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object { }} 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_default { get { + bool ret; + device.properties.get_boolean("is-default", out ret); + return ret; + }} + public DeviceProtocol protocol { get { + 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; + return DeviceProtocol.OTHER; + }} private string device_name; private string device_display_name; diff --git a/plugins/rtp/src/plugin.vala b/plugins/rtp/src/plugin.vala index 0e519b37..e7ee7117 100644 --- a/plugins/rtp/src/plugin.vala +++ b/plugins/rtp/src/plugin.vala @@ -203,8 +203,6 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { switch (message.type) { case Gst.MessageType.DEVICE_ADDED: message.parse_device_added(out gst_device); - if (gst_device.properties.has_name("pipewire-proplist") && gst_device.has_classes("Audio")) return Source.CONTINUE; - if (gst_device.properties.get_string("device.class") == "monitor") return Source.CONTINUE; if (devices.any_match((it) => it.matches(gst_device))) return Source.CONTINUE; device = new Device(this, gst_device); devices.add(device); @@ -212,16 +210,12 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { #if GST_1_16 case Gst.MessageType.DEVICE_CHANGED: message.parse_device_changed(out gst_device, out old_gst_device); - if (gst_device.properties.has_name("pipewire-proplist") && gst_device.has_classes("Audio")) return Source.CONTINUE; - if (gst_device.properties.get_string("device.class") == "monitor") return Source.CONTINUE; device = devices.first_match((it) => it.matches(old_gst_device)); if (device != null) device.update(gst_device); break; #endif case Gst.MessageType.DEVICE_REMOVED: message.parse_device_removed(out gst_device); - if (gst_device.properties.has_name("pipewire-proplist") && gst_device.has_classes("Audio")) return Source.CONTINUE; - if (gst_device.properties.get_string("device.class") == "monitor") return Source.CONTINUE; device = devices.first_match((it) => it.matches(gst_device)); if (device != null) devices.remove(device); break; @@ -310,46 +304,42 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { } public Gee.List get_devices(string media, bool incoming) { - + Gee.List devices; if (media == "video" && !incoming) { - return get_video_sources(); + devices = get_video_sources(); + } else if (media == "audio") { + devices = get_audio_devices(incoming); + } else { + devices = new ArrayList(); + devices.add_all_iterator(this.devices.filter(it => it.media == media && (incoming && it.is_sink || !incoming && it.is_source) && !it.is_monitor)); } + devices.sort((media_left, media_right) => { + return strcmp(media_left.id, media_right.id); + }); + + return devices; + } + + public Gee.List get_audio_devices(bool incoming) { + ArrayList pulse_devices = new ArrayList(); + ArrayList other_devices = new ArrayList(); - ArrayList result = new ArrayList(); foreach (Device device in devices) { - if (device.media == media && (incoming && device.is_sink || !incoming && device.is_source)) { - result.add(device); + if (device.media != "audio") continue; + if (incoming && !device.is_sink || !incoming && !device.is_source) continue; + + // Skip monitors + if (device.is_monitor) continue; + + if (device.protocol == DeviceProtocol.PULSEAUDIO) { + pulse_devices.add(device); + } else { + other_devices.add(device); } } - if (media == "audio") { - // Reorder sources - result.sort((media_left, media_right) => { - Device left = media_left as Device; - Device right = media_right as Device; - if (left == null) return 1; - if (right == null) return -1; - bool left_is_pipewire = left.device.properties.has_name("pipewire-proplist"); - bool right_is_pipewire = right.device.properties.has_name("pipewire-proplist"); - - bool left_is_default = false; - left.device.properties.get_boolean("is-default", out left_is_default); - bool right_is_default = false; - right.device.properties.get_boolean("is-default", out right_is_default); - - // Prefer pipewire - if (left_is_pipewire && !right_is_pipewire) return -1; - if (right_is_pipewire && !left_is_pipewire) return 1; - - // Prefer pulse audio default device - if (left_is_default && !right_is_default) return -1; - if (right_is_default && !left_is_default) return 1; - - - return 0; - }); - } - return result; + // If we have any pulseaudio devices, present only those. Don't want duplicated devices from pipewire and pulseaudio. + return pulse_devices.size > 0 ? pulse_devices : other_devices; } public Gee.List get_video_sources() { @@ -371,7 +361,10 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { // Don't allow grey-scale devices if (!is_color) continue; - if (device.device.properties.has_name("pipewire-proplist")) { + // Skip monitors + if (device.is_monitor) continue; + + if (device.protocol == DeviceProtocol.PIPEWIRE) { pipewire_devices.add(device); } else { other_devices.add(device); @@ -379,46 +372,56 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object { } // If we have any pipewire devices, present only those. Don't want duplicated devices from pipewire and video for linux. - ArrayList devices = pipewire_devices.size > 0 ? pipewire_devices : other_devices; + return pipewire_devices.size > 0 ? pipewire_devices : other_devices; + } - // Reorder sources - devices.sort((media_left, media_right) => { - Device left = media_left as Device; - Device right = media_right as Device; - if (left == null) return 1; - if (right == null) return -1; - - int left_fps = 0; - for (int i = 0; i < left.device.caps.get_size(); i++) { - unowned Gst.Structure structure = left.device.caps.get_structure(i); - int num = 0, den = 0; - if (structure.has_field("framerate") && structure.get_fraction("framerate", out num, out den)) left_fps = int.max(left_fps, num / den); - } - - int right_fps = 0; - for (int i = 0; i < left.device.caps.get_size(); i++) { - unowned Gst.Structure structure = left.device.caps.get_structure(i); - int num = 0, den = 0; - if (structure.has_field("framerate") && structure.get_fraction("framerate", out num, out den)) right_fps = int.max(right_fps, num / den); - } - - // More FPS is better - if (left_fps > right_fps) return -1; - if (right_fps > left_fps) return 1; - - return 0; - }); - - return devices; + private int get_max_fps(Device device) { + int fps = 0; + for (int i = 0; i < device.device.caps.get_size(); i++) { + unowned Gst.Structure structure = device.device.caps.get_structure(i); + int num = 0, den = 0; + if (structure.has_field("framerate") && structure.get_fraction("framerate", out num, out den)) fps = int.max(fps, num / den); + } + return fps; } public Device? get_preferred_device(string media, bool incoming) { + Gee.List devices = new ArrayList(); foreach (MediaDevice media_device in get_devices(media, incoming)) { - Device? device = media_device as Device; - if (device != null) return device; + if (media_device is Device) devices.add((Device)media_device); + } + if (devices.is_empty) { + warning("No preferred device for %s %s. Media will not be processed.", incoming ? "incoming" : "outgoing", media); + return null; + } + + // Take default if present + foreach (Device device in devices) { + if (device.is_default) { + debug("Using %s for %s %s as it's default", device.display_name, incoming ? "incoming" : "outgoing", media); + return device; + } + } + + if (media == "video") { + // Pick best FPS + int max_fps = 0; + Device? max_fps_device = null; + foreach (Device device in devices) { + int fps = get_max_fps(device); + if (fps > max_fps) { + max_fps = fps; + max_fps_device = device; + } + } + debug("Using %s for %s %s as it has max FPS (%d)", max_fps_device.display_name, incoming ? "incoming" : "outgoing", media, max_fps); + return max_fps_device; + } else { + // Pick any + Device? device = devices.first(); + debug("Using %s for %s %s as it's first pick", device.display_name, incoming ? "incoming" : "outgoing", media); + return device; } - warning("No preferred device for %s %s. Media will not be processed.", incoming ? "incoming" : "outgoing", media); - return null; } public MediaDevice? get_device(Xmpp.Xep.JingleRtp.Stream stream, bool incoming) { diff --git a/plugins/rtp/src/stream.vala b/plugins/rtp/src/stream.vala index dc712b61..2a236533 100644 --- a/plugins/rtp/src/stream.vala +++ b/plugins/rtp/src/stream.vala @@ -318,8 +318,6 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream { } if (our_ssrc != buffer_ssrc) { warning("Sending RTP %s buffer seq %u with SSRC %u when our ssrc is %u", media, buffer_seq, buffer_ssrc, our_ssrc); - } else { - debug("Sending RTP %s buffer seq %u with SSRC %u", media, buffer_seq, buffer_ssrc); } }