diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 745871fb3..bbd7ce0ef 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -24,11 +24,13 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { public const string ACTION_FULLSCREEN = "fullscreen"; public const string ACTION_TAKE_PHOTO = "take_photo"; public const string ACTION_RECORD = "record"; + public const string ACTION_CHANGE_CAPS = "change-caps"; private const GLib.ActionEntry[] ACTION_ENTRIES = { {ACTION_FULLSCREEN, on_fullscreen}, {ACTION_TAKE_PHOTO, on_take_photo}, {ACTION_RECORD, on_record, null, "false", null}, + {ACTION_CHANGE_CAPS, on_change_caps, "u", "uint32 0", null}, }; private const string PHOTO_ICON_SYMBOLIC = "view-list-images-symbolic"; @@ -39,6 +41,8 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { private Widgets.CameraView camera_view; private Gtk.Menu camera_options; + private GLib.Menu resolution_menu; + private Gtk.MenuButton resolution_button; private Gtk.Button take_button; private Gtk.Image take_image; private Gtk.Label take_timer_label; @@ -49,6 +53,7 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { private Gtk.MenuButton menu_button; private Gtk.Box linked_box; + private Gst.Device? current_device = null; private bool timer_running = false; public bool recording { get; private set; default = false; } @@ -97,7 +102,6 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { }); var recording_finished_fail_toast = new Granite.Widgets.Toast (_("Recording failed")); - var overlay = new Gtk.Overlay (); overlay.add (camera_view); overlay.add_overlay (recording_finished_toast); @@ -163,23 +167,10 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { mode_switch = new Granite.ModeSwitch.from_icon_name (PHOTO_ICON_SYMBOLIC, VIDEO_ICON_SYMBOLIC) { valign = Gtk.Align.CENTER }; - mode_switch.notify["active"].connect (() => { - if (mode_switch.active) { - Camera.Application.settings.set_enum ("mode", Utils.ActionType.VIDEO); - take_button.action_name = Camera.MainWindow.ACTION_PREFIX + Camera.MainWindow.ACTION_RECORD; - take_image.icon_name = VIDEO_ICON_SYMBOLIC; - timer_button.sensitive = false; - } else { - Camera.Application.settings.set_enum ("mode", Utils.ActionType.PHOTO); - take_button.action_name = Camera.MainWindow.ACTION_PREFIX + Camera.MainWindow.ACTION_TAKE_PHOTO; - take_image.icon_name = PHOTO_ICON_SYMBOLIC; - timer_button.sensitive = true; - } - }); - Camera.Application.settings.changed["mode"].connect ((key) => { - mode_switch.active = Camera.Application.settings.get_enum ("mode") == Utils.ActionType.VIDEO; - }); + + mode_switch.notify["active"].connect_after (on_mode_changed); mode_switch.active = Camera.Application.settings.get_enum ("mode") == Utils.ActionType.VIDEO; + // No need to monitor external changes to own settings as inside sandbox and only changed by MainWindow /* Construct AppMenu */ var mirror_switch = new Granite.SwitchModelButton (_("Mirror")); @@ -268,6 +259,11 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { linked_box.pack_start (take_button); linked_box.pack_start (camera_menu_revealer); + resolution_menu = new GLib.Menu (); + resolution_button = new Gtk.MenuButton () { + image = new Gtk.Image.from_icon_name ("preferences-desktop-display-symbolic", Gtk.IconSize.MENU), + }; + resolution_button.set_menu_model (resolution_menu); /* Pack tools into HeaderBar */ var header_widget = new Gtk.HeaderBar () { show_close_button = true, // Gtk4 -> show_title_buttons = true, @@ -277,11 +273,14 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { header_widget.pack_start (timer_button); header_widget.pack_end (menu_button); header_widget.pack_end (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); + header_widget.pack_end (resolution_button); + header_widget.pack_end (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); header_widget.pack_end (mode_switch); notify["recording"].connect (() => { timer_button.sensitive = !recording && !mode_switch.active; mode_switch.sensitive = !recording; + resolution_button.sensitive = !recording; video_timer_revealer.reveal_child = recording; if (recording) { @@ -331,6 +330,13 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { } } + private void on_change_caps (GLib.SimpleAction action, GLib.Variant? parameter) { + if (parameter != null) { + camera_view.change_caps ((int)parameter.get_uint32 ()); + change_action_state (ACTION_CHANGE_CAPS, parameter); + } + } + public override bool configure_event (Gdk.EventConfigure event) { if (configure_id != 0) { GLib.Source.remove (configure_id); @@ -364,6 +370,7 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { } private void add_camera_option (Gst.Device camera) { + current_device = camera; var menuitem = new Gtk.RadioMenuItem.with_label (null, camera.display_name); menuitem.set_data ("camera", camera); camera_options.append (menuitem); @@ -375,13 +382,16 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { } menuitem.active = true; menuitem.activate.connect (() => { + current_device = menuitem.get_data ("camera"); if (menuitem.active) { - camera_view.change_camera (menuitem.get_data ("camera")); + camera_view.change_camera (current_device); + update_resolution_menu (); } }); menuitem.show (); update_take_button (); + update_resolution_menu (); enable_header (true); } @@ -399,10 +409,78 @@ public class Camera.MainWindow : Hdy.ApplicationWindow { camera_options.remove (to_remove); } + if (camera_options.get_children ().length () > 0) { + camera_options.active = 0; + } + + update_resolution_menu (); update_take_button (); enable_header (camera_options.get_children ().length () > 0); } + private void update_resolution_menu () { + resolution_button.tooltip_text = mode_switch.active ? _("Video capture resolution") : _("Photo capture resolution"); + var caps_index = mode_switch.active ? camera_view.current_video_caps_index : camera_view.current_picture_caps_index; + if (caps_index < 0) { + // Suppress terminal warnings during startup + return; + } + + resolution_menu.remove_all (); + if (current_device == null || current_device.get_caps () == null) { + return; + } + + int prev_w = 0, prev_h = 0; + double prev_fr = 0.0; + var caps = current_device.get_caps (); + for (uint i = 0; i < caps.get_size (); i++) { + unowned var s = caps.get_structure (i); + if (s.get_name () != (mode_switch.active ? "video/x-raw" : "image/jpeg")) { + continue; + } + + int w, h; + double fr = 0.0; + if (Camera.Utils.parse_structure (s, out w, out h, out fr)) { + // Check not duplicate ( for simplicity assume duplicates listed next to each other) + if (w != prev_w || h != prev_h || fr != prev_fr) { + if (mode_switch.active) { // Show framerate for video capture + resolution_menu.append ( + "%d×%d (%0.f fps)".printf (w, h, fr), + GLib.Action.print_detailed_name ("win.change-caps", new GLib.Variant.uint32 (i)) + ); + } else { // Framerate not useful for still image capture + resolution_menu.append ( + "%d×%d".printf (w, h), + GLib.Action.print_detailed_name ("win.change-caps", new GLib.Variant.uint32 (i)) + ); + } + } + + prev_w = w; + prev_h = h; + prev_fr = fr; + } + } + + change_action_state (ACTION_CHANGE_CAPS, new GLib.Variant.uint32 (caps_index)); + } + + private void on_mode_changed () { + camera_view.on_mode_changed (mode_switch.active); + update_resolution_menu (); + if (mode_switch.active) { + take_button.action_name = Camera.MainWindow.ACTION_PREFIX + Camera.MainWindow.ACTION_RECORD; + take_image.icon_name = VIDEO_ICON_SYMBOLIC; + timer_button.sensitive = false; + } else { + take_button.action_name = Camera.MainWindow.ACTION_PREFIX + Camera.MainWindow.ACTION_TAKE_PHOTO; + take_image.icon_name = PHOTO_ICON_SYMBOLIC; + timer_button.sensitive = true; + } + } + private void update_take_button () { unowned Gtk.StyleContext take_button_style_context = take_button.get_style_context (); if (camera_options.get_children ().length () > 1) { diff --git a/src/Utils.vala b/src/Utils.vala index ad3370643..44516cf52 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -56,4 +56,35 @@ namespace Camera.Utils { return GLib.Path.build_path (Path.DIR_SEPARATOR_S, media_directory, "Webcam"); } + + public bool parse_structure (Gst.Structure s, out int width, out int height, out double framerate) { + framerate = 0.0; + int num = 0, den = 1; + if (s.get ("width", typeof (int), out width, + "height", typeof (int), out height)) { + + unowned GLib.Value? fraction = s.get_value ("framerate"); + if (fraction.holds (typeof (Gst.Fraction))) { + num = Gst.Value.get_fraction_numerator (fraction); + den = Gst.Value.get_fraction_denominator (fraction); + } else if (fraction.holds (typeof (Gst.FractionRange))) { + var range_max = Gst.Value.get_fraction_range_max (fraction); + num = Gst.Value.get_fraction_numerator (range_max); + den = Gst.Value.get_fraction_denominator (range_max); + } else if (fraction.holds (typeof (Gst.ValueList))) { + unowned GLib.Value? val = Gst.ValueList.get_value (fraction, 0); + num = Gst.Value.get_fraction_numerator (val); + den = Gst.Value.get_fraction_denominator (val); + } else { + warning ("Unknown fraction type: %s", fraction.type_name ()); + return false; + } + } else { + warning ("no resolution in caps"); + return false; + } + + framerate = (double)num / (double)den; + return true; + } } diff --git a/src/Widgets/CameraView.vala b/src/Widgets/CameraView.vala index 0b3fe200e..3974060eb 100644 --- a/src/Widgets/CameraView.vala +++ b/src/Widgets/CameraView.vala @@ -1,4 +1,4 @@ -/* + /* * Copyright (c) 2011-2019 elementary, inc. (https://elementary.io) * * This program is free software; you can redistribute it and/or @@ -30,12 +30,15 @@ public class Camera.Widgets.CameraView : Gtk.Box { private Gtk.Label status_label; Gtk.Widget gst_video_widget; - private Gst.Pipeline pipeline; - private Gst.Element tee; + private Gst.Pipeline preview_pipeline; private Gst.Video.ColorBalance color_balance; private Gst.Video.Direction? hflip; - private Gst.Bin? record_bin; private Gst.Device? current_device = null; + + private int default_video_caps_index = -1; + public int current_video_caps_index { get; private set; default = -1; } + public int current_picture_caps_index { get; private set; default = -1; } + private uint init_device_timeout_id = 0; public uint n_cameras { @@ -44,9 +47,6 @@ public class Camera.Widgets.CameraView : Gtk.Box { } } - private int picture_width; - private int picture_height; - private Gst.DeviceMonitor monitor = new Gst.DeviceMonitor (); public bool recording { get; private set; default = false; } public bool horizontal_flip { @@ -112,6 +112,16 @@ public class Camera.Widgets.CameraView : Gtk.Box { }); } + public void on_mode_changed (bool is_video) { + if (current_device == null) { + return; + } else if (is_video) { + set_preview_caps (current_video_caps_index); + } else { + set_preview_caps (default_video_caps_index); + } + } + private void on_camera_added (Gst.Device device) { if (init_device_timeout_id > 0) { Source.remove (init_device_timeout_id); @@ -120,6 +130,7 @@ public class Camera.Widgets.CameraView : Gtk.Box { camera_added (device); change_camera (device); } + private void on_camera_removed (Gst.Device device) { camera_removed (device); if (n_cameras == 0) { @@ -173,46 +184,52 @@ public class Camera.Widgets.CameraView : Gtk.Box { stop_recording (); } - if (record_bin != null) { - record_bin.set_state (Gst.State.NULL); - record_bin.sync_state_with_parent (); - record_bin.sync_children_states (); - } - - if (pipeline != null) { - pipeline.set_state (Gst.State.NULL); - pipeline.sync_children_states (); + if (preview_pipeline != null) { + preview_pipeline.set_state (Gst.State.NULL); + preview_pipeline.sync_children_states (); - Gst.Debug.BIN_TO_DOT_FILE (pipeline, Gst.DebugGraphDetails.VERBOSE, "changing"); + Gst.Debug.BIN_TO_DOT_FILE (preview_pipeline, Gst.DebugGraphDetails.VERBOSE, "changing"); } - create_pipeline (camera); - current_device = camera; - } - - private void create_pipeline (Gst.Device camera) { - try { - var caps = camera.get_caps (); - picture_width = 640; - picture_height = 480; - var max_area = picture_width * picture_height; + var caps = camera.get_caps (); + int largest_picture_index = -1; + int largest_video_index = -1; + var max_area_picture = 640 * 480; + var max_area_video = 320 * 240; - for (uint i = 0; i < caps.get_size (); i++) { + if (caps != null) { + for (int i = 0; i < caps.get_size (); i++) { unowned var s = caps.get_structure (i); - if (s.get_name () == "image/jpeg") { - int w, h; - s.get_int ("width", out w); - s.get_int ("height", out h); - if (w * h > max_area) { - picture_width = w; - picture_height = h; - max_area = w * h; + int w, h; + double fr = 0.0; + if (Camera.Utils.parse_structure (s, out w, out h, out fr)) { + if (s.get_name () == "image/jpeg") { + if (w * h > max_area_picture) { + largest_picture_index = i; + max_area_picture = w * h; + } + } else if (s.get_name () == "video/x-raw") { + if (w * h > max_area_video && fr >= 10.0) { + largest_video_index = i; + max_area_video = w * h; + } } } } + } - var device_src = camera.create_element (VIDEO_SRC_NAME); - pipeline = (Gst.Pipeline) Gst.parse_launch ( + current_picture_caps_index = largest_picture_index; + current_video_caps_index = largest_video_index; + default_video_caps_index = largest_video_index; + current_device = camera; + create_video_pipeline (); + } + + private void create_video_pipeline () { + try { + var device_src = current_device.create_element (VIDEO_SRC_NAME); + preview_pipeline = (Gst.Pipeline) Gst.parse_launch ( + "capsfilter name=capsfilter ! " + "decodebin name=decodebin ! " + "videoflip method=horizontal-flip name=hflip ! " + "videobalance name=balance ! " + @@ -223,17 +240,35 @@ public class Camera.Widgets.CameraView : Gtk.Box { "videoscale name=videoscale" ); - pipeline.add (device_src); - device_src.link (pipeline.get_by_name ("decodebin")); - tee = pipeline.get_by_name ("tee"); - hflip = (pipeline.get_by_name ("hflip") as Gst.Video.Direction); - color_balance = (pipeline.get_by_name ("balance") as Gst.Video.ColorBalance); + preview_pipeline.add (device_src); + dynamic Gst.Element capsfilter = preview_pipeline.get_by_name ("capsfilter"); + device_src.link (capsfilter); + + // Ensure action state changes with caps filter + capsfilter.get_static_pad ("src").add_probe (Gst.PadProbeType.EVENT_BOTH, (pad, info) => { + unowned Gst.Event? event = info.get_event (); + if (event.type == Gst.EventType.CAPS) { + //TODO Find correct resolution index and update action state. + } + + return Gst.PadProbeReturn.OK; + }); + + var default_caps = get_caps_from_index (default_video_caps_index); + if (default_caps != null) { + capsfilter.caps = default_caps; + } else { + critical ("Could not find default caps from index %u", default_video_caps_index); + } + + hflip = (preview_pipeline.get_by_name ("hflip") as Gst.Video.Direction); + color_balance = (preview_pipeline.get_by_name ("balance") as Gst.Video.ColorBalance); if (gst_video_widget != null) { main_widget.remove (gst_video_widget); } - dynamic Gst.Element videorate = pipeline.get_by_name ("videorate"); + dynamic Gst.Element videorate = preview_pipeline.get_by_name ("videorate"); videorate.max_rate = 30; videorate.drop_only = true; @@ -241,21 +276,20 @@ public class Camera.Widgets.CameraView : Gtk.Box { if (gtksink != null) { dynamic Gst.Element glsinkbin = Gst.ElementFactory.make ("glsinkbin", null); glsinkbin.sink = gtksink; - pipeline.add (glsinkbin); - pipeline.get_by_name ("videoscale").link (glsinkbin); + preview_pipeline.add (glsinkbin); + preview_pipeline.get_by_name ("videoscale").link (glsinkbin); } else { gtksink = Gst.ElementFactory.make ("gtksink", null); - pipeline.add (gtksink); - pipeline.get_by_name ("videoscale").link (gtksink); + preview_pipeline.add (gtksink); + preview_pipeline.get_by_name ("videoscale").link (gtksink); } gst_video_widget = gtksink.widget; - main_widget.add (gst_video_widget); // must be add_child for GTK4 gst_video_widget.show (); main_widget.visible_child = gst_video_widget; - pipeline.set_state (Gst.State.PLAYING); + preview_pipeline.set_state (Gst.State.PLAYING); } catch (Error e) { // It is possible that there is another camera present that could selected so do not show // no_device_view @@ -264,50 +298,91 @@ public class Camera.Widgets.CameraView : Gtk.Box { dialog.destroy (); } } - - public void change_color_balance (double brightnesss, double contrast) { - color_balance.set_property ("brightness", brightnesss); - color_balance.set_property ("contrast", contrast); - } - - public void take_photo () { - if (recording || pipeline == null) { - return; - } - - recording = true; - pipeline.set_state (Gst.State.NULL); - pipeline.sync_children_states (); - - var preview_video_src = (Gst.Element) pipeline.get_by_name (VIDEO_SRC_NAME); - string device_path; - preview_video_src.get ("device", out device_path); + private Gst.Pipeline? create_picture_pipeline () { var brightness_value = GLib.Value (typeof (double)); color_balance.get_property ("brightness", ref brightness_value); var contrast_value = GLib.Value (typeof (double)); color_balance.get_property ("contrast", ref contrast_value); - Gst.Pipeline picture_pipeline; + Gst.Pipeline? picture_pipeline = null; + + var preview_video_src = (Gst.Element) preview_pipeline.get_by_name (VIDEO_SRC_NAME); + string device_path; + preview_video_src.get ("device", out device_path); + try { - picture_pipeline = (Gst.Pipeline) Gst.parse_launch ( - "v4l2src device=%s name=%s num-buffers=1 !".printf (device_path, VIDEO_SRC_NAME) + - "videoscale ! video/x-raw, width=%d, height=%d !".printf (picture_width, picture_height) + - "videoflip method=%s !".printf ((horizontal_flip)?"horizontal-flip":"none") + + picture_pipeline = (Gst.Pipeline) Gst.parse_launch ( + VIDEO_SRC_NAME + " device=%s name=%s num-buffers=1 !".printf (device_path, VIDEO_SRC_NAME) + + "capsfilter name=capsfilter ! " + + "decodebin name=decodebin ! " + + "videoflip method=%s !".printf (horizontal_flip ? "horizontal-flip" : "none") + "videobalance brightness=%f contrast=%f !".printf (brightness_value.get_double (), contrast_value.get_double ()) + "jpegenc ! filesink location=%s name=filesink".printf (Camera.Utils.get_new_media_filename (Camera.Utils.ActionType.PHOTO)) ); + dynamic Gst.Element capsfilter = picture_pipeline.get_by_name ("capsfilter"); + picture_pipeline.get_by_name (VIDEO_SRC_NAME).link (capsfilter); + var current_picture_caps = get_caps_from_index (current_picture_caps_index); + if (current_picture_caps != null) { + capsfilter.caps = current_picture_caps; + } } catch (Error e) { - warning ("Could not make picture pipeline for photo - %s", e.message); + // It is possible that there is another camera present that could selected so do not show + // no_device_view + var dialog = new Granite.MessageDialog.with_image_from_icon_name (_("Unable To View Camera"), e.message, "dialog-error"); + dialog.run (); + dialog.destroy (); + } + + return picture_pipeline; + } + + public void change_color_balance (double brightnesss, double contrast) { + color_balance.set_property ("brightness", brightnesss); + color_balance.set_property ("contrast", contrast); + } + + public void change_caps (int index) { + var new_caps = get_caps_from_index (index); + if (new_caps != null) { + unowned var s = new_caps.get_structure (0); + if (s.get_name () == "image/jpeg") { + current_picture_caps_index = index; + } else if (s.get_name () == "video/x-raw") { + current_video_caps_index = index; + dynamic Gst.Element capsfilter = preview_pipeline.get_by_name ("capsfilter"); + capsfilter.caps = new_caps; + } + } else { + critical ("Requested caps index not found %u", index); + } + } + + private void set_preview_caps (int index) { + var new_caps = get_caps_from_index (index); + if (new_caps != null) { + dynamic Gst.Element capsfilter = preview_pipeline.get_by_name ("capsfilter"); + capsfilter.caps = new_caps; + } else { + critical ("Failed to set preview caps index %i", index); + } + } + + public void take_photo () { + if (recording || preview_pipeline == null) { return; } + recording = true; + preview_pipeline.set_state (Gst.State.NULL); + preview_pipeline.sync_children_states (); + var picture_pipeline = create_picture_pipeline (); var filesink = picture_pipeline.get_by_name ("filesink"); filesink.get_static_pad ("sink").add_probe (Gst.PadProbeType.EVENT_DOWNSTREAM, (pad, info) => { if (info.get_event ().type == Gst.EventType.EOS) { Idle.add (() => { picture_pipeline.set_state (Gst.State.NULL); play_shutter_sound (); - create_pipeline (current_device); + create_video_pipeline (); recording = false; return Source.REMOVE; @@ -322,7 +397,7 @@ public class Camera.Widgets.CameraView : Gtk.Box { picture_pipeline.set_state (Gst.State.PLAYING); picture_pipeline.sync_children_states (); - Gst.Debug.BIN_TO_DOT_FILE (pipeline, Gst.DebugGraphDetails.VERBOSE, "snapshot"); + Gst.Debug.BIN_TO_DOT_FILE (picture_pipeline, Gst.DebugGraphDetails.VERBOSE, "snapshot"); } public void start_recording () { @@ -331,7 +406,8 @@ public class Camera.Widgets.CameraView : Gtk.Box { } recording = true; - record_bin = new Gst.Bin (null); + var record_bin = new Gst.Bin (null); + record_bin.set_name ("record_bin"); string[] missing_messages = {}; var queue = Gst.ElementFactory.make ("queue", null); @@ -389,12 +465,13 @@ public class Camera.Widgets.CameraView : Gtk.Box { var ghostpad = new Gst.GhostPad (null, queue.get_static_pad ("sink")); record_bin.add_pad (ghostpad); - pipeline.set_state (Gst.State.PAUSED); - pipeline.add (record_bin); + preview_pipeline.set_state (Gst.State.PAUSED); + preview_pipeline.add (record_bin); record_bin.sync_state_with_parent (); - tee.link (record_bin); - pipeline.set_state (Gst.State.PLAYING); - Gst.Debug.BIN_TO_DOT_FILE (pipeline, Gst.DebugGraphDetails.VERBOSE, "recording"); + preview_pipeline.get_by_name ("tee").link (record_bin); + preview_pipeline.set_state (Gst.State.PLAYING); + + Gst.Debug.BIN_TO_DOT_FILE (preview_pipeline, Gst.DebugGraphDetails.VERBOSE, "recording"); } public void stop_recording () { @@ -402,10 +479,9 @@ public class Camera.Widgets.CameraView : Gtk.Box { return; } - pipeline.set_state (Gst.State.PAUSED); - tee.unlink (record_bin); + preview_pipeline.set_state (Gst.State.PAUSED); + var record_bin = (Gst.Bin?)(preview_pipeline.get_by_name ("record_bin")); var filesink = record_bin.get_by_name ("filesink"); - if (filesink != null) { var locationval = GLib.Value (typeof (string)); filesink.get_property ("location", ref locationval); @@ -413,11 +489,24 @@ public class Camera.Widgets.CameraView : Gtk.Box { recording_finished (location); } - pipeline.remove (record_bin); - pipeline.set_state (Gst.State.PLAYING); + preview_pipeline.get_by_name ("tee").unlink (record_bin); + preview_pipeline.remove (record_bin); record_bin.set_state (Gst.State.NULL); - record_bin.dispose (); + record_bin.dispose (); // Required for successful saving of video file recording = false; + preview_pipeline.set_state (Gst.State.PLAYING); + } + + private Gst.Caps? get_caps_from_index (int index) { + var caps = current_device.get_caps (); + if (caps != null && index >= 0 && index < caps.get_size ()) { + unowned var s = caps.get_structure (index); + var new_caps = new Gst.Caps.empty (); + new_caps.append_structure (s.copy ()); + return new_caps; + } else { + return null; + } } private static void play_shutter_sound () {