main.cc
C++ source, Unicode text, UTF-8 text
1
/*
2
Camera+ for Halium-based GNU/Linux phones
3
Copyright 2026, roundabout-host.com
4
5
This program is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public License as published by
7
the Free Software Foundation, either version 3 of the License, or
8
(at your option) any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
GNU General Public License for more details.
14
15
You should have received a copy of the GNU General Public License
16
along with this program. If not, see <https://www.gnu.org/licenses/>.
17
*/
18
19
#define LIBFEEDBACK_USE_UNSTABLE_API 1
20
21
#include <droidmedia/droidmediacamera.h>
22
#include <cstdio>
23
#include <cstdlib>
24
#include <cstring>
25
#include <gtkmm.h>
26
#include <glibmm.h>
27
#include <memory>
28
#include <vector>
29
#include <libfeedback.h>
30
#include <iostream>
31
#include <thread>
32
#include <chrono>
33
#include <ctime>
34
#include <sstream>
35
#include <iomanip>
36
#include <unordered_map>
37
38
static inline uint8_t clamp(int v) {
39
return v < 0 ? 0 : (v > 255 ? 255 : v);
40
}
41
42
inline void set_expand_in_direction(Gtk::Widget &widget, Gtk::Orientation orientation) {
43
if(orientation == Gtk::Orientation::HORIZONTAL) {
44
widget.set_vexpand(false);
45
widget.set_hexpand(true);
46
} else {
47
widget.set_vexpand(true);
48
widget.set_hexpand(false);
49
}
50
}
51
52
std::string write_parameters(std::unordered_map<std::string, std::string> const parameters) {
53
std::stringstream str;
54
for(auto const ¶m: parameters) {
55
str << param.first << '=' << param.second << ';';
56
}
57
return str.str();
58
}
59
60
std::map<std::string, int> const ORIENTATIONS = {
61
{"", 0},
62
{"normal", 0},
63
{"left-up", 90},
64
{"bottom-up", 180},
65
{"right-up", 270}
66
};
67
68
std::vector<std::pair<int, int>> parse_size_list(std::string const &sizes) {
69
std::vector<std::pair<int, int>> result;
70
std::stringstream str(sizes);
71
while(!str.eof()) {
72
int w, h;
73
str >> w; str.get(); str >> h; str.get();
74
result.emplace_back(w, h);
75
}
76
return result;
77
}
78
79
class AndroCamera : public Glib::Object {
80
private:
81
int camera_id;
82
DroidMediaCameraCallbacks cbs{};
83
DroidMediaBufferQueueCallbacks buffer_cbs{};
84
DroidMediaBufferQueue *bq = nullptr;
85
86
static void buffers_released_cb(void *self) {
87
fprintf(stderr, "buffers_released_cb\n");
88
}
89
90
static bool frame_available_cb(void *self, DroidMediaBuffer *buffer) {
91
auto cam = static_cast<AndroCamera*>(self);
92
93
DroidMediaBufferInfo info{};
94
droid_media_buffer_get_info(buffer, &info);
95
96
DroidMediaBufferYCbCr ycbcr{};
97
if(!droid_media_buffer_lock_ycbcr(buffer, DROID_MEDIA_BUFFER_LOCK_READ, &ycbcr)) {
98
droid_media_buffer_release(buffer, nullptr, nullptr);
99
return true;
100
}
101
auto rgb = std::make_shared<std::vector<uint8_t>>(info.width * info.height * 4);
102
uint8_t *dst = rgb->data();
103
for(int y = 0; y < info.height; ++y) {
104
uint8_t *Yrow = static_cast<uint8_t*>(ycbcr.y) + y * ycbcr.ystride;
105
uint8_t *VUrow = static_cast<uint8_t*>(ycbcr.cb) + (y / 2) * ycbcr.cstride;
106
uint8_t *UVrow = static_cast<uint8_t*>(ycbcr.cr) + (y / 2) * ycbcr.cstride;
107
108
for(int x = 0; x < info.width; ++x) {
109
int yy = Yrow[x];
110
111
int cr = UVrow[(x / 2) * ycbcr.chroma_step] - 128;
112
int cb = VUrow[(x / 2) * ycbcr.chroma_step] - 128;
113
114
int r = yy + ((91881 * cr) >> 16);
115
int g = yy - ((22554 * cb + 46802 * cr) >> 16);
116
int b = yy + ((116130 * cb) >> 16);
117
118
*dst++ = clamp(b);
119
*dst++ = clamp(g);
120
*dst++ = clamp(r);
121
*dst++ = 0xFF;
122
}
123
}
124
125
droid_media_buffer_unlock(buffer);
126
droid_media_buffer_release(buffer, nullptr, nullptr);
127
128
// Send to GTK main thread safely
129
Glib::signal_idle().connect_once([cam, rgb, w = info.width, h = info.height] {
130
cam->signal_frame_available.emit(rgb, w, h);
131
});
132
133
return true;
134
}
135
136
static bool buffer_created_cb(void *self, DroidMediaBuffer *) {
137
return true;
138
}
139
140
static void shutter_cb(void *self) {
141
fprintf(stderr, "shutter_cb\n");
142
}
143
144
static void compressed_image_cb(void *self, DroidMediaData *mem) {
145
if(!mem || !mem->data || mem->size == 0) {
146
fprintf(stderr, "compressed_image_cb: empty image\n");
147
return;
148
}
149
if(auto self_casted = static_cast<AndroCamera*>(self)) {
150
self_casted->signal_compressed_image.emit(mem);
151
}
152
}
153
154
public:
155
DroidMediaCamera *camera = nullptr;
156
sigc::signal<void(DroidMediaData*)> signal_compressed_image;
157
sigc::signal<void(std::shared_ptr<std::vector<uint8_t>>, int, int)> signal_frame_available;
158
std::unordered_map<std::string, std::string> parameters;
159
DroidMediaCameraInfo physical_info;
160
std::vector<std::pair<int, int>> picture_sizes, preview_sizes, video_sizes;
161
162
explicit AndroCamera(int camera_id) : Glib::Object(), camera_id(camera_id) {}
163
164
void connect() {
165
// Provides facing and orientation
166
droid_media_camera_get_info(&physical_info, camera_id);
167
168
camera = droid_media_camera_connect(camera_id);
169
170
cbs.shutter_cb = shutter_cb;
171
cbs.compressed_image_cb = compressed_image_cb;
172
droid_media_camera_set_callbacks(camera, &cbs, this);
173
174
buffer_cbs.buffers_released = buffers_released_cb;
175
buffer_cbs.frame_available = frame_available_cb;
176
buffer_cbs.buffer_created = buffer_created_cb;
177
178
bq = droid_media_camera_get_buffer_queue(camera);
179
droid_media_buffer_queue_set_callbacks(bq, &buffer_cbs, this);
180
181
std::stringstream param;
182
param << droid_media_camera_get_parameters(camera);
183
while(!param.eof()) {
184
std::string key, value;
185
std::getline(param, key, '=');
186
std::getline(param, value, ';');
187
parameters[key] = value;
188
}
189
190
picture_sizes = parse_size_list(parameters.at("picture-size-values"));
191
video_sizes = parse_size_list(parameters.at("video-size-values"));
192
preview_sizes = parse_size_list(parameters.at("preview-size-values"));
193
}
194
195
void disconnect() {
196
droid_media_camera_disconnect(camera);
197
camera = nullptr;
198
cbs = {};
199
buffer_cbs = {};
200
physical_info = {};
201
}
202
};
203
204
class PanoramaCamera : public Gtk::Application {
205
Gtk::Window *window = nullptr;
206
Gtk::Box *box = nullptr, *format_box = nullptr, *format_resolution_control = nullptr, *control_row = nullptr;
207
Gtk::Scale *format_scale = nullptr;
208
Gtk::Revealer *format_bar = nullptr;
209
Gtk::DrawingArea *drawing = nullptr;
210
Gtk::Button *capture_button = nullptr, *format_down_button = nullptr, *format_up_button = nullptr;
211
Gtk::Label *format_label = nullptr;
212
213
Glib::RefPtr<Gtk::Adjustment> format_adjustment;
214
215
std::unique_ptr<AndroCamera> cam;
216
Cairo::RefPtr<Cairo::ImageSurface> surface;
217
Glib::RefPtr<Gio::DBus::Connection> dbus;
218
Glib::RefPtr<Gio::DBus::Proxy> accelerometer;
219
220
Glib::RefPtr<Gtk::CssProvider> css;
221
Glib::RefPtr<Gdk::Surface> window_surface;
222
223
sigc::connection compressed_image_connection, frame_available_connection, accelerometer_connection;
224
225
std::unordered_map<std::string, std::string> parameters;
226
227
int device_rotation_degrees = 0;
228
229
void on_compressed_image(DroidMediaData *mem) {
230
std::vector<uint8_t> image_copy(static_cast<uint8_t*>(mem->data), static_cast<uint8_t*>(mem->data) + mem->size);
231
std::thread([image_copy = std::move(image_copy)]() mutable {
232
auto now = std::chrono::system_clock::now();
233
auto now_time_t = std::chrono::system_clock::to_time_t(now);
234
std::tm local_time = *std::localtime(&now_time_t);
235
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
236
237
std::ostringstream filename;
238
filename.imbue(std::locale::classic());
239
filename << (local_time.tm_year + 1900) << '-'
240
<< std::setw(2) << std::setfill('0') << (local_time.tm_mon + 1) << '-'
241
<< std::setw(2) << std::setfill('0') << local_time.tm_mday << '_'
242
<< std::setw(2) << std::setfill('0') << local_time.tm_hour << '-'
243
<< std::setw(2) << std::setfill('0') << local_time.tm_min << '-'
244
<< std::setw(2) << std::setfill('0') << local_time.tm_sec << '.'
245
<< std::setw(3) << std::setfill('0') << ms.count()
246
<< ".jpeg";
247
FILE *f = fopen(filename.str().c_str(), "wb");
248
if(!f) {
249
perror("fopen");
250
return;
251
}
252
fwrite(image_copy.data(), 1, image_copy.size(), f);
253
fclose(f);
254
fprintf(stderr, "%s written: %zu bytes\n", filename.str().c_str(), image_copy.size());
255
}).detach();
256
}
257
258
void on_frame_available(std::shared_ptr<std::vector<uint8_t>> pixels, int width, int height) {
259
// TODO: fix repeated allocations
260
if(!(width && height && pixels)) {
261
return;
262
}
263
int actual_orientation = (cam->physical_info.orientation + device_rotation_degrees) % 360;
264
if(actual_orientation == 0) {
265
surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, width, height);
266
267
uint8_t *dst = surface->get_data();
268
const uint8_t *src = pixels->data();
269
int stride = surface->get_stride();
270
271
for(int y = 0; y < height; ++y) {
272
memcpy(dst + y * stride, src + y * width * 4, width * 4);
273
}
274
275
surface->mark_dirty();
276
drawing->queue_draw();
277
} else if(actual_orientation == 180) {
278
surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, width, height);
279
280
uint8_t *dst = surface->get_data();
281
const uint8_t *src = pixels->data();
282
int stride = surface->get_stride();
283
284
for(int y = 0; y < height; ++y) {
285
for(int x = 0; x < width; ++x) {
286
const uint8_t *src_px = src + ((height - 1 - y) * width + (width - 1 - x)) * 4;
287
uint8_t *dst_px = dst + y * stride + x * 4;
288
memcpy(dst_px, src_px, 4);
289
}
290
}
291
292
surface->mark_dirty();
293
drawing->queue_draw();
294
} else if(actual_orientation == 90) {
295
surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, height, width);
296
297
uint8_t *dst = surface->get_data();
298
const uint8_t *src = pixels->data();
299
int stride = surface->get_stride();
300
301
for(int y = 0; y < width; ++y) {
302
for(int x = 0; x < height; ++x) {
303
const uint8_t *src_px = src + ((height - 1 - x) * width + y) * 4;
304
uint8_t *dst_px = dst + y * stride + x * 4;
305
memcpy(dst_px, src_px, 4);
306
}
307
}
308
309
surface->mark_dirty();
310
drawing->queue_draw();
311
} else if(actual_orientation == 270) {
312
surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, height, width);
313
314
uint8_t *dst = surface->get_data();
315
const uint8_t *src = pixels->data();
316
int stride = surface->get_stride();
317
318
for(int y = 0; y < width; ++y) {
319
for(int x = 0; x < height; ++x) {
320
const uint8_t *src_px = src + ((height - 1 - x) * width + y) * 4;
321
uint8_t *dst_px = dst + (width - y - 1) * stride + (height - x - 1) * 4;
322
memcpy(dst_px, src_px, 4);
323
}
324
}
325
326
surface->mark_dirty();
327
drawing->queue_draw();
328
}
329
}
330
331
public:
332
PanoramaCamera() : Gtk::Application("com.roundabout_host.roundabout.Camera", Gio::Application::Flags::NONE) {}
333
334
static Glib::RefPtr<PanoramaCamera> create() {
335
return Glib::make_refptr_for_instance<PanoramaCamera>(new PanoramaCamera());
336
}
337
338
void on_startup() override {
339
// TODO: i18n
340
Gtk::Application::on_startup();
341
dbus = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::SYSTEM);
342
droid_media_init();
343
lfb_init("com.roundabout_host.roundabout.Camera", nullptr);
344
css = Gtk::CssProvider::create();
345
css->load_from_string(
346
".panorama-camera-window { "
347
"background-color: #000000; "
348
"}"
349
);
350
Gtk::StyleContext::add_provider_for_display(Gdk::Display::get_default(), css, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
351
352
int count = droid_media_camera_get_number_of_cameras();
353
if(count < 1) {
354
fprintf(stderr, "No cameras available!\n");
355
return;
356
}
357
358
cam = std::make_unique<AndroCamera>(0);
359
cam->connect();
360
361
format_adjustment = Gtk::Adjustment::create(0, 0, cam->picture_sizes.size() - 1);
362
format_adjustment->signal_value_changed().connect([this]() {
363
int value = format_adjustment->get_value(), w = cam->picture_sizes.at(cam->picture_sizes.size() - 1 - value).first, h = cam->picture_sizes.at(cam->picture_sizes.size() - 1 - value).second;
364
droid_media_camera_stop_preview(cam->camera);
365
parameters["picture-size"] = std::to_string(w) + "x" + std::to_string(h);
366
367
std::string new_params = write_parameters(parameters);
368
droid_media_camera_set_parameters(cam->camera, new_params.c_str());
369
droid_media_camera_start_preview(cam->camera);
370
droid_media_camera_set_parameters(cam->camera, new_params.c_str());
371
if(format_label) {
372
format_label->set_text(std::to_string(w) + "×" + std::to_string(h));
373
}
374
std::cerr << w << 'x' << h << '\n';
375
});
376
377
std::cout << droid_media_camera_get_parameters(cam->camera) << '\n';
378
379
parameters["picture-size"] = "1920x1080";
380
parameters["preview-size"] = "1024x768";
381
parameters["video-size"] = "176x144";
382
parameters["sensitivity"] = "100";
383
parameters["exposure-time"] = "83200";
384
parameters["flash-mode"] = "off";
385
386
droid_media_camera_set_parameters(
387
cam->camera,
388
write_parameters(parameters).c_str()
389
);
390
391
compressed_image_connection = cam->signal_compressed_image.connect(sigc::mem_fun(*this, &PanoramaCamera::on_compressed_image));
392
frame_available_connection = cam->signal_frame_available.connect(sigc::mem_fun(*this, &PanoramaCamera::on_frame_available));
393
394
if(!cam->camera) {
395
fprintf(stderr, "Failed to connect camera!\n");
396
return;
397
}
398
399
if(!droid_media_camera_start_preview(cam->camera)) {
400
fprintf(stderr, "start_preview failed\n");
401
cam->disconnect();
402
return;
403
}
404
405
droid_media_camera_set_parameters(
406
cam->camera,
407
write_parameters(parameters).c_str()
408
);
409
410
fprintf(stderr, "start_preview OK\n");
411
}
412
413
void set_gui_orientation(Gtk::Orientation orientation) {
414
box->set_orientation(orientation);
415
auto reverse_orientation = orientation == Gtk::Orientation::HORIZONTAL? Gtk::Orientation::VERTICAL : Gtk::Orientation::HORIZONTAL;
416
417
control_row->set_orientation(reverse_orientation);
418
format_scale->set_orientation(reverse_orientation);
419
format_resolution_control->set_orientation(reverse_orientation);
420
421
set_expand_in_direction(*format_bar, reverse_orientation);
422
set_expand_in_direction(*format_scale, reverse_orientation);
423
424
if(orientation == Gtk::Orientation::HORIZONTAL) {
425
format_scale->set_inverted(true);
426
format_bar->set_transition_type(Gtk::RevealerTransitionType::SLIDE_LEFT);
427
format_down_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-down-symbolic")));
428
format_up_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-up-symbolic")));
429
format_resolution_control->reorder_child_after(*format_down_button, *format_scale);
430
format_resolution_control->reorder_child_at_start(*format_up_button);
431
} else {
432
format_scale->set_inverted(false);
433
format_bar->set_transition_type(Gtk::RevealerTransitionType::SLIDE_UP);
434
format_down_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
435
format_up_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
436
format_resolution_control->reorder_child_after(*format_up_button, *format_scale);
437
format_resolution_control->reorder_child_at_start(*format_down_button);
438
}
439
}
440
441
void set_window_size() {
442
if(window_surface->get_width() > window_surface->get_height()) {
443
set_gui_orientation(Gtk::Orientation::HORIZONTAL);
444
} else {
445
set_gui_orientation(Gtk::Orientation::VERTICAL);
446
}
447
}
448
449
void on_orientation_changed(std::string const &orientation) {
450
device_rotation_degrees = ORIENTATIONS.at(orientation);
451
int actual_orientation = (cam->physical_info.orientation + device_rotation_degrees) % 360;
452
std::cout << "rotation is " << actual_orientation << " degrees!\n";
453
parameters["jpeg-orientation"] = std::to_string(actual_orientation);
454
droid_media_camera_set_parameters(cam->camera, write_parameters(parameters).c_str());
455
}
456
457
void on_activate() override {
458
Gtk::Application::on_activate();
459
window = Gtk::make_managed<Gtk::Window>();
460
auto settings = window->get_settings();
461
settings->property_gtk_application_prefer_dark_theme().set_value(true);
462
drawing = Gtk::make_managed<Gtk::DrawingArea>();
463
464
drawing->set_draw_func([this](const Cairo::RefPtr<Cairo::Context>& cr, int w, int h) {
465
if(surface) {
466
double scale = std::min(double(w) / surface->get_width(),
467
double(h) / surface->get_height());
468
double offset_x = (w - surface->get_width() * scale) / 2;
469
double offset_y = (h - surface->get_height() * scale) / 2;
470
cr->translate(offset_x, offset_y);
471
cr->scale(scale, scale);
472
cr->set_source(surface, 0, 0);
473
cr->paint();
474
}
475
});
476
477
window->add_css_class("panorama-camera-window");
478
box = Gtk::make_managed<Gtk::Box>();
479
box->append(*drawing);
480
control_row = Gtk::make_managed<Gtk::Box>();
481
482
format_bar = Gtk::make_managed<Gtk::Revealer>();
483
auto format_button = Gtk::make_managed<Gtk::ToggleButton>();
484
format_label = Gtk::make_managed<Gtk::Label>();
485
format_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("image-x-generic-symbolic")));
486
format_button->signal_clicked().connect([this, format_button]() {
487
format_bar->set_reveal_child(format_button->get_active());
488
});
489
format_scale = Gtk::make_managed<Gtk::Scale>();
490
format_scale->set_adjustment(format_adjustment);
491
format_scale->set_round_digits(0);
492
format_box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL);
493
format_resolution_control = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL);
494
format_down_button = Gtk::make_managed<Gtk::Button>();
495
format_down_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-previous-symbolic")));
496
format_down_button->signal_clicked().connect([this]() {
497
format_adjustment->set_value(std::max(format_adjustment->get_lower(), format_adjustment->get_value() - 1));
498
});
499
format_up_button = Gtk::make_managed<Gtk::Button>();
500
format_up_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("go-next-symbolic")));
501
format_up_button->signal_clicked().connect([this]() {
502
format_adjustment->set_value(std::min(format_adjustment->get_upper(), format_adjustment->get_value() + 1));
503
});
504
format_resolution_control->append(*format_down_button);
505
format_resolution_control->append(*format_scale);
506
format_resolution_control->append(*format_up_button);
507
format_box->append(*format_label);
508
format_box->append(*format_resolution_control);
509
format_bar->set_child(*format_box);
510
format_bar->set_hexpand(true);
511
control_row->append(*format_button);
512
box->append(*format_bar);
513
514
format_adjustment->set_value(format_adjustment->get_upper());
515
516
box->append(*control_row);
517
capture_button = Gtk::make_managed<Gtk::Button>();
518
capture_button->set_child(*Gtk::make_managed<Gtk::Image>(Gio::Icon::create("camera-photo-symbolic")));
519
capture_button->signal_clicked().connect([this]() {
520
LfbEvent *event = lfb_event_new("camera-shutter");
521
lfb_event_trigger_feedback(event, NULL);
522
g_object_unref(event);
523
droid_media_camera_take_picture(cam->camera, 0);
524
});
525
box->append(*capture_button);
526
drawing->set_hexpand(true);
527
drawing->set_vexpand(true);
528
window->set_child(*box);
529
add_window(*window);
530
window->present();
531
window_surface = window->get_surface();
532
window_surface->property_width().signal_changed().connect(sigc::mem_fun(*this, &PanoramaCamera::set_window_size));
533
window_surface->property_height().signal_changed().connect(sigc::mem_fun(*this, &PanoramaCamera::set_window_size));
534
535
for(auto e: cam->picture_sizes) {
536
std::cout << e.first << "*" << e.second << '\n';
537
}
538
539
// To support screen rotation
540
accelerometer = Gio::DBus::Proxy::create_sync(
541
dbus,
542
"net.hadess.SensorProxy",
543
"/net/hadess/SensorProxy",
544
"net.hadess.SensorProxy"
545
);
546
Glib::VariantBase value;
547
accelerometer->get_cached_property(value, "AccelerometerOrientation");
548
549
if(value) {
550
auto orientation = value.get_dynamic<std::string>();
551
on_orientation_changed(orientation);
552
}
553
accelerometer->call_sync("ClaimAccelerometer");
554
accelerometer_connection = accelerometer->signal_properties_changed().connect([this](const Gio::DBus::Proxy::MapChangedProperties& changed, const std::vector<Glib::ustring>&) {
555
auto it = changed.find("AccelerometerOrientation");
556
if(it != changed.end()) {
557
auto orientation = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring>>(it->second).get();
558
on_orientation_changed(orientation);
559
}
560
}
561
);
562
}
563
564
void on_shutdown() override {
565
Gtk::Application::on_shutdown();
566
accelerometer->call_sync("ReleaseAccelerometer");
567
if(accelerometer_connection) {
568
accelerometer_connection.disconnect();
569
}
570
if(frame_available_connection) {
571
frame_available_connection.disconnect();
572
}
573
if(compressed_image_connection) {
574
compressed_image_connection.disconnect();
575
}
576
droid_media_camera_stop_preview(cam->camera);
577
cam->disconnect();
578
}
579
};
580
581
int main(int argc, char *argv[]) {
582
auto app = PanoramaCamera::create();
583
return app->run(argc, argv);
584
}
585
586