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