Important information: Google announced that, from September 2026, Android devices will require ALL apps to be signed by Google, effectively leading to an iOS situation. Value your right to a computer that does what you want; do not tolerate this monopolistic practice! Contact me if you don't understand why it is bad. Click to learn more.

 main.cc

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