Signed-off-by: Vasiliy Doylov <nekocwd@mainlining.org>
This commit is contained in:
Vasiliy Doylov 2025-06-11 18:06:41 +03:00
parent 06406bb455
commit ed2e965f9c
Signed by: NekoCWD
GPG key ID: B7BE22D44474A582
12 changed files with 298 additions and 204 deletions

View file

@ -1,44 +0,0 @@
{
"id": "io.gitlab.nekocwd.eyeneko",
"runtime": "org.gnome.Platform",
"runtime-version": "master",
"sdk": "org.gnome.Sdk",
"sdk-extensions": ["org.freedesktop.Sdk.Extension.vala"],
"command": "eyeneko",
"finish-args": [
"--share=network",
"--share=ipc",
"--socket=fallback-x11",
"--device=dri",
"--socket=wayland"
],
"build-options": {
"append-path": "/usr/lib/sdk/vala/bin",
"prepend-ld-library-path": "/usr/lib/sdk/vala/lib"
},
"cleanup": [
"/include",
"/lib/pkgconfig",
"/man",
"/share/doc",
"/share/gtk-doc",
"/share/man",
"/share/pkgconfig",
"/share/vala",
"*.la",
"*.a"
],
"modules": [
{
"name": "eyeneko",
"builddir": true,
"buildsystem": "meson",
"sources": [
{
"type": "git",
"url": "file:///home/neko/Projects"
}
]
}
]
}

View file

@ -29,7 +29,7 @@ public class AutoFocus : Object {
public void adjust_step (double prev_score) {
// message ("%lf < %lf < %lf : %lf %lf / %lf", best_lens_pos, prev_pos, max_pos, step, prev_score, best_score);
if (fc < Environment.get_variable ("FRAME_DELAY").to_int ()) {
if (fc < EyeNeko.Env.get_variable_or ("FRAME_DELAY", "3").to_int ()) {
fc++;
return;
} else
@ -51,10 +51,16 @@ public class AutoFocus : Object {
break;
case 20:
message ("meow on 2");
step = 0.1;
step = 0.5;
set_diff (2);
set_camfocus (prev_pos);
break;
case 5:
message ("meow on 0.5");
step = 0.1;
set_diff (1);
set_camfocus (prev_pos);
break;
case 1:
message ("meow on 0.1");
step = 10;
@ -84,4 +90,13 @@ public class AutoFocus : Object {
prev_pos += step;
set_camfocus (prev_pos);
}
private static AutoFocus _instance = null;
public static AutoFocus instance {
get {
if (_instance == null)
_instance = new AutoFocus ();
return _instance;
}
}
}

View file

@ -39,10 +39,14 @@ public class EyeNeko.Elements.BinBase : Gst.Bin {
});
}
protected void add_pads(Gst.Element sink, Gst.Element src) {
sinkpad = new Gst.GhostPad("sink", sink.get_static_pad("sink"));
add_pad(sinkpad);
srcpad = new Gst.GhostPad("src", src.get_static_pad("src"));
add_pad(srcpad);
protected void add_pads(Gst.Element? sink, Gst.Element? src) {
if (sink != null) {
sinkpad = new Gst.GhostPad("sink", sink.get_static_pad("sink"));
add_pad(sinkpad);
}
if (src != null) {
srcpad = new Gst.GhostPad("src", src.get_static_pad("src"));
add_pad(srcpad);
}
}
}

View file

@ -0,0 +1,46 @@
/* downscale.vala
*
* Copyright 2025 Vasiliy Doylov <nekocwd@mainlining.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
public class EyeNeko.Elements.CameraSrc : BinBase {
public Gst.Element source = null;
public Gst.Element capsfilter = Gst.ElementFactory.make ("capsfilter", "capsfilter");
public Gst.Element decodebin = Gst.ElementFactory.make ("decodebin3", "decoder");
public Gst.Element identity = Gst.ElementFactory.make ("identity");
static construct {
set_static_metadata ("camerasrc",
"Source",
"Camera source",
"nekocwd@mainlining.org");
}
public CameraSrc (Gst.Element src, Gst.Caps caps = new Gst.Caps.any ()) {
source = src;
add_many (source, capsfilter, decodebin, identity);
source.link_many (capsfilter, decodebin);
capsfilter.set_property ("caps", caps);
decodebin.pad_added.connect ((pad) => {
if (pad.get_stream () != null && pad.get_stream ().stream_type == Gst.StreamType.VIDEO) {
pad.link (identity.get_static_pad ("sink"));
}
});
add_pads (null, identity);
}
}

View file

@ -1,6 +1,8 @@
public class EyeNeko.Elements.FocusAnalyze : BinBase {
public Gst.Element tee = Gst.ElementFactory.make ("tee");
public Gst.Element queue_analyze = Gst.ElementFactory.make ("queue");
public Gst.Element videocrop = Gst.ElementFactory.make ("videocrop");
public Gst.Element glupload = Gst.ElementFactory.make ("glupload");
public Gst.Element laplacian = Gst.ElementFactory.make ("gleffects_laplacian");
public Gst.Element gldownload = Gst.ElementFactory.make ("gldownload");
public Gst.Element videoconvert = Gst.ElementFactory.make ("videoconvert");
@ -10,10 +12,23 @@ public class EyeNeko.Elements.FocusAnalyze : BinBase {
public Gst.Element queue_passthrough = Gst.ElementFactory.make ("queue");
public double focus_mark { get; private set; }
public const int window_h = 256;
public const int window_w = 256;
public double window_wpos { get; set; default = 0.1; }
public double window_hpos { get; set; default = 0.1; }
public int width = 0;
public int height = 0;
public enum Orientation {
ROTATE0,
ROTATE90,
ROTATE180,
ROTATE270
}
Orientation orientation = Orientation.ROTATE0;
public Gdk.Paintable paintable {
owned get {
Gdk.Paintable paintable;
Gdk.Paintable paintable = null;
gtk4paintablesink.get ("paintable", out paintable);
return paintable;
}
@ -26,6 +41,7 @@ public class EyeNeko.Elements.FocusAnalyze : BinBase {
double val;
message.get_structure ().get_double ("luma-average", out val);
focus_mark = val;
AutoFocus.instance.adjust_step (focus_mark);
}
break;
default:
@ -41,14 +57,105 @@ public class EyeNeko.Elements.FocusAnalyze : BinBase {
"Focus analyze video filter.",
"nekocwd@mainlining.org");
}
public void set_window () {
var wpos = window_wpos;
var hpos = window_hpos;
switch (orientation) {
case Orientation.ROTATE90:
wpos = window_hpos;
hpos = 1 - window_wpos;
break;
case Orientation.ROTATE180:
wpos = 1 - window_wpos;
hpos = 1 - window_hpos;
break;
case Orientation.ROTATE270:
wpos = 1 - window_hpos;
hpos = window_wpos;
break;
default:
break;
}
int left = (int) (width * wpos - window_w / 2);
left = int.min (width - window_w, int.max (left, 0));
left = int.max (0, left);
int right = width - left - window_w;
right = int.max (0, right);
int top = (int) (height * hpos - window_h / 2);
top = int.min (height - window_h, int.max (top, 0));
top = int.max (0, top);
int bottom = height - top - window_h;
bottom = int.max (0, bottom);
videocrop.set_property ("left", left);
videocrop.set_property ("right", right);
videocrop.set_property ("top", top);
videocrop.set_property ("bottom", bottom);
message ("l=%d r=%d t=%d b=%d (%dx%d)", left, right, top, bottom, width - left - right, height - top - bottom);
}
static bool sink_event (Gst.Pad pad, Gst.Object? parent, owned Gst.Event event) {
var filter = (FocusAnalyze) parent;
if (event.type == Gst.EventType.TAG) {
Gst.TagList taglist;
event.parse_tag (out taglist);
string orientation;
taglist.get_string ("image-orientation", out orientation);
if (orientation != null) {
switch (orientation) {
case "rotate-0" :
filter.orientation = Orientation.ROTATE0;
break;
case "rotate-90":
filter.orientation = Orientation.ROTATE90;
break;
case "rotate-180":
filter.orientation = Orientation.ROTATE180;
break;
case "rotate-270":
filter.orientation = Orientation.ROTATE270;
break;
default:
break;
}
message ("%s", orientation);
}
}
if (event.type == Gst.EventType.CAPS) {
message ("Caps");
Gst.Caps caps;
event.parse_caps (out caps);
var info = new Gst.Video.Info.with_caps (caps);
filter.width = info.width;
filter.height = info.height;
filter.set_window ();
}
if (event.type == Gst.EventType.STREAM_START) {
message ("Stream-Collection");
filter.width = 0;
filter.height = 0;
filter.set_window ();
}
return pad.event_default (parent, event);
}
construct {
add_many (tee, queue_analyze, laplacian, gldownload, videoconvert, videoanalyze, videoconvert2, gtk4paintablesink, queue_passthrough);
tee.link_many (queue_analyze, laplacian, gldownload, videoconvert, videoanalyze, videoconvert2, gtk4paintablesink);
add_many (tee, queue_analyze, videocrop, glupload, laplacian, gldownload, videoconvert, videoanalyze, videoconvert2, gtk4paintablesink, queue_passthrough);
tee.link_many (queue_passthrough);
add_pads (tee, queue_passthrough);
tee.link_many (queue_analyze, videocrop, glupload, laplacian, gldownload, videoconvert, videoanalyze, videoconvert2, gtk4paintablesink);
sinkpad.set_event_function (sink_event, null, null);
var bus = new Gst.Bus ();
videoanalyze.set_bus (bus);
bus.add_watch (0, bus_callback);
notify["window-wpos"].connect (set_window);
notify["window-hpos"].connect (set_window);
}
}

View file

@ -1,16 +1,41 @@
public class EyeNeko.Gstreamer : Object {
public Gst.Element viewfinder;
private Gst.Bin camerabin;
public enum CameraBinMode {
VIDEO = 2,
PHOTO = 1,
}
// Public properties
public bool ready { get; set; default = false; }
public CameraBinMode camerabin_mode { get; set; default = CameraBinMode.PHOTO; }
public Gdk.Paintable viewfinder_paintable {
owned get {
Gdk.Paintable paintable;
viewfinder.get ("paintable", out paintable);
return paintable;
}
}
public Gdk.Paintable focus_paintable {
owned get {
return focus_analyze.paintable;
}
}
// Private elements
private Gst.Bin camerabin = (Gst.Bin) Gst.ElementFactory.make ("camerabin", "camerabin");
private Gst.Element viewfinder = Gst.ElementFactory.make ("gtk4paintablesink", "paintablesink");
private Gst.Bin camerasrc_wrapper = (Gst.Bin) Gst.ElementFactory.make ("wrappercamerabinsrc");
public Elements.FocusAnalyze focus_analyze = new Elements.FocusAnalyze ();
private Elements.ColorCorrectionMatrix color_correction_matrix = new Elements.ColorCorrectionMatrix ();
private Elements.Downscale downscale = new Elements.Downscale ();
public Camera current_camera = null;
public int downscale_video_to = int.parse (Env.get_variable_or ("DOWNSCALE_VIDEO", "640"));
public int downscale_photo_to = int.parse (Env.get_variable_or ("DOWNSCALE_PHOTO", "0"));
public delegate int SelectStreamCB (Gst.Element decodebin, Gst.StreamCollection collection, Gst.Stream stream, void* userdata);
Gst.Caps get_best_caps (Gst.Device device) {
@ -23,7 +48,7 @@ public class EyeNeko.Gstreamer : Object {
var caps = device.get_caps ();
for (uint i = 0; i < caps.get_size (); i++) {
var format = caps.get_structure (i).get_name ();
int num, denom, width, height;
int num = 1, denom = 1, width, height;
if (caps.get_structure (i).get_field_type ("width") != typeof (int) || caps.get_structure (i).get_field_type ("height") != typeof (int)) {
warning ("Can't find best caps :(. I hate arrays!");
}
@ -86,13 +111,6 @@ public class EyeNeko.Gstreamer : Object {
message ("Capturing %s", location);
camerabin.set_property ("location", location);
Signal.emit_by_name (camerabin, "start-capture");
var t = new TimeoutSource.seconds (5);
t.set_callback (() => {
Gst.Debug.bin_to_dot_file ((Gst.Bin) camerabin, Gst.DebugGraphDetails.ALL, "Capture");
return false;
});
t.attach ();
}
public void stop_capture () {
@ -114,37 +132,13 @@ public class EyeNeko.Gstreamer : Object {
}
public void start_stream_from (Camera camera) {
current_camera = camera;
var camerasrc = new Gst.Bin ("CameraSRC");
var src = camera.device.create_element ("camerasrc");
var caps = get_best_caps (camera.device);
var capsfilt = Gst.ElementFactory.make ("capsfilter");
capsfilt.set_property ("caps", caps);
var db = Gst.ElementFactory.make ("decodebin3");
var identity = Gst.ElementFactory.make ("identity");
camerasrc.add_many (src, capsfilt, db, identity);
src.link_many (capsfilt, db);
db.pad_added.connect ((pad) => {
if (pad.get_stream () != null && pad.get_stream ().stream_type == Gst.StreamType.VIDEO) {
pad.link (identity.get_static_pad ("sink"));
}
});
camerasrc.add_pad (new Gst.GhostPad ("src", identity.get_static_pad ("src")));
var camerasrc_wrapper = Gst.ElementFactory.make ("wrappercamerabinsrc");
camerasrc_wrapper.set_property ("video-source", camerasrc);
camerasrc_wrapper.set_property ("video-source-filter",
pipe_elements ("Camera filter",
new Elements.Downscale.to (camerabin_mode == CameraBinMode.VIDEO ? downscale_video_to : downscale_photo_to),
new Logic.Filters.CCM ("CCM", true, true).element
));
camerabin.set_property ("camera-source", camerasrc_wrapper);
camerabin.set_state (Gst.State.NULL);
current_camera = camera;
camerasrc_wrapper.set_property
("video-source",
new Elements.CameraSrc (camera.device.create_element (null),
get_best_caps (camera.device)));
camerabin.set_state (Gst.State.PLAYING);
}
@ -159,35 +153,53 @@ public class EyeNeko.Gstreamer : Object {
public void init (ref weak string[] args) {
Gst.init (ref args);
camerabin.set_property ("camera-source", camerasrc_wrapper);
camerabin.set_property ("viewfinder-sink", viewfinder);
color_correction_matrix.red_in_red = 1f;
color_correction_matrix.red_in_blue = 0.1f;
color_correction_matrix.red_in_green = 0.1f;
color_correction_matrix.blue_in_red = 0.1f;
color_correction_matrix.blue_in_blue = 1f;
color_correction_matrix.blue_in_green = 0.1f;
color_correction_matrix.green_in_green = 0.8f;
camerasrc_wrapper.set_property ("video-source-filter",
pipe_elements ("Camera Processing",
focus_analyze,
Gst.ElementFactory.make ("glupload"),
color_correction_matrix,
downscale
));
camerabin.set_property ("image-filter",
pipe_elements ("Image Processing",
Gst.ElementFactory.make ("gldownload")
));
camerabin = (Gst.Bin) Gst.ElementFactory.make ("camerabin", "camerabin");
camerabin.set_property ("video-filter",
pipe_elements ("Video Pipeline",
pipe_elements ("Video Processing",
Gst.ElementFactory.make ("gldownload"),
Gst.ElementFactory.make ("queue"),
Gst.ElementFactory.make ("videoconvert"),
Gst.parse_launch (Env.get_variable_or
(
"VIDEO_ENCODE",
"x264enc tune=zerolatency speed-preset=ultrafast bitrate=8192"
))));
)
)));
camerabin.bind_property ("idle", this, "ready", BindingFlags.SYNC_CREATE);
this.bind_property ("camerabin-mode", camerabin, "mode");
add_enc_profile ();
notify["camerabin-mode"].connect (() => {
downscale.max_size = camerabin_mode == CameraBinMode.VIDEO ? downscale_video_to : downscale_photo_to;
start_stream_from (current_camera);
});
camerabin.bus.add_watch (0, bus_callback);
}
public Gdk.Paintable init_viewfinder () {
Gdk.Paintable paintable;
viewfinder = Gst.ElementFactory.make ("gtk4paintablesink", "paintablesink");
viewfinder.get ("paintable", out paintable);
camerabin.set_property ("viewfinder-sink", viewfinder);
return paintable;
}
public static void add_pads_to_bin (ref Gst.Bin bin, bool src = true, bool sink = true) {
if (src)
bin.add_pad (new Gst.GhostPad ("src", ((Gst.Element) bin.get_child_by_index (0)).get_static_pad ("src")));
@ -201,10 +213,10 @@ public class EyeNeko.Gstreamer : Object {
GLib.Error err;
string debug;
message.parse_error (out err, out debug);
GLib.warning ("Error: %s", err.message);
GLib.warning ("Error: %s %s", err.message, err.domain.to_string ());
break;
case Gst.MessageType.EOS:
GLib.warning ("end of stream\n");
GLib.warning ("End of stream\n");
break;
case Gst.MessageType.STATE_CHANGED:
Gst.State oldstate;
@ -212,21 +224,13 @@ public class EyeNeko.Gstreamer : Object {
Gst.State pending;
message.parse_state_changed (out oldstate, out newstate,
out pending);
// GLib.message ("state changed: %s->%s:%s\n",
// oldstate.to_string (), newstate.to_string (),
// pending.to_string ());
break;
case Gst.MessageType.TAG:
Gst.TagList tag_list;
stdout.printf ("taglist found\n");
message.parse_tag (out tag_list);
/*
GLib.message ("state changed: %s->%s:%s\n",
oldstate.to_string (), newstate.to_string (),
pending.to_string ());
*/
break;
case Gst.MessageType.ELEMENT:
if (message.get_structure ().get_name () == "GstVideoAnalyse") {
double val;
message.get_structure ().get_double ("luma-average", out val);
// af.adjust_step (val);
}
break;
default:
break;
@ -246,4 +250,9 @@ public class EyeNeko.Gstreamer : Object {
return _instance;
}
}
static construct {
unowned string[] ? gst_args = null;
Gst.init (ref gst_args);
}
}

View file

@ -1,34 +0,0 @@
public class EyeNeko.Logic.Filters.CCM : GLFilter {
public float red_in_red { get; set; default = 1.0f; }
public float red_in_green { get; set; default = 0.1f; }
public float red_in_blue { get; set; default = 0.1f; }
public float green_in_red { get; set; default = 0.0f; }
public float green_in_green { get; set; default = 0.8f; }
public float green_in_blue { get; set; default = 0.0f; }
public float blue_in_red { get; set; default = 0.1f; }
public float blue_in_green { get; set; default = 0.1f; }
public float blue_in_blue { get; set; default = 1.0f; }
public CCM (string name = "CCM", bool glupload = false, bool gldownload = false) {
base ("ccm", name, glupload, gldownload);
update_props ();
notify.connect (update_props);
}
private void update_props () {
Gst.Structure uniforms = new Gst.Structure.empty ("uniforms");
uniforms.set ("rr", typeof (float), red_in_red);
uniforms.set ("rg", typeof (float), red_in_green);
uniforms.set ("rb", typeof (float), red_in_blue);
uniforms.set ("gr", typeof (float), green_in_red);
uniforms.set ("gg", typeof (float), green_in_green);
uniforms.set ("gb", typeof (float), green_in_blue);
uniforms.set ("br", typeof (float), blue_in_red);
uniforms.set ("bg", typeof (float), blue_in_green);
uniforms.set ("bb", typeof (float), blue_in_blue);
gl_shader.set_property ("uniforms", uniforms);
}
}

View file

@ -1,43 +0,0 @@
namespace EyeNeko.Logic.Filters {
public interface Filter : Object {
public abstract Gst.Element element { get; }
}
public class GLFilter : Filter, Object {
public static string get_shader_source (string name) {
try {
return (string) resources_lookup_data ("/io/gitlab/nekocwd/eyeneko/shaders/" + name + ".glsl", GLib.ResourceLookupFlags.NONE).get_data ();
} catch (Error err) {
warning ("Error during shader lookup. %s", err.message);
return "";
}
}
public Gst.Element element { get; }
public Gst.Element gl_shader { get; }
public GLFilter (string shader_name, string elem_name = "GLShader", bool glupload = false, bool gldownload = false) {
var bin = new Gst.Bin (elem_name);
var gl_upload = Gst.ElementFactory.make ("glupload", @"$(elem_name)-upload");
var gl_download = Gst.ElementFactory.make ("gldownload", @"$(elem_name)-download");
_gl_shader = Gst.ElementFactory.make ("glshader", @"$(elem_name)-shader");
gl_shader.set_property ("fragment", GLFilter.get_shader_source (shader_name));
if (glupload) {
bin.add (gl_upload);
}
bin.add (gl_shader);
if (gldownload) {
bin.add (gl_download);
gl_shader.link (gl_download);
}
if (glupload) {
gl_upload.link (gl_shader);
}
EyeNeko.Gstreamer.add_pads_to_bin (ref bin);
_element = bin;
}
}
}

View file

@ -6,14 +6,14 @@ eyeneko_sources = [
'gst.vala',
'pipetap_proxy.vala',
'auto_focus.vala',
'logic/color_correction_filter.vala',
'logic/filter.vala',
'logic/helpers.vala',
'elements/bin_base.vala',
'elements/camerasrc.vala',
'elements/downscale.vala',
'elements/focus_analyze.vala',
'elements/gl_filter.vala',
'elements/color_correction_matrix.vala',
'ui/preferences.vala',
]
vapi_dir = meson.current_source_dir() / 'vapi'

6
src/ui/preferences.vala Normal file
View file

@ -0,0 +1,6 @@
public class EyeNeko.Ui.Settings : Adw.PreferencesDialog {
private Adw.PreferencesPage cameras = new Adw.PreferencesPage ();
construct {
cameras.title = _("Cameras");
}
}

View file

@ -13,13 +13,19 @@ template $EyeNekoWindow: Adw.ApplicationWindow {
content: Gtk.Overlay overlay {
child: Gtk.Picture viewfinder {
halign: fill;
valign: fill;
valign: start;
hexpand: true;
vexpand: true;
};
};
}
Gtk.Picture focus_overlay {
opacity: 0.5;
halign: start;
valign: start;
}
Adw.ToolbarView toolbar {
styles [
"flat",
@ -64,7 +70,7 @@ Adw.ToolbarView toolbar {
valign: center;
}
Button {
Button focus_btn {
valign: center;
halign: end;
width-request: 60;

View file

@ -27,12 +27,19 @@ public class EyeNeko.Window : Adw.ApplicationWindow {
[GtkChild]
private unowned Gtk.Picture viewfinder;
[GtkChild]
private unowned Gtk.Picture focus_overlay;
[GtkChild]
private unowned Gtk.MenuButton video_source_btn;
[GtkChild]
private unowned Gtk.Button capture_btn;
[GtkChild]
private unowned Adw.ToggleGroup camera_mode;
[GtkChild]
private unowned Gtk.Button focus_btn;
[GtkChild]
private unowned Gtk.Label focus_label;
public string camera_path { get; set; default = "Unknown"; }
private void setup_video_source_changer () {
notify["camera-path"].connect (() => {
@ -74,16 +81,31 @@ public class EyeNeko.Window : Adw.ApplicationWindow {
public Window (Gtk.Application app) {
Object (application: app);
overlay.add_overlay (toolbar);
overlay.add_overlay (focus_overlay);
setup_video_source_changer ();
Gstreamer.instance.bind_property ("focus-paintable", focus_overlay, "paintable", BindingFlags.SYNC_CREATE);
viewfinder.set_paintable (Gstreamer.instance.init_viewfinder ());
viewfinder.set_paintable (Gstreamer.instance.viewfinder_paintable);
Gstreamer.instance.start_stream_from (Gstreamer.Camera.get_all ()[0]);
var ec = new Gtk.GestureClick ();
overlay.add_controller (ec);
ec.pressed.connect ((n, x, y) => {
if (!viewfinder.contains (x, y))
return;
var width = x / viewfinder.get_width ();
var height = y / viewfinder.get_height ();
Gstreamer.instance.focus_analyze.window_hpos = height;
Gstreamer.instance.focus_analyze.window_wpos = width;
});
ec.button = 0;
// focus.clicked.connect (Gstreamer.af.reset_focus);
// Gstreamer.af.bind_property ("prev_pos", focus_label, "label", GLib.BindingFlags.SYNC_CREATE, (b, src, ref tgt) => { tgt = "%.1f".printf (src.get_double ()); return true; });
focus_btn.clicked.connect (AutoFocus.instance.reset_focus);
AutoFocus.instance.bind_property ("prev_pos", focus_label, "label", GLib.BindingFlags.SYNC_CREATE, (b, src, ref tgt) => { tgt = "%.1f".printf (src.get_double ()); return true; });
capture_btn.clicked.connect (() => {
message ("%d %d", Gstreamer.instance.camerabin_mode, (int) Gstreamer.instance.ready);