From 5023b54ba76325bb0b5598d59714bdad2d55d81e Mon Sep 17 00:00:00 2001
From: Stanislaw Halik <sthalik@misaki.pl>
Date: Mon, 18 Mar 2019 15:20:09 +0100
Subject: video: add support for camera modules

Issue: #910
---
 api/plugin-support.hpp                  |  36 ++++---
 compat/camera-names.cpp                 | 102 -------------------
 compat/camera-names.hpp                 |  18 ----
 cv/video-property-page.cpp              | 165 ------------------------------
 cv/video-property-page.hpp              |  13 ---
 tracker-aruco/ftnoir_tracker_aruco.cpp  |  43 +++++---
 tracker-aruco/ftnoir_tracker_aruco.h    |   4 +-
 tracker-pt/CMakeLists.txt               |   2 +-
 tracker-pt/ftnoir_tracker_pt.cpp        |   3 +-
 tracker-pt/ftnoir_tracker_pt_dialog.cpp |  13 +--
 tracker-pt/module/camera.cpp            | 101 +++++++------------
 tracker-pt/module/camera.h              |  17 +---
 tracker-pt/module/point_extractor.cpp   |   2 -
 tracker-pt/pt-api.hpp                   |   5 +-
 tracker-wii/wii_camera.cpp              |   7 +-
 tracker-wii/wii_camera.h                |   3 +-
 tracker-wii/wii_point_extractor.cpp     |   2 -
 variant/default/_variant.cmake          |   1 +
 variant/default/main-window.cpp         |   6 +-
 video-opencv/CMakeLists.txt             |   6 ++
 video-opencv/camera-impl.cpp            | 173 ++++++++++++++++++++++++++++++++
 video-opencv/camera-names.cpp           | 106 +++++++++++++++++++
 video-opencv/camera-names.hpp           |  18 ++++
 video-opencv/export.hpp                 |  11 ++
 video-opencv/lang/nl_NL.ts              |   4 +
 video-opencv/lang/ru_RU.ts              |   4 +
 video-opencv/lang/stub.ts               |   4 +
 video-opencv/lang/zh_CN.ts              |   4 +
 video-opencv/video-property-page.cpp    | 165 ++++++++++++++++++++++++++++++
 video-opencv/video-property-page.hpp    |  13 +++
 video/camera.cpp                        |  66 ++++++++++++
 video/camera.hpp                        |  95 ++++++++++++++++++
 32 files changed, 783 insertions(+), 429 deletions(-)
 delete mode 100644 compat/camera-names.cpp
 delete mode 100644 compat/camera-names.hpp
 delete mode 100644 cv/video-property-page.cpp
 delete mode 100644 cv/video-property-page.hpp
 create mode 100644 video-opencv/CMakeLists.txt
 create mode 100644 video-opencv/camera-impl.cpp
 create mode 100644 video-opencv/camera-names.cpp
 create mode 100644 video-opencv/camera-names.hpp
 create mode 100644 video-opencv/export.hpp
 create mode 100644 video-opencv/lang/nl_NL.ts
 create mode 100644 video-opencv/lang/ru_RU.ts
 create mode 100644 video-opencv/lang/stub.ts
 create mode 100644 video-opencv/lang/zh_CN.ts
 create mode 100644 video-opencv/video-property-page.cpp
 create mode 100644 video-opencv/video-property-page.hpp
 create mode 100644 video/camera.cpp
 create mode 100644 video/camera.hpp

diff --git a/api/plugin-support.hpp b/api/plugin-support.hpp
index df1344bf..65b8e10d 100644
--- a/api/plugin-support.hpp
+++ b/api/plugin-support.hpp
@@ -35,10 +35,11 @@ struct dylib final
         Tracker = 0xcafebeef,
         Protocol = 0xdeadf00d,
         Extension = 0xcafebabe,
+        Video = 0xbadf00d,
         Invalid = (unsigned)-1,
     };
 
-    dylib(const QString& filename_, Type t) :
+    dylib(const QString& filename_, Type t, bool load = true) :
         full_filename(filename_),
         module_name(trim_filename(filename_))
     {
@@ -57,19 +58,25 @@ struct dylib final
         if (check(!handle.load()))
             return;
 
-        if (check((Dialog = (module_ctor_t) handle.resolve("GetDialog"), !Dialog)))
-            return;
+        if (load)
+        {
+            if (check((Dialog = (module_ctor_t) handle.resolve("GetDialog"), !Dialog)))
+                return;
 
-        if (check((Constructor = (module_ctor_t) handle.resolve("GetConstructor"), !Constructor)))
-            return;
+            if (check((Constructor = (module_ctor_t) handle.resolve("GetConstructor"), !Constructor)))
+                return;
 
-        if (check((Meta = (module_metadata_t) handle.resolve("GetMetadata"), !Meta)))
-            return;
+            if (check((Meta = (module_metadata_t) handle.resolve("GetMetadata"), !Meta)))
+                return;
+
+            std::unique_ptr<Metadata_> m{Meta()};
 
-        std::unique_ptr<Metadata_> m{Meta()};
+            if (check(!m))
+                return;
 
-        icon = m->icon();
-        name = m->name();
+            icon = m->icon();
+            name = m->name();
+        }
 
         type = t;
 #ifdef __clang__
@@ -88,18 +95,20 @@ struct dylib final
         const struct filter_ {
             Type type{Invalid};
             QString glob;
+            bool load = true;
         } filters[] = {
             { Filter, QStringLiteral(OPENTRACK_LIBRARY_PREFIX "opentrack-filter-*." OPENTRACK_LIBRARY_EXTENSION), },
             { Tracker, QStringLiteral(OPENTRACK_LIBRARY_PREFIX "opentrack-tracker-*." OPENTRACK_LIBRARY_EXTENSION), },
             { Protocol, QStringLiteral(OPENTRACK_LIBRARY_PREFIX "opentrack-proto-*." OPENTRACK_LIBRARY_EXTENSION), },
             { Extension, QStringLiteral(OPENTRACK_LIBRARY_PREFIX "opentrack-ext-*." OPENTRACK_LIBRARY_EXTENSION), },
+            { Video, QStringLiteral(OPENTRACK_LIBRARY_PREFIX "opentrack-video-*." OPENTRACK_LIBRARY_EXTENSION), false, },
         };
 
         for (const filter_& filter : filters)
         {
             for (const QString& filename : module_directory.entryList({ filter.glob }, QDir::Files, QDir::Name))
             {
-                auto lib = std::make_shared<dylib>(QStringLiteral("%1/%2").arg(library_path, filename), filter.type);
+                auto lib = std::make_shared<dylib>(QStringLiteral("%1/%2").arg(library_path, filename), filter.type, filter.load);
 
                 if (lib->type == Invalid)
                     continue;
@@ -159,6 +168,7 @@ private:
                     OPENTRACK_LIBRARY_PREFIX "opentrack-proto-",
                     OPENTRACK_LIBRARY_PREFIX "opentrack-filter-",
                     OPENTRACK_LIBRARY_PREFIX "opentrack-ext-",
+                    OPENTRACK_LIBRARY_PREFIX "opentrack-video-",
                 };
 
                 for (auto name : names)
@@ -198,7 +208,8 @@ struct Modules final
         filter_modules(filter(dylib::Filter)),
         tracker_modules(filter(dylib::Tracker)),
         protocol_modules(filter(dylib::Protocol)),
-        extension_modules(filter(dylib::Extension))
+        extension_modules(filter(dylib::Extension)),
+        video_modules(filter(dylib::Video))
     {}
     dylib_list& filters() { return filter_modules; }
     dylib_list& trackers() { return tracker_modules; }
@@ -211,6 +222,7 @@ private:
     dylib_list tracker_modules;
     dylib_list protocol_modules;
     dylib_list extension_modules;
+    dylib_list video_modules;
 
     static dylib_list& sorted(dylib_list& xs)
     {
diff --git a/compat/camera-names.cpp b/compat/camera-names.cpp
deleted file mode 100644
index 246d76ee..00000000
--- a/compat/camera-names.cpp
+++ /dev/null
@@ -1,102 +0,0 @@
-#include "camera-names.hpp"
-
-#ifdef _WIN32
-#   include <cwchar>
-#   define NO_DSHOW_STRSAFE
-#   include <dshow.h>
-#elif defined(__unix) || defined(__linux) || defined(__APPLE__)
-#   include <unistd.h>
-#endif
-
-#ifdef __linux
-#   include <fcntl.h>
-#   include <sys/ioctl.h>
-#   include <linux/videodev2.h>
-#   include <cerrno>
-#   include <cstring>
-#endif
-
-#include <QDebug>
-
-int camera_name_to_index(const QString &name)
-{
-    auto list = get_camera_names();
-    int ret = list.indexOf(name);
-    if (ret < 0)
-        ret = 0;
-    return ret;
-}
-
-QList<QString> get_camera_names()
-{
-    QList<QString> ret;
-#ifdef _WIN32
-    // Create the System Device Enumerator.
-    HRESULT hr;
-    CoInitialize(nullptr);
-    ICreateDevEnum *pSysDevEnum = nullptr;
-    hr = CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void **)&pSysDevEnum);
-    if (FAILED(hr))
-    {
-        qDebug() << "failed CLSID_SystemDeviceEnum" << hr;
-        return ret;
-    }
-    // Obtain a class enumerator for the video compressor category.
-    IEnumMoniker *pEnumCat = nullptr;
-    hr = pSysDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnumCat, 0);
-
-    if (hr == S_OK) {
-        // Enumerate the monikers.
-        IMoniker *pMoniker = nullptr;
-        ULONG cFetched;
-        while (pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK)
-        {
-            IPropertyBag *pPropBag;
-            hr = pMoniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, (void **)&pPropBag);
-            if (SUCCEEDED(hr))	{
-                // To retrieve the filter's friendly name, do the following:
-                VARIANT var;
-                VariantInit(&var);
-                hr = pPropBag->Read(L"FriendlyName", &var, nullptr);
-                if (SUCCEEDED(hr))
-                {
-                    // Display the name in your UI somehow.
-                    QString str((QChar*)var.bstrVal, int(std::wcslen(var.bstrVal)));
-                    ret.append(str);
-                }
-                VariantClear(&var);
-                pPropBag->Release();
-            }
-            pMoniker->Release();
-        }
-        pEnumCat->Release();
-    }
-    else
-        qDebug() << "failed CLSID_VideoInputDeviceCategory" << hr;
-
-    pSysDevEnum->Release();
-#endif
-
-#ifdef __linux
-    for (int i = 0; i < 16; i++) {
-        char buf[32];
-        snprintf(buf, sizeof(buf), "/dev/video%d", i);
-
-        if (access(buf, R_OK | W_OK) == 0) {
-            int fd = open(buf, O_RDONLY);
-            if (fd == -1)
-                continue;
-            struct v4l2_capability video_cap;
-            if(ioctl(fd, VIDIOC_QUERYCAP, &video_cap) == -1)
-            {
-                qDebug() << "VIDIOC_QUERYCAP" << errno;
-                close(fd);
-                continue;
-            }
-            ret.append(QString{(const char*)video_cap.card});
-            close(fd);
-        }
-    }
-#endif
-    return ret;
-}
diff --git a/compat/camera-names.hpp b/compat/camera-names.hpp
deleted file mode 100644
index 97184c8c..00000000
--- a/compat/camera-names.hpp
+++ /dev/null
@@ -1,18 +0,0 @@
-/* Copyright (c) 2014-2015, Stanislaw Halik <sthalik@misaki.pl>
-
- * Permission to use, copy, modify, and/or distribute this
- * software for any purpose with or without fee is hereby granted,
- * provided that the above copyright notice and this permission
- * notice appear in all copies.
- */
-
-#pragma once
-
-#include <QList>
-#include <QString>
-
-#include "export.hpp"
-
-OTR_COMPAT_EXPORT QList<QString> get_camera_names();
-OTR_COMPAT_EXPORT int camera_name_to_index(const QString &name);
-
diff --git a/cv/video-property-page.cpp b/cv/video-property-page.cpp
deleted file mode 100644
index bae5e8b3..00000000
--- a/cv/video-property-page.cpp
+++ /dev/null
@@ -1,165 +0,0 @@
-/* Copyright (c) 2016 Stanislaw Halik
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- */
-
-#include "video-property-page.hpp"
-
-#ifdef _WIN32
-
-#include "compat/camera-names.hpp"
-#include "compat/sleep.hpp"
-#include "compat/run-in-thread.hpp"
-#include "compat/library-path.hpp"
-
-#include <cstring>
-
-#include <opencv2/videoio.hpp>
-
-#include <QApplication>
-#include <QProcess>
-#include <QThread>
-#include <QMessageBox>
-
-#include <QDebug>
-
-bool video_property_page::show_from_capture(cv::VideoCapture& cap, int /*index */)
-{
-    return cap.set(cv::CAP_PROP_SETTINGS, 0);
-}
-
-struct prop_settings_worker final : QThread
-{
-    explicit prop_settings_worker(int idx);
-    ~prop_settings_worker() override;
-
-private:
-    void open_prop_page();
-    void run() override;
-
-    cv::VideoCapture cap;
-    int idx = -1;
-};
-
-prop_settings_worker::prop_settings_worker(int idx_)
-{
-    int ret = (int)cap.get(cv::CAP_PROP_SETTINGS);
-
-    if (ret != 0)
-        run_in_thread_async(qApp, [] {
-            QMessageBox::warning(nullptr,
-                                 "Camera properties",
-                                 "Camera dialog already opened",
-                                 QMessageBox::Cancel,
-                                 QMessageBox::NoButton);
-        });
-    else
-    {
-        idx = idx_;
-        // DON'T MOVE IT
-        // ps3 eye will reset to default settings if done from another thread
-        open_prop_page();
-    }
-}
-
-void prop_settings_worker::open_prop_page()
-{
-    cap.open(idx);
-
-    if (cap.isOpened())
-    {
-        cv::Mat tmp;
-
-        for (unsigned k = 0; k < 2000/50; k++)
-        {
-            if (cap.read(tmp))
-            {
-                qDebug() << "got frame" << tmp.rows << tmp.cols;
-                goto ok;
-            }
-            portable::sleep(50);
-        }
-    }
-
-    qDebug() << "property-page: can't open camera";
-    idx = -1;
-
-    return;
-
-ok:
-    portable::sleep(100);
-
-    qDebug() << "property-page: opening for" << idx;
-
-    if (!cap.set(cv::CAP_PROP_SETTINGS, 0))
-    {
-        run_in_thread_async(qApp, [] {
-            QMessageBox::warning(nullptr,
-                                 "Camera properties",
-                                 "Can't open camera dialog",
-                                 QMessageBox::Cancel,
-                                 QMessageBox::NoButton);
-        });
-    }
-}
-
-prop_settings_worker::~prop_settings_worker()
-{
-    if (idx != -1)
-    {
-        // ax filter is race condition-prone
-        portable::sleep(250);
-        cap.release();
-        // idem
-        portable::sleep(250);
-
-        qDebug() << "property-page: closed" << idx;
-    }
-}
-
-void prop_settings_worker::run()
-{
-    if (idx != -1)
-    {
-        while (cap.get(cv::CAP_PROP_SETTINGS) > 0)
-            portable::sleep(1000);
-    }
-}
-
-bool video_property_page::show(int idx)
-{
-    auto thread = new prop_settings_worker(idx);
-
-    // XXX is this a race condition?
-    thread->moveToThread(qApp->thread());
-    QObject::connect(thread, &QThread::finished, qApp, [thread] { thread->deleteLater(); }, Qt::DirectConnection);
-
-    thread->start();
-
-    return true;
-}
-
-#elif defined(__linux)
-#   include <QProcess>
-#   include "compat/camera-names.hpp"
-
-bool video_property_page::show(int idx)
-{
-    const QList<QString> camera_names(get_camera_names());
-
-    if (idx >= 0 && idx < camera_names.size())
-        return QProcess::startDetached("qv4l2", QStringList { "-d", QString("/dev/video%1").arg(idx) });
-    else
-        return false;
-}
-
-bool video_property_page::show_from_capture(cv::VideoCapture&, int idx)
-{
-    return show(idx);
-}
-#else
-bool video_property_page::show(int) { return false; }
-bool video_property_page::show_from_capture(cv::VideoCapture&, int) { return false; }
-#endif
diff --git a/cv/video-property-page.hpp b/cv/video-property-page.hpp
deleted file mode 100644
index c2b9525d..00000000
--- a/cv/video-property-page.hpp
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma once
-
-#include <QString>
-#include <opencv2/videoio.hpp>
-
-struct video_property_page final
-{
-    video_property_page() = delete;
-    static bool show(int id);
-    static bool show_from_capture(cv::VideoCapture& cap, int index);
-private:
-};
-
diff --git a/tracker-aruco/ftnoir_tracker_aruco.cpp b/tracker-aruco/ftnoir_tracker_aruco.cpp
index 8928566f..d9674755 100644
--- a/tracker-aruco/ftnoir_tracker_aruco.cpp
+++ b/tracker-aruco/ftnoir_tracker_aruco.cpp
@@ -6,8 +6,6 @@
  */
 
 #include "ftnoir_tracker_aruco.h"
-#include "cv/video-property-page.hpp"
-#include "compat/camera-names.hpp"
 #include "compat/sleep.hpp"
 #include "compat/math-imports.hpp"
 
@@ -75,7 +73,6 @@ aruco_tracker::~aruco_tracker()
     wait();
     // fast start/stop causes breakage
     portable::sleep(1000);
-    camera.release();
 }
 
 module_status aruco_tracker::start_tracker(QFrame* videoframe)
@@ -166,16 +163,18 @@ bool aruco_tracker::open_camera()
 
     QMutexLocker l(&camera_mtx);
 
-    camera = cv::VideoCapture(camera_name_to_index(s.camera_name));
+    camera = video::make_camera(s.camera_name);
+    video::impl::camera::info args {};
+
     if (res.width)
     {
-        camera.set(cv::CAP_PROP_FRAME_WIDTH, res.width);
-        camera.set(cv::CAP_PROP_FRAME_HEIGHT, res.height);
+        args.width = res.width;
+        args.height = res.height;
     }
     if (fps)
-        camera.set(cv::CAP_PROP_FPS, fps);
+        args.fps = fps;
 
-    if (!camera.isOpened())
+    if (!camera->start(args))
     {
         qDebug() << "aruco tracker: can't open camera";
         return false;
@@ -372,14 +371,28 @@ void aruco_tracker::run()
         {
             QMutexLocker l(&camera_mtx);
 
-            if (!camera.read(color))
+            auto [ img, res ] = camera->get_frame();
+
+            if (!res)
             {
                 portable::sleep(100);
                 continue;
             }
-        }
 
-        cv::cvtColor(color, grayscale, cv::COLOR_BGR2GRAY);
+            color = cv::Mat(img.height, img.width, CV_8UC(img.channels), (void*)img.data, img.stride);
+
+            switch (img.channels)
+            {
+            case 1:
+                grayscale.setTo(color); break;
+            case 3:
+                cv::cvtColor(color, grayscale, cv::COLOR_BGR2GRAY);
+                break;
+            default:
+                qDebug() << "aruco: can't handle" << img.channels << "color channels";
+                return;
+            }
+        }
 
 #ifdef DEBUG_UNSHARP_MASKING
         {
@@ -496,7 +509,9 @@ aruco_dialog::aruco_dialog() :
 
     tracker = nullptr;
     calib_timer.setInterval(100);
-    ui.cameraName->addItems(get_camera_names());
+
+    for (const auto& str : video::camera_names())
+        ui.cameraName->addItem(str);
 
     tie_setting(s.camera_name, ui.cameraName);
     tie_setting(s.resolution, ui.resolution);
@@ -572,10 +587,10 @@ void aruco_dialog::camera_settings()
     if (tracker)
     {
         QMutexLocker l(&tracker->camera_mtx);
-        video_property_page::show_from_capture(tracker->camera, camera_name_to_index(s.camera_name));
+        (void)tracker->camera->show_dialog();
     }
     else
-        video_property_page::show(camera_name_to_index(s.camera_name));
+        (void)video::show_dialog(s.camera_name);
 }
 
 void aruco_dialog::update_camera_settings_state(const QString& name)
diff --git a/tracker-aruco/ftnoir_tracker_aruco.h b/tracker-aruco/ftnoir_tracker_aruco.h
index 1d6fd107..0a33f02b 100644
--- a/tracker-aruco/ftnoir_tracker_aruco.h
+++ b/tracker-aruco/ftnoir_tracker_aruco.h
@@ -13,6 +13,7 @@
 #include "api/plugin-api.hpp"
 #include "cv/video-widget.hpp"
 #include "compat/timer.hpp"
+#include "video/camera.hpp"
 
 #include "aruco/markerdetector.h"
 
@@ -27,7 +28,6 @@
 #include <cinttypes>
 
 #include <opencv2/core.hpp>
-#include <opencv2/videoio.hpp>
 
 // value 0->1
 //#define DEBUG_UNSHARP_MASKING .75
@@ -77,7 +77,7 @@ public:
 
     void getRT(cv::Matx33d &r, cv::Vec3d &t);
     QMutex camera_mtx;
-    cv::VideoCapture camera;
+    std::unique_ptr<video::impl::camera> camera;
 
 private:
     bool detect_with_roi();
diff --git a/tracker-pt/CMakeLists.txt b/tracker-pt/CMakeLists.txt
index f12f530b..304a6b3d 100644
--- a/tracker-pt/CMakeLists.txt
+++ b/tracker-pt/CMakeLists.txt
@@ -2,7 +2,7 @@ find_package(OpenCV QUIET)
 if(OpenCV_FOUND)
     otr_module(tracker-pt-base STATIC)
     target_include_directories(${self} SYSTEM PUBLIC ${OpenCV_INCLUDE_DIRS})
-    target_link_libraries(${self} opencv_imgproc opentrack-cv opencv_core)
+    target_link_libraries(${self} opencv_imgproc opentrack-cv opencv_core opentrack-video)
     set_property(TARGET ${self} PROPERTY OUTPUT_NAME "pt-base")
 endif()
 add_subdirectory(module)
diff --git a/tracker-pt/ftnoir_tracker_pt.cpp b/tracker-pt/ftnoir_tracker_pt.cpp
index 3854e531..4b796af7 100644
--- a/tracker-pt/ftnoir_tracker_pt.cpp
+++ b/tracker-pt/ftnoir_tracker_pt.cpp
@@ -8,7 +8,6 @@
 
 #include "ftnoir_tracker_pt.h"
 #include "video/video-widget.hpp"
-#include "compat/camera-names.hpp"
 #include "compat/math-imports.hpp"
 
 #include "pt-api.hpp"
@@ -121,7 +120,7 @@ bool Tracker_PT::maybe_reopen_camera()
 {
     QMutexLocker l(&camera_mtx);
 
-    return camera->start(camera_name_to_index(s.camera_name),
+    return camera->start(s.camera_name,
                          s.cam_fps, s.cam_res_x, s.cam_res_y);
 }
 
diff --git a/tracker-pt/ftnoir_tracker_pt_dialog.cpp b/tracker-pt/ftnoir_tracker_pt_dialog.cpp
index 2b06c823..edf689a9 100644
--- a/tracker-pt/ftnoir_tracker_pt_dialog.cpp
+++ b/tracker-pt/ftnoir_tracker_pt_dialog.cpp
@@ -7,10 +7,9 @@
  */
 
 #include "ftnoir_tracker_pt_dialog.h"
-
 #include "compat/math.hpp"
-#include "compat/camera-names.hpp"
-#include "cv/video-property-page.hpp"
+#include "video/camera.hpp"
+
 #include <opencv2/core.hpp>
 
 #include <QString>
@@ -33,7 +32,8 @@ TrackerDialog_PT::TrackerDialog_PT(const QString& module_name) :
 
     ui.setupUi(this);
 
-    ui.camdevice_combo->addItems(get_camera_names());
+    for (const QString& str : video::camera_names())
+        ui.camdevice_combo->addItem(str);
 
     tie_setting(s.camera_name, ui.camdevice_combo);
     tie_setting(s.cam_res_x, ui.res_x_spin);
@@ -231,10 +231,7 @@ void TrackerDialog_PT::show_camera_settings()
         tracker->camera->show_camera_settings();
     }
     else
-    {
-        const int idx = camera_name_to_index(s.camera_name);
-        video_property_page::show(idx);
-    }
+        (void)video::show_dialog(s.camera_name);
 }
 
 void TrackerDialog_PT::trans_calib_step()
diff --git a/tracker-pt/module/camera.cpp b/tracker-pt/module/camera.cpp
index 1afecc92..687f5bff 100644
--- a/tracker-pt/module/camera.cpp
+++ b/tracker-pt/module/camera.cpp
@@ -8,15 +8,9 @@
 #include "camera.h"
 #include "frame.hpp"
 
-#include "compat/sleep.hpp"
-#include "compat/camera-names.hpp"
 #include "compat/math-imports.hpp"
 
-#include <opencv2/imgproc.hpp>
-
-#include "cv/video-property-page.hpp"
-
-#include <cstdlib>
+#include <opencv2/core.hpp>
 
 namespace pt_module {
 
@@ -26,22 +20,18 @@ Camera::Camera(const QString& module_name) : s { module_name }
 
 QString Camera::get_desired_name() const
 {
-    return desired_name;
+    return cam_desired.name;
 }
 
 QString Camera::get_active_name() const
 {
-    return active_name;
+    return cam_info.name;
 }
 
 void Camera::show_camera_settings()
 {
-    const int idx = camera_name_to_index(s.camera_name);
-
-    if (cap && cap->isOpened())
-        video_property_page::show_from_capture(*cap, idx);
-    else
-        video_property_page::show(idx);
+    if (cap)
+        (void)cap->show_dialog();
 }
 
 Camera::result Camera::get_info() const
@@ -83,59 +73,53 @@ Camera::result Camera::get_frame(pt_frame& frame_)
         return { false, {} };
 }
 
-bool Camera::start(int idx, int fps, int res_x, int res_y)
+bool Camera::start(const QString& name, int fps, int res_x, int res_y)
 {
-    if (idx >= 0 && fps >= 0 && res_x >= 0 && res_y >= 0)
+    if (fps >= 0 && res_x >= 0 && res_y >= 0)
     {
-        if (cam_desired.idx != idx ||
+        if (cam_desired.name != name ||
             (int)cam_desired.fps != fps ||
             cam_desired.res_x != res_x ||
             cam_desired.res_y != res_y ||
-            !cap || !cap->isOpened() || !cap->grab())
+            !cap || !cap->is_open())
         {
             stop();
 
-            desired_name = get_camera_names().value(idx);
-            cam_desired.idx = idx;
+            cam_desired.name = name;
             cam_desired.fps = fps;
             cam_desired.res_x = res_x;
             cam_desired.res_y = res_y;
             cam_desired.fov = fov;
 
-            cap = camera_ptr(new cv::VideoCapture(idx));
+            cap = video::make_camera(name);
 
-            if (cam_desired.res_x > 0 && cam_desired.res_y > 0)
-            {
-                cap->set(cv::CAP_PROP_FRAME_WIDTH,  res_x);
-                cap->set(cv::CAP_PROP_FRAME_HEIGHT, res_y);
-            }
+            if (!cap)
+                goto fail;
 
-            if (fps > 0)
-                cap->set(cv::CAP_PROP_FPS, fps);
+            camera::info info {};
+            info.fps = fps;
+            info.width = res_x;
+            info.height = res_y;
 
-            if (cap->isOpened())
-            {
-                cam_info = pt_camera_info();
-                cam_info.idx = idx;
-                dt_mean = 0;
-                active_name = desired_name;
+            if (!cap->start(info))
+                goto fail;
 
-                cv::Mat tmp;
+            cam_info = pt_camera_info();
+            cam_info.name = name;
+            dt_mean = 0;
 
-                if (get_frame_(tmp))
-                {
-                    t.start();
-                    return true;
-                }
-            }
+            cv::Mat tmp;
 
-            cap = nullptr;
-            return false;
-        }
+            if (!get_frame_(tmp))
+                goto fail;
 
-        return true;
+            t.start();
+        }
     }
 
+    return true;
+
+fail:
     stop();
     return false;
 }
@@ -143,34 +127,23 @@ bool Camera::start(int idx, int fps, int res_x, int res_y)
 void Camera::stop()
 {
     cap = nullptr;
-    desired_name = QString{};
-    active_name = QString{};
     cam_info = {};
     cam_desired = {};
 }
 
-bool Camera::get_frame_(cv::Mat& frame)
+bool Camera::get_frame_(cv::Mat& img)
 {
-    if (cap && cap->isOpened())
+    if (cap && cap->is_open())
     {
-        for (unsigned i = 0; i < 10; i++)
+        auto [ frame, ret ] = cap->get_frame();
+        if (ret)
         {
-            if (cap->read(frame))
-                return true;
-            portable::sleep(50);
+            img = cv::Mat(frame.height, frame.width, CV_8UC(frame.channels), (void*)frame.data, frame.stride);
+            return true;
         }
     }
-    return false;
-}
 
-void Camera::camera_deleter::operator()(cv::VideoCapture* cap)
-{
-    if (cap)
-    {
-        if (cap->isOpened())
-            cap->release();
-        delete cap;
-    }
+    return false;
 }
 
 } // ns pt_module
diff --git a/tracker-pt/module/camera.h b/tracker-pt/module/camera.h
index 2ea633d0..02e2fe4d 100644
--- a/tracker-pt/module/camera.h
+++ b/tracker-pt/module/camera.h
@@ -9,11 +9,11 @@
 
 #include "pt-api.hpp"
 #include "compat/timer.hpp"
+#include "video/camera.hpp"
 
 #include <memory>
 
 #include <opencv2/core.hpp>
-#include <opencv2/videoio.hpp>
 
 #include <QString>
 
@@ -23,7 +23,7 @@ struct Camera final : pt_camera
 {
     Camera(const QString& module_name);
 
-    bool start(int idx, int fps, int res_x, int res_y) override;
+    bool start(const QString& name, int fps, int res_x, int res_y) override;
     void stop() override;
 
     result get_frame(pt_frame& Frame) override;
@@ -37,23 +37,16 @@ struct Camera final : pt_camera
     void show_camera_settings() override;
 
 private:
+    using camera = typename video::impl::camera;
+
     [[nodiscard]] bool get_frame_(cv::Mat& frame);
 
     f dt_mean = 0, fov = 30;
     Timer t;
     pt_camera_info cam_info;
     pt_camera_info cam_desired;
-    QString desired_name, active_name;
-
-    struct camera_deleter final
-    {
-        void operator()(cv::VideoCapture* cap);
-    };
-
-    using camera_ptr = std::unique_ptr<cv::VideoCapture, camera_deleter>;
-
-    camera_ptr cap;
 
+    std::unique_ptr<camera> cap;
     pt_settings s;
 
     static constexpr f dt_eps = f{1}/256;
diff --git a/tracker-pt/module/point_extractor.cpp b/tracker-pt/module/point_extractor.cpp
index 298d8752..2cb5db97 100644
--- a/tracker-pt/module/point_extractor.cpp
+++ b/tracker-pt/module/point_extractor.cpp
@@ -13,8 +13,6 @@
 #include "cv/numeric.hpp"
 #include "compat/math.hpp"
 
-#include <opencv2/videoio.hpp>
-
 #undef PREVIEW
 //#define PREVIEW
 
diff --git a/tracker-pt/pt-api.hpp b/tracker-pt/pt-api.hpp
index b44cfea2..741576a1 100644
--- a/tracker-pt/pt-api.hpp
+++ b/tracker-pt/pt-api.hpp
@@ -12,6 +12,7 @@
 #include <opencv2/core.hpp>
 
 #include <QImage>
+#include <QString>
 
 #ifdef __clang__
 #   pragma clang diagnostic push
@@ -30,7 +31,7 @@ struct pt_camera_info final
 
     int res_x = 0;
     int res_y = 0;
-    int idx = -1;
+    QString name;
 };
 
 struct pt_pixel_pos_mixin
@@ -74,7 +75,7 @@ struct pt_camera
     pt_camera();
     virtual ~pt_camera();
 
-    [[nodiscard]] virtual bool start(int idx, int fps, int res_x, int res_y) = 0;
+    [[nodiscard]] virtual bool start(const QString& name, int fps, int res_x, int res_y) = 0;
     virtual void stop() = 0;
 
     virtual result get_frame(pt_frame& frame) = 0;
diff --git a/tracker-wii/wii_camera.cpp b/tracker-wii/wii_camera.cpp
index 90ad6385..97a32b9f 100644
--- a/tracker-wii/wii_camera.cpp
+++ b/tracker-wii/wii_camera.cpp
@@ -16,13 +16,10 @@
 #include "wii_frame.hpp"
 
 #include "compat/sleep.hpp"
-#include "compat/camera-names.hpp"
 #include "compat/math-imports.hpp"
 
 #include <opencv2/imgproc.hpp>
 
-#include "cv/video-property-page.hpp"
-
 #include <bluetoothapis.h>
 
 using namespace pt_module;
@@ -33,7 +30,7 @@ WIICamera::WIICamera(const QString& module_name) : s { module_name }
 	cam_info.res_x = 1024;
 	cam_info.res_y = 768;
 	cam_info.fov = 42.0f;
-    cam_info.idx = 0;
+	cam_info.name = "Wii";
 }
 
 WIICamera::~WIICamera()
@@ -86,7 +83,7 @@ WIICamera::result WIICamera::get_frame(pt_frame& frame_)
 	return result(true, cam_info);
 }
 
-bool WIICamera::start(int idx, int fps, int res_x, int res_y)
+bool WIICamera::start(const QString& name, int fps, int res_x, int res_y)
 {
 	m_pDev = std::make_unique<wiimote>();
 	m_pDev->ChangedCallback = on_state_change;
diff --git a/tracker-wii/wii_camera.h b/tracker-wii/wii_camera.h
index 05f5436c..7bc74559 100644
--- a/tracker-wii/wii_camera.h
+++ b/tracker-wii/wii_camera.h
@@ -17,7 +17,6 @@
 #include <tuple>
 
 #include <opencv2/core.hpp>
-#include <opencv2/videoio.hpp>
 
 #include <QString>
 
@@ -31,7 +30,7 @@ struct WIICamera final : pt_camera
     WIICamera(const QString& module_name);
     ~WIICamera() override;
 
-    bool start(int idx, int fps, int res_x, int res_y) override;
+    bool start(const QString& name, int fps, int res_x, int res_y) override;
     void stop() override;
 
     result get_frame(pt_frame& Frame) override;
diff --git a/tracker-wii/wii_point_extractor.cpp b/tracker-wii/wii_point_extractor.cpp
index a23e0e5b..89e4b41b 100644
--- a/tracker-wii/wii_point_extractor.cpp
+++ b/tracker-wii/wii_point_extractor.cpp
@@ -14,8 +14,6 @@
 #include "cv/numeric.hpp"
 #include "compat/math.hpp"
 
-#include <opencv2/videoio.hpp>
-
 #undef PREVIEW
 //#define PREVIEW
 
diff --git a/variant/default/_variant.cmake b/variant/default/_variant.cmake
index 161fefda..7501b0a4 100644
--- a/variant/default/_variant.cmake
+++ b/variant/default/_variant.cmake
@@ -24,6 +24,7 @@ function(otr_init_variant)
         "migration"
         "main-window"
         "video"
+        "video-*"
     )
 
     set_property(GLOBAL PROPERTY opentrack-subprojects "${subprojects}")
diff --git a/variant/default/main-window.cpp b/variant/default/main-window.cpp
index a223c32f..334695cb 100644
--- a/variant/default/main-window.cpp
+++ b/variant/default/main-window.cpp
@@ -448,7 +448,7 @@ void main_window::stop_tracker_()
     with_tracker_teardown sentinel;
 
     pose_update_timer.stop();
-    ui.pose_display->rotate_sync(0,0,0, 0,0,0);
+    ui.pose_display->present(0,0,0, 0,0,0);
 
     if (pTrackerDialog)
         pTrackerDialog->unregister_tracker();
@@ -473,8 +473,8 @@ void main_window::stop_tracker_()
 
 void main_window::show_pose_(const double* mapped, const double* raw)
 {
-    ui.pose_display->rotate_async(mapped[Yaw], mapped[Pitch], -mapped[Roll],
-                                  mapped[TX], mapped[TY], mapped[TZ]);
+    ui.pose_display->present(mapped[Yaw], mapped[Pitch], -mapped[Roll],
+                             mapped[TX], mapped[TY], mapped[TZ]);
 
     QLCDNumber* raw_[] = {
         ui.raw_x, ui.raw_y, ui.raw_z,
diff --git a/video-opencv/CMakeLists.txt b/video-opencv/CMakeLists.txt
new file mode 100644
index 00000000..d8b9b896
--- /dev/null
+++ b/video-opencv/CMakeLists.txt
@@ -0,0 +1,6 @@
+find_package(OpenCV QUIET)
+
+if(OpenCV_FOUND)
+    otr_module(video-opencv)
+    target_link_libraries(${self} opencv_core opencv_videoio opentrack-video)
+endif()
diff --git a/video-opencv/camera-impl.cpp b/video-opencv/camera-impl.cpp
new file mode 100644
index 00000000..ca18fd4b
--- /dev/null
+++ b/video-opencv/camera-impl.cpp
@@ -0,0 +1,173 @@
+/* Copyright (c) 2019 Stanislaw Halik <sthalik@misaki.pl>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ */
+
+#include "compat/sleep.hpp"
+#include "video/camera.hpp"
+
+#include "camera-names.hpp"
+#include "video-property-page.hpp"
+
+#include <optional>
+
+#include <opencv2/core.hpp>
+#include <opencv2/videoio.hpp>
+
+using namespace video::impl;
+
+struct cam;
+
+struct metadata : camera_
+{
+    metadata();
+    std::vector<QString> camera_names() const override;
+    std::unique_ptr<camera> make_camera(const QString& name) override;
+    bool can_show_dialog(const QString& camera_name) override;
+    bool show_dialog(const QString& camera_name) override;
+};
+
+struct cam final : camera
+{
+    cam(int idx);
+    ~cam() override;
+
+    bool start(const info& args) override;
+    void stop() override;
+    bool is_open() override;
+    std::tuple<const frame&, bool> get_frame() override;
+    bool show_dialog() override;
+
+    bool get_frame_();
+
+    std::optional<cv::VideoCapture> cap;
+    cv::Mat mat;
+    frame frame_;
+    int idx = -1;
+};
+
+metadata::metadata() = default;
+
+std::unique_ptr<camera> metadata::make_camera(const QString& name)
+{
+    int idx = camera_name_to_index(name);
+    if (idx != -1)
+        return std::make_unique<cam>(idx);
+    else
+        return nullptr;
+}
+
+std::vector<QString> metadata::camera_names() const
+{
+    return get_camera_names();
+}
+
+bool metadata::can_show_dialog(const QString& camera_name)
+{
+    return camera_name_to_index(camera_name) != -1;
+}
+
+bool metadata::show_dialog(const QString& camera_name)
+{
+    int idx = camera_name_to_index(camera_name);
+    if (idx != -1)
+    {
+        video_property_page::show(idx);
+        return true;
+    }
+    else
+        return false;
+}
+
+cam::cam(int idx) : idx(idx)
+{
+}
+
+cam::~cam()
+{
+    stop();
+}
+
+void cam::stop()
+{
+    if (cap)
+    {
+        if (cap->isOpened())
+            cap->release();
+        cap = std::nullopt;
+    }
+    mat = cv::Mat();
+    frame_ = { {}, false };
+}
+
+bool cam::is_open()
+{
+    return !!cap;
+}
+
+bool cam::start(const info& args)
+{
+    stop();
+    cap.emplace(idx);
+
+    if (args.width > 0 && args.height > 0)
+    {
+        cap->set(cv::CAP_PROP_FRAME_WIDTH,  args.width);
+        cap->set(cv::CAP_PROP_FRAME_HEIGHT, args.height);
+    }
+    if (args.fps > 0)
+        cap->set(cv::CAP_PROP_FPS, args.fps);
+
+    if (!cap->isOpened())
+        goto fail;
+
+    if (!get_frame_())
+        goto fail;
+
+    return true;
+
+fail:
+    stop();
+    return false;
+}
+
+bool cam::get_frame_()
+{
+    if (!is_open())
+        return false;
+
+    for (unsigned i = 0; i < 10; i++)
+    {
+        if (cap->read(mat))
+        {
+            frame_.data = mat.data;
+            frame_.width = mat.cols;
+            frame_.height = mat.rows;
+            frame_.stride = mat.step.p[0];
+            frame_.channels = mat.channels();
+
+            return true;
+        }
+        portable::sleep(50);
+    }
+
+    return false;
+}
+
+std::tuple<const frame&, bool> cam::get_frame()
+{
+    bool ret = get_frame_();
+    return { frame_, ret };
+}
+
+bool cam::show_dialog()
+{
+    if (is_open())
+        return video_property_page::show_from_capture(*cap, idx);
+    else
+        return video_property_page::show(idx);
+}
+
+OTR_REGISTER_CAMERA(metadata)
diff --git a/video-opencv/camera-names.cpp b/video-opencv/camera-names.cpp
new file mode 100644
index 00000000..69926e5a
--- /dev/null
+++ b/video-opencv/camera-names.cpp
@@ -0,0 +1,106 @@
+#include "camera-names.hpp"
+
+#include <algorithm>
+#include <iterator>
+
+#ifdef _WIN32
+#   include <cwchar>
+#   define NO_DSHOW_STRSAFE
+#   include <dshow.h>
+#elif defined(__unix) || defined(__linux) || defined(__APPLE__)
+#   include <unistd.h>
+#endif
+
+#ifdef __linux
+#   include <fcntl.h>
+#   include <sys/ioctl.h>
+#   include <linux/videodev2.h>
+#   include <cerrno>
+#   include <cstring>
+#endif
+
+#include <QDebug>
+
+int camera_name_to_index(const QString &name)
+{
+    auto list = get_camera_names();
+    auto it = std::find(list.cbegin(), list.cend(), name);
+    if (it != list.cend())
+        return std::distance(list.cbegin(), it);
+
+    return -1;
+}
+
+std::vector<QString> get_camera_names()
+{
+    std::vector<QString> ret;
+#ifdef _WIN32
+    // Create the System Device Enumerator.
+    HRESULT hr;
+    CoInitialize(nullptr);
+    ICreateDevEnum *pSysDevEnum = nullptr;
+    hr = CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void **)&pSysDevEnum);
+    if (FAILED(hr))
+    {
+        qDebug() << "failed CLSID_SystemDeviceEnum" << hr;
+        return ret;
+    }
+    // Obtain a class enumerator for the video compressor category.
+    IEnumMoniker *pEnumCat = nullptr;
+    hr = pSysDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnumCat, 0);
+
+    if (hr == S_OK) {
+        // Enumerate the monikers.
+        IMoniker *pMoniker = nullptr;
+        ULONG cFetched;
+        while (pEnumCat->Next(1, &pMoniker, &cFetched) == S_OK)
+        {
+            IPropertyBag *pPropBag;
+            hr = pMoniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, (void **)&pPropBag);
+            if (SUCCEEDED(hr))	{
+                // To retrieve the filter's friendly name, do the following:
+                VARIANT var;
+                VariantInit(&var);
+                hr = pPropBag->Read(L"FriendlyName", &var, nullptr);
+                if (SUCCEEDED(hr))
+                {
+                    // Display the name in your UI somehow.
+                    QString str((QChar*)var.bstrVal, int(std::wcslen(var.bstrVal)));
+                    ret.push_back(str);
+                }
+                VariantClear(&var);
+                pPropBag->Release();
+            }
+            pMoniker->Release();
+        }
+        pEnumCat->Release();
+    }
+    else
+        qDebug() << "failed CLSID_VideoInputDeviceCategory" << hr;
+
+    pSysDevEnum->Release();
+#endif
+
+#ifdef __linux
+    for (int i = 0; i < 16; i++) {
+        char buf[32];
+        snprintf(buf, sizeof(buf), "/dev/video%d", i);
+
+        if (access(buf, R_OK | W_OK) == 0) {
+            int fd = open(buf, O_RDONLY);
+            if (fd == -1)
+                continue;
+            struct v4l2_capability video_cap;
+            if(ioctl(fd, VIDIOC_QUERYCAP, &video_cap) == -1)
+            {
+                qDebug() << "VIDIOC_QUERYCAP" << errno;
+                close(fd);
+                continue;
+            }
+            ret.push_back(QString((const char*)video_cap.card));
+            close(fd);
+        }
+    }
+#endif
+    return ret;
+}
diff --git a/video-opencv/camera-names.hpp b/video-opencv/camera-names.hpp
new file mode 100644
index 00000000..9f0883f5
--- /dev/null
+++ b/video-opencv/camera-names.hpp
@@ -0,0 +1,18 @@
+/* Copyright (c) 2014-2015, Stanislaw Halik <sthalik@misaki.pl>
+
+ * Permission to use, copy, modify, and/or distribute this
+ * software for any purpose with or without fee is hereby granted,
+ * provided that the above copyright notice and this permission
+ * notice appear in all copies.
+ */
+
+#pragma once
+
+#include <vector>
+#include <QString>
+
+#include "export.hpp"
+
+std::vector<QString> get_camera_names();
+int camera_name_to_index(const QString &name);
+
diff --git a/video-opencv/export.hpp b/video-opencv/export.hpp
new file mode 100644
index 00000000..1d43a9f1
--- /dev/null
+++ b/video-opencv/export.hpp
@@ -0,0 +1,11 @@
+// generates export.hpp for each module from compat/linkage.hpp
+
+#pragma once
+
+#include "compat/linkage-macros.hpp"
+
+#ifdef BUILD_VIDEO_OPENCV
+#   define OTR_VIDEO_OPENCV_EXPORT OTR_GENERIC_EXPORT
+#else
+#   define OTR_VIDEO_OPENCV_EXPORT OTR_GENERIC_IMPORT
+#endif
diff --git a/video-opencv/lang/nl_NL.ts b/video-opencv/lang/nl_NL.ts
new file mode 100644
index 00000000..6401616d
--- /dev/null
+++ b/video-opencv/lang/nl_NL.ts
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+</TS>
diff --git a/video-opencv/lang/ru_RU.ts b/video-opencv/lang/ru_RU.ts
new file mode 100644
index 00000000..6401616d
--- /dev/null
+++ b/video-opencv/lang/ru_RU.ts
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+</TS>
diff --git a/video-opencv/lang/stub.ts b/video-opencv/lang/stub.ts
new file mode 100644
index 00000000..6401616d
--- /dev/null
+++ b/video-opencv/lang/stub.ts
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+</TS>
diff --git a/video-opencv/lang/zh_CN.ts b/video-opencv/lang/zh_CN.ts
new file mode 100644
index 00000000..6401616d
--- /dev/null
+++ b/video-opencv/lang/zh_CN.ts
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1">
+</TS>
diff --git a/video-opencv/video-property-page.cpp b/video-opencv/video-property-page.cpp
new file mode 100644
index 00000000..92abd887
--- /dev/null
+++ b/video-opencv/video-property-page.cpp
@@ -0,0 +1,165 @@
+/* Copyright (c) 2016 Stanislaw Halik
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ */
+
+#include "video-property-page.hpp"
+
+#ifdef _WIN32
+
+#include "camera-names.hpp"
+#include "compat/sleep.hpp"
+#include "compat/run-in-thread.hpp"
+#include "compat/library-path.hpp"
+
+#include <cstring>
+
+#include <opencv2/videoio.hpp>
+
+#include <QApplication>
+#include <QProcess>
+#include <QThread>
+#include <QMessageBox>
+
+#include <QDebug>
+
+bool video_property_page::show_from_capture(cv::VideoCapture& cap, int /*index */)
+{
+    return cap.set(cv::CAP_PROP_SETTINGS, 0);
+}
+
+struct prop_settings_worker final : QThread
+{
+    explicit prop_settings_worker(int idx);
+    ~prop_settings_worker() override;
+
+private:
+    void open_prop_page();
+    void run() override;
+
+    cv::VideoCapture cap;
+    int idx = -1;
+};
+
+prop_settings_worker::prop_settings_worker(int idx_)
+{
+    int ret = (int)cap.get(cv::CAP_PROP_SETTINGS);
+
+    if (ret != 0)
+        run_in_thread_async(qApp, [] {
+            QMessageBox::warning(nullptr,
+                                 "Camera properties",
+                                 "Camera dialog already opened",
+                                 QMessageBox::Cancel,
+                                 QMessageBox::NoButton);
+        });
+    else
+    {
+        idx = idx_;
+        // DON'T MOVE IT
+        // ps3 eye will reset to default settings if done from another thread
+        open_prop_page();
+    }
+}
+
+void prop_settings_worker::open_prop_page()
+{
+    cap.open(idx);
+
+    if (cap.isOpened())
+    {
+        cv::Mat tmp;
+
+        for (unsigned k = 0; k < 2000/50; k++)
+        {
+            if (cap.read(tmp))
+            {
+                qDebug() << "got frame" << tmp.rows << tmp.cols;
+                goto ok;
+            }
+            portable::sleep(50);
+        }
+    }
+
+    qDebug() << "property-page: can't open camera";
+    idx = -1;
+
+    return;
+
+ok:
+    portable::sleep(100);
+
+    qDebug() << "property-page: opening for" << idx;
+
+    if (!cap.set(cv::CAP_PROP_SETTINGS, 0))
+    {
+        run_in_thread_async(qApp, [] {
+            QMessageBox::warning(nullptr,
+                                 "Camera properties",
+                                 "Can't open camera dialog",
+                                 QMessageBox::Cancel,
+                                 QMessageBox::NoButton);
+        });
+    }
+}
+
+prop_settings_worker::~prop_settings_worker()
+{
+    if (idx != -1)
+    {
+        // ax filter is race condition-prone
+        portable::sleep(250);
+        cap.release();
+        // idem
+        portable::sleep(250);
+
+        qDebug() << "property-page: closed" << idx;
+    }
+}
+
+void prop_settings_worker::run()
+{
+    if (idx != -1)
+    {
+        while (cap.get(cv::CAP_PROP_SETTINGS) > 0)
+            portable::sleep(1000);
+    }
+}
+
+bool video_property_page::show(int idx)
+{
+    auto thread = new prop_settings_worker(idx);
+
+    // XXX is this a race condition?
+    thread->moveToThread(qApp->thread());
+    QObject::connect(thread, &QThread::finished, qApp, [thread] { thread->deleteLater(); }, Qt::DirectConnection);
+
+    thread->start();
+
+    return true;
+}
+
+#elif defined(__linux)
+#   include <QProcess>
+#   include "compat/camera-names.hpp"
+
+bool video_property_page::show(int idx)
+{
+    const QList<QString> camera_names(get_camera_names());
+
+    if (idx >= 0 && idx < camera_names.size())
+        return QProcess::startDetached("qv4l2", QStringList { "-d", QString("/dev/video%1").arg(idx) });
+    else
+        return false;
+}
+
+bool video_property_page::show_from_capture(cv::VideoCapture&, int idx)
+{
+    return show(idx);
+}
+#else
+bool video_property_page::show(int) { return false; }
+bool video_property_page::show_from_capture(cv::VideoCapture&, int) { return false; }
+#endif
diff --git a/video-opencv/video-property-page.hpp b/video-opencv/video-property-page.hpp
new file mode 100644
index 00000000..c2b9525d
--- /dev/null
+++ b/video-opencv/video-property-page.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <QString>
+#include <opencv2/videoio.hpp>
+
+struct video_property_page final
+{
+    video_property_page() = delete;
+    static bool show(int id);
+    static bool show_from_capture(cv::VideoCapture& cap, int index);
+private:
+};
+
diff --git a/video/camera.cpp b/video/camera.cpp
new file mode 100644
index 00000000..c33ab13a
--- /dev/null
+++ b/video/camera.cpp
@@ -0,0 +1,66 @@
+#include "camera.hpp"
+
+#include <algorithm>
+#include <utility>
+#include <QMutex>
+
+static std::vector<std::unique_ptr<video::impl::camera_>> metadata;
+static QMutex mtx;
+
+namespace video::impl {
+
+camera_::camera_() = default;
+camera_::~camera_() = default;
+
+camera::camera() = default;
+camera::~camera() = default;
+
+void register_camera(std::unique_ptr<impl::camera_> camera)
+{
+    QMutexLocker l(&mtx);
+    metadata.push_back(std::move(camera));
+}
+
+} // ns video::impl
+
+namespace video {
+
+bool show_dialog(const QString& camera_name)
+{
+    QMutexLocker l(&mtx);
+
+    for (auto& camera : metadata)
+        for (const QString& name : camera->camera_names())
+            if (name == camera_name)
+                return camera->show_dialog(camera_name);
+
+    return false;
+}
+
+std::unique_ptr<camera_impl> make_camera(const QString& name)
+{
+    QMutexLocker l(&mtx);
+
+    for (auto& camera : metadata)
+        for (const QString& name_ : camera->camera_names())
+            if (name_ == name)
+                return camera->make_camera(name);
+
+    return nullptr;
+}
+
+std::vector<QString> camera_names()
+{
+    QMutexLocker l(&mtx);
+    std::vector<QString> names; names.reserve(32);
+
+    for (auto& camera : metadata)
+        for (const QString& name : camera->camera_names())
+            if (std::find(names.cbegin(), names.cend(), name) == names.cend())
+                names.push_back(name);
+
+    std::sort(names.begin(), names.end());
+    return names;
+}
+
+} // ns video
diff --git a/video/camera.hpp b/video/camera.hpp
new file mode 100644
index 00000000..c9577933
--- /dev/null
+++ b/video/camera.hpp
@@ -0,0 +1,95 @@
+/* Copyright (c) 2019 Stanislaw Halik <sthalik@misaki.pl>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ */
+
+#pragma once
+
+#include "export.hpp"
+
+#include <memory>
+#include <vector>
+
+#include <QString>
+
+namespace video
+{
+
+struct frame final
+{
+    unsigned char* data = nullptr;
+    int width = 0, height = 0, stride = 0, channels = 0;
+};
+
+} // ns video
+
+namespace video::impl {
+
+using namespace video;
+
+struct camera;
+
+struct OTR_VIDEO_EXPORT camera_
+{
+    camera_();
+    virtual ~camera_();
+
+    virtual std::vector<QString> camera_names() const = 0;
+    virtual std::unique_ptr<camera> make_camera(const QString& name) = 0;
+    [[nodiscard]] virtual bool show_dialog(const QString& camera_name) = 0;
+    virtual bool can_show_dialog(const QString& camera_name) = 0;
+};
+
+struct OTR_VIDEO_EXPORT camera
+{
+    struct info final
+    {
+        int width = 0, height = 0, fps = 0;
+    };
+
+    camera();
+    virtual ~camera();
+
+    [[nodiscard]] virtual bool start(const info& args) = 0;
+    virtual void stop() = 0;
+    virtual bool is_open() = 0;
+
+    virtual std::tuple<const frame&, bool> get_frame() = 0;
+    [[nodiscard]] virtual bool show_dialog() = 0;
+};
+
+OTR_VIDEO_EXPORT
+void register_camera(std::unique_ptr<impl::camera_> metadata);
+
+} // ns video::impl
+
+#define OTR_REGISTER_CAMERA2(type, ctr)                                     \
+    namespace {                                                             \
+        struct init_##ctr                                                   \
+        {                                                                   \
+            static char fuzz;                                               \
+        };                                                                  \
+        char init_##ctr :: fuzz =                                           \
+            (::video::impl::register_camera(std::make_unique<type>()), 0);  \
+    } // anon ns
+
+#define OTR_REGISTER_CAMERA(type)                                           \
+    OTR_REGISTER_CAMERA2(type, __COUNTER__)
+
+namespace video
+{
+using camera_impl = typename impl::camera;
+
+OTR_VIDEO_EXPORT
+std::unique_ptr<camera_impl> make_camera(const QString& name);
+
+OTR_VIDEO_EXPORT
+std::vector<QString> camera_names();
+
+[[nodiscard]]
+OTR_VIDEO_EXPORT
+bool show_dialog(const QString& camera_name);
+
+} // ns video
-- 
cgit v1.2.3