summaryrefslogtreecommitdiff
path: root/src/apps
diff options
context:
space:
mode:
authorLaurent Pinchart <laurent.pinchart@ideasonboard.com>2022-10-20 00:44:55 +0300
committerLaurent Pinchart <laurent.pinchart@ideasonboard.com>2022-10-20 13:36:25 +0300
commit84ad104499d9efc0253dae1a60ee070ed375ad95 (patch)
treed10fd53eb79cebb28fa3f72b18b46dddb6382b83 /src/apps
parentdaf3f4b59f4ea0ecb42c6a39fe909f071d3a2842 (diff)
Move test applications to src/apps/
The cam and qcam test application share code, currently through a crude hack that references the cam source files directly from the qcam meson.build file. To prepare for the introduction of hosting that code in a static library, move all applications to src/apps/. Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com> Reviewed-by: Paul Elder <paul.elder@ideasonboard.com> Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
Diffstat (limited to 'src/apps')
-rw-r--r--src/apps/cam/camera_session.cpp450
-rw-r--r--src/apps/cam/camera_session.h79
-rw-r--r--src/apps/cam/capture-script.yaml71
-rw-r--r--src/apps/cam/capture_script.cpp535
-rw-r--r--src/apps/cam/capture_script.h68
-rw-r--r--src/apps/cam/dng_writer.cpp653
-rw-r--r--src/apps/cam/dng_writer.h27
-rw-r--r--src/apps/cam/drm.cpp717
-rw-r--r--src/apps/cam/drm.h334
-rw-r--r--src/apps/cam/event_loop.cpp150
-rw-r--r--src/apps/cam/event_loop.h68
-rw-r--r--src/apps/cam/file_sink.cpp137
-rw-r--r--src/apps/cam/file_sink.h43
-rw-r--r--src/apps/cam/frame_sink.cpp67
-rw-r--r--src/apps/cam/frame_sink.h32
-rw-r--r--src/apps/cam/image.cpp109
-rw-r--r--src/apps/cam/image.h50
-rw-r--r--src/apps/cam/kms_sink.cpp538
-rw-r--r--src/apps/cam/kms_sink.h83
-rw-r--r--src/apps/cam/main.cpp362
-rw-r--r--src/apps/cam/main.h26
-rw-r--r--src/apps/cam/meson.build74
-rw-r--r--src/apps/cam/options.cpp1141
-rw-r--r--src/apps/cam/options.h157
-rw-r--r--src/apps/cam/sdl_sink.cpp214
-rw-r--r--src/apps/cam/sdl_sink.h48
-rw-r--r--src/apps/cam/sdl_texture.cpp36
-rw-r--r--src/apps/cam/sdl_texture.h30
-rw-r--r--src/apps/cam/sdl_texture_mjpg.cpp83
-rw-r--r--src/apps/cam/sdl_texture_mjpg.h23
-rw-r--r--src/apps/cam/sdl_texture_yuv.cpp33
-rw-r--r--src/apps/cam/sdl_texture_yuv.h26
-rw-r--r--src/apps/cam/stream_options.cpp134
-rw-r--r--src/apps/cam/stream_options.h28
-rw-r--r--src/apps/lc-compliance/capture_test.cpp128
-rw-r--r--src/apps/lc-compliance/environment.cpp22
-rw-r--r--src/apps/lc-compliance/environment.h27
-rw-r--r--src/apps/lc-compliance/main.cpp193
-rw-r--r--src/apps/lc-compliance/meson.build31
-rw-r--r--src/apps/lc-compliance/simple_capture.cpp191
-rw-r--r--src/apps/lc-compliance/simple_capture.h65
-rw-r--r--src/apps/meson.build6
-rw-r--r--src/apps/qcam/assets/feathericons/activity.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/airplay.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/alert-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/alert-octagon.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/alert-triangle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/align-center.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/align-justify.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/align-left.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/align-right.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/anchor.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/aperture.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/archive.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-down-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-down-left.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-down-right.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-down.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-left-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-left.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-right-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-right.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-up-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-up-left.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-up-right.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/arrow-up.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/at-sign.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/award.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/bar-chart-2.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/bar-chart.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/battery-charging.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/battery.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/bell-off.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/bell.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/bluetooth.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/bold.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/book-open.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/book.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/bookmark.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/box.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/briefcase.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/calendar.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/camera-off.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/camera.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/cast.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/check-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/check-square.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/check.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/chevron-down.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/chevron-left.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/chevron-right.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/chevron-up.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/chevrons-down.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/chevrons-left.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/chevrons-right.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/chevrons-up.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/chrome.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/clipboard.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/clock.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/cloud-drizzle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/cloud-lightning.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/cloud-off.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/cloud-rain.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/cloud-snow.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/cloud.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/code.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/codepen.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/codesandbox.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/coffee.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/columns.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/command.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/compass.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/copy.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/corner-down-left.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/corner-down-right.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/corner-left-down.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/corner-left-up.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/corner-right-down.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/corner-right-up.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/corner-up-left.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/corner-up-right.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/cpu.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/credit-card.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/crop.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/crosshair.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/database.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/delete.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/disc.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/dollar-sign.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/download-cloud.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/download.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/droplet.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/edit-2.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/edit-3.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/edit.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/external-link.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/eye-off.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/eye.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/facebook.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/fast-forward.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/feather.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/feathericons.qrc11
-rw-r--r--src/apps/qcam/assets/feathericons/figma.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/file-minus.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/file-plus.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/file-text.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/file.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/film.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/filter.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/flag.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/folder-minus.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/folder-plus.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/folder.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/framer.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/frown.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/gift.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/git-branch.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/git-commit.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/git-merge.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/git-pull-request.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/github.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/gitlab.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/globe.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/grid.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/hard-drive.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/hash.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/headphones.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/heart.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/help-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/hexagon.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/home.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/image.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/inbox.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/info.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/instagram.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/italic.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/key.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/layers.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/layout.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/life-buoy.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/link-2.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/link.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/linkedin.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/list.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/loader.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/lock.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/log-in.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/log-out.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/mail.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/map-pin.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/map.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/maximize-2.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/maximize.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/meh.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/menu.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/message-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/message-square.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/mic-off.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/mic.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/minimize-2.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/minimize.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/minus-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/minus-square.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/minus.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/monitor.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/moon.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/more-horizontal.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/more-vertical.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/mouse-pointer.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/move.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/music.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/navigation-2.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/navigation.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/octagon.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/package.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/paperclip.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/pause-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/pause.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/pen-tool.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/percent.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/phone-call.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/phone-forwarded.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/phone-incoming.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/phone-missed.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/phone-off.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/phone-outgoing.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/phone.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/pie-chart.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/play-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/play.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/plus-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/plus-square.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/plus.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/pocket.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/power.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/printer.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/radio.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/refresh-ccw.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/refresh-cw.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/repeat.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/rewind.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/rotate-ccw.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/rotate-cw.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/rss.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/save.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/scissors.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/search.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/send.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/server.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/settings.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/share-2.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/share.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/shield-off.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/shield.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/shopping-bag.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/shopping-cart.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/shuffle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/sidebar.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/skip-back.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/skip-forward.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/slack.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/slash.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/sliders.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/smartphone.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/smile.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/speaker.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/square.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/star.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/stop-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/sun.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/sunrise.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/sunset.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/tablet.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/tag.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/target.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/terminal.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/thermometer.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/thumbs-down.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/thumbs-up.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/toggle-left.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/toggle-right.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/tool.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/trash-2.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/trash.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/trello.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/trending-down.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/trending-up.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/triangle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/truck.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/tv.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/twitch.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/twitter.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/type.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/umbrella.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/underline.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/unlock.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/upload-cloud.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/upload.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/user-check.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/user-minus.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/user-plus.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/user-x.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/user.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/users.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/video-off.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/video.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/voicemail.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/volume-1.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/volume-2.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/volume-x.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/volume.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/watch.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/wifi-off.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/wifi.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/wind.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/x-circle.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/x-octagon.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/x-square.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/x.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/youtube.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/zap-off.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/zap.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/zoom-in.svg1
-rw-r--r--src/apps/qcam/assets/feathericons/zoom-out.svg1
-rw-r--r--src/apps/qcam/assets/shader/RGB.frag22
-rw-r--r--src/apps/qcam/assets/shader/YUV_2_planes.frag42
-rw-r--r--src/apps/qcam/assets/shader/YUV_3_planes.frag36
-rw-r--r--src/apps/qcam/assets/shader/YUV_packed.frag83
-rw-r--r--src/apps/qcam/assets/shader/bayer_1x_packed.frag216
-rw-r--r--src/apps/qcam/assets/shader/bayer_8.frag107
-rw-r--r--src/apps/qcam/assets/shader/bayer_8.vert51
-rw-r--r--src/apps/qcam/assets/shader/identity.vert18
-rw-r--r--src/apps/qcam/assets/shader/shaders.qrc13
-rw-r--r--src/apps/qcam/cam_select_dialog.cpp111
-rw-r--r--src/apps/qcam/cam_select_dialog.h47
-rw-r--r--src/apps/qcam/format_converter.cpp359
-rw-r--r--src/apps/qcam/format_converter.h62
-rw-r--r--src/apps/qcam/main.cpp89
-rw-r--r--src/apps/qcam/main_window.cpp790
-rw-r--r--src/apps/qcam/main_window.h133
-rw-r--r--src/apps/qcam/meson.build83
-rw-r--r--src/apps/qcam/message_handler.cpp27
-rw-r--r--src/apps/qcam/message_handler.h24
-rw-r--r--src/apps/qcam/viewfinder.h34
-rw-r--r--src/apps/qcam/viewfinder_gl.cpp835
-rw-r--r--src/apps/qcam/viewfinder_gl.h108
-rw-r--r--src/apps/qcam/viewfinder_qt.cpp181
-rw-r--r--src/apps/qcam/viewfinder_qt.h64
349 files changed, 11117 insertions, 0 deletions
diff --git a/src/apps/cam/camera_session.cpp b/src/apps/cam/camera_session.cpp
new file mode 100644
index 00000000..6b409c98
--- /dev/null
+++ b/src/apps/cam/camera_session.cpp
@@ -0,0 +1,450 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * camera_session.cpp - Camera capture session
+ */
+
+#include <iomanip>
+#include <iostream>
+#include <limits.h>
+#include <sstream>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/property_ids.h>
+
+#include "camera_session.h"
+#include "capture_script.h"
+#include "event_loop.h"
+#include "file_sink.h"
+#ifdef HAVE_KMS
+#include "kms_sink.h"
+#endif
+#include "main.h"
+#ifdef HAVE_SDL
+#include "sdl_sink.h"
+#endif
+#include "stream_options.h"
+
+using namespace libcamera;
+
+CameraSession::CameraSession(CameraManager *cm,
+ const std::string &cameraId,
+ unsigned int cameraIndex,
+ const OptionsParser::Options &options)
+ : options_(options), cameraIndex_(cameraIndex), last_(0),
+ queueCount_(0), captureCount_(0), captureLimit_(0),
+ printMetadata_(false)
+{
+ char *endptr;
+ unsigned long index = strtoul(cameraId.c_str(), &endptr, 10);
+ if (*endptr == '\0' && index > 0 && index <= cm->cameras().size())
+ camera_ = cm->cameras()[index - 1];
+ else
+ camera_ = cm->get(cameraId);
+
+ if (!camera_) {
+ std::cerr << "Camera " << cameraId << " not found" << std::endl;
+ return;
+ }
+
+ if (camera_->acquire()) {
+ std::cerr << "Failed to acquire camera " << cameraId
+ << std::endl;
+ return;
+ }
+
+ StreamRoles roles = StreamKeyValueParser::roles(options_[OptStream]);
+
+ std::unique_ptr<CameraConfiguration> config =
+ camera_->generateConfiguration(roles);
+ if (!config || config->size() != roles.size()) {
+ std::cerr << "Failed to get default stream configuration"
+ << std::endl;
+ return;
+ }
+
+ /* Apply configuration if explicitly requested. */
+ if (StreamKeyValueParser::updateConfiguration(config.get(),
+ options_[OptStream])) {
+ std::cerr << "Failed to update configuration" << std::endl;
+ return;
+ }
+
+ bool strictFormats = options_.isSet(OptStrictFormats);
+
+#ifdef HAVE_KMS
+ if (options_.isSet(OptDisplay)) {
+ if (options_.isSet(OptFile)) {
+ std::cerr << "--display and --file options are mutually exclusive"
+ << std::endl;
+ return;
+ }
+
+ if (roles.size() != 1) {
+ std::cerr << "Display doesn't support multiple streams"
+ << std::endl;
+ return;
+ }
+
+ if (roles[0] != StreamRole::Viewfinder) {
+ std::cerr << "Display requires a viewfinder stream"
+ << std::endl;
+ return;
+ }
+ }
+#endif
+
+ if (options_.isSet(OptCaptureScript)) {
+ std::string scriptName = options_[OptCaptureScript].toString();
+ script_ = std::make_unique<CaptureScript>(camera_, scriptName);
+ if (!script_->valid()) {
+ std::cerr << "Invalid capture script '" << scriptName
+ << "'" << std::endl;
+ return;
+ }
+ }
+
+ switch (config->validate()) {
+ case CameraConfiguration::Valid:
+ break;
+
+ case CameraConfiguration::Adjusted:
+ if (strictFormats) {
+ std::cout << "Adjusting camera configuration disallowed by --strict-formats argument"
+ << std::endl;
+ return;
+ }
+ std::cout << "Camera configuration adjusted" << std::endl;
+ break;
+
+ case CameraConfiguration::Invalid:
+ std::cout << "Camera configuration invalid" << std::endl;
+ return;
+ }
+
+ config_ = std::move(config);
+}
+
+CameraSession::~CameraSession()
+{
+ if (camera_)
+ camera_->release();
+}
+
+void CameraSession::listControls() const
+{
+ for (const auto &[id, info] : camera_->controls()) {
+ std::cout << "Control: " << id->name() << ": "
+ << info.toString() << std::endl;
+ }
+}
+
+void CameraSession::listProperties() const
+{
+ for (const auto &[key, value] : camera_->properties()) {
+ const ControlId *id = properties::properties.at(key);
+
+ std::cout << "Property: " << id->name() << " = "
+ << value.toString() << std::endl;
+ }
+}
+
+void CameraSession::infoConfiguration() const
+{
+ unsigned int index = 0;
+ for (const StreamConfiguration &cfg : *config_) {
+ std::cout << index << ": " << cfg.toString() << std::endl;
+
+ const StreamFormats &formats = cfg.formats();
+ for (PixelFormat pixelformat : formats.pixelformats()) {
+ std::cout << " * Pixelformat: "
+ << pixelformat << " "
+ << formats.range(pixelformat).toString()
+ << std::endl;
+
+ for (const Size &size : formats.sizes(pixelformat))
+ std::cout << " - " << size << std::endl;
+ }
+
+ index++;
+ }
+}
+
+int CameraSession::start()
+{
+ int ret;
+
+ queueCount_ = 0;
+ captureCount_ = 0;
+ captureLimit_ = options_[OptCapture].toInteger();
+ printMetadata_ = options_.isSet(OptMetadata);
+
+ ret = camera_->configure(config_.get());
+ if (ret < 0) {
+ std::cout << "Failed to configure camera" << std::endl;
+ return ret;
+ }
+
+ streamNames_.clear();
+ for (unsigned int index = 0; index < config_->size(); ++index) {
+ StreamConfiguration &cfg = config_->at(index);
+ streamNames_[cfg.stream()] = "cam" + std::to_string(cameraIndex_)
+ + "-stream" + std::to_string(index);
+ }
+
+ camera_->requestCompleted.connect(this, &CameraSession::requestComplete);
+
+#ifdef HAVE_KMS
+ if (options_.isSet(OptDisplay))
+ sink_ = std::make_unique<KMSSink>(options_[OptDisplay].toString());
+#endif
+
+#ifdef HAVE_SDL
+ if (options_.isSet(OptSDL))
+ sink_ = std::make_unique<SDLSink>();
+#endif
+
+ if (options_.isSet(OptFile)) {
+ if (!options_[OptFile].toString().empty())
+ sink_ = std::make_unique<FileSink>(camera_.get(), streamNames_,
+ options_[OptFile]);
+ else
+ sink_ = std::make_unique<FileSink>(camera_.get(), streamNames_);
+ }
+
+ if (sink_) {
+ ret = sink_->configure(*config_);
+ if (ret < 0) {
+ std::cout << "Failed to configure frame sink"
+ << std::endl;
+ return ret;
+ }
+
+ sink_->requestProcessed.connect(this, &CameraSession::sinkRelease);
+ }
+
+ allocator_ = std::make_unique<FrameBufferAllocator>(camera_);
+
+ return startCapture();
+}
+
+void CameraSession::stop()
+{
+ int ret = camera_->stop();
+ if (ret)
+ std::cout << "Failed to stop capture" << std::endl;
+
+ if (sink_) {
+ ret = sink_->stop();
+ if (ret)
+ std::cout << "Failed to stop frame sink" << std::endl;
+ }
+
+ sink_.reset();
+
+ requests_.clear();
+
+ allocator_.reset();
+}
+
+int CameraSession::startCapture()
+{
+ int ret;
+
+ /* Identify the stream with the least number of buffers. */
+ unsigned int nbuffers = UINT_MAX;
+ for (StreamConfiguration &cfg : *config_) {
+ ret = allocator_->allocate(cfg.stream());
+ if (ret < 0) {
+ std::cerr << "Can't allocate buffers" << std::endl;
+ return -ENOMEM;
+ }
+
+ unsigned int allocated = allocator_->buffers(cfg.stream()).size();
+ nbuffers = std::min(nbuffers, allocated);
+ }
+
+ /*
+ * TODO: make cam tool smarter to support still capture by for
+ * example pushing a button. For now run all streams all the time.
+ */
+
+ for (unsigned int i = 0; i < nbuffers; i++) {
+ std::unique_ptr<Request> request = camera_->createRequest();
+ if (!request) {
+ std::cerr << "Can't create request" << std::endl;
+ return -ENOMEM;
+ }
+
+ for (StreamConfiguration &cfg : *config_) {
+ Stream *stream = cfg.stream();
+ const std::vector<std::unique_ptr<FrameBuffer>> &buffers =
+ allocator_->buffers(stream);
+ const std::unique_ptr<FrameBuffer> &buffer = buffers[i];
+
+ ret = request->addBuffer(stream, buffer.get());
+ if (ret < 0) {
+ std::cerr << "Can't set buffer for request"
+ << std::endl;
+ return ret;
+ }
+
+ if (sink_)
+ sink_->mapBuffer(buffer.get());
+ }
+
+ requests_.push_back(std::move(request));
+ }
+
+ if (sink_) {
+ ret = sink_->start();
+ if (ret) {
+ std::cout << "Failed to start frame sink" << std::endl;
+ return ret;
+ }
+ }
+
+ ret = camera_->start();
+ if (ret) {
+ std::cout << "Failed to start capture" << std::endl;
+ if (sink_)
+ sink_->stop();
+ return ret;
+ }
+
+ for (std::unique_ptr<Request> &request : requests_) {
+ ret = queueRequest(request.get());
+ if (ret < 0) {
+ std::cerr << "Can't queue request" << std::endl;
+ camera_->stop();
+ if (sink_)
+ sink_->stop();
+ return ret;
+ }
+ }
+
+ if (captureLimit_)
+ std::cout << "cam" << cameraIndex_
+ << ": Capture " << captureLimit_ << " frames"
+ << std::endl;
+ else
+ std::cout << "cam" << cameraIndex_
+ << ": Capture until user interrupts by SIGINT"
+ << std::endl;
+
+ return 0;
+}
+
+int CameraSession::queueRequest(Request *request)
+{
+ if (captureLimit_ && queueCount_ >= captureLimit_)
+ return 0;
+
+ if (script_)
+ request->controls() = script_->frameControls(queueCount_);
+
+ queueCount_++;
+
+ return camera_->queueRequest(request);
+}
+
+void CameraSession::requestComplete(Request *request)
+{
+ if (request->status() == Request::RequestCancelled)
+ return;
+
+ /*
+ * Defer processing of the completed request to the event loop, to avoid
+ * blocking the camera manager thread.
+ */
+ EventLoop::instance()->callLater([=]() { processRequest(request); });
+}
+
+void CameraSession::processRequest(Request *request)
+{
+ /*
+ * If we've reached the capture limit, we're done. This doesn't
+ * duplicate the check below that emits the captureDone signal, as this
+ * function will be called for each request still in flight after the
+ * capture limit is reached and we don't want to emit the signal every
+ * single time.
+ */
+ if (captureLimit_ && captureCount_ >= captureLimit_)
+ return;
+
+ const Request::BufferMap &buffers = request->buffers();
+
+ /*
+ * Compute the frame rate. The timestamp is arbitrarily retrieved from
+ * the first buffer, as all buffers should have matching timestamps.
+ */
+ uint64_t ts = buffers.begin()->second->metadata().timestamp;
+ double fps = ts - last_;
+ fps = last_ != 0 && fps ? 1000000000.0 / fps : 0.0;
+ last_ = ts;
+
+ bool requeue = true;
+
+ std::stringstream info;
+ info << ts / 1000000000 << "."
+ << std::setw(6) << std::setfill('0') << ts / 1000 % 1000000
+ << " (" << std::fixed << std::setprecision(2) << fps << " fps)";
+
+ for (const auto &[stream, buffer] : buffers) {
+ const FrameMetadata &metadata = buffer->metadata();
+
+ info << " " << streamNames_[stream]
+ << " seq: " << std::setw(6) << std::setfill('0') << metadata.sequence
+ << " bytesused: ";
+
+ unsigned int nplane = 0;
+ for (const FrameMetadata::Plane &plane : metadata.planes()) {
+ info << plane.bytesused;
+ if (++nplane < metadata.planes().size())
+ info << "/";
+ }
+ }
+
+ if (sink_) {
+ if (!sink_->processRequest(request))
+ requeue = false;
+ }
+
+ std::cout << info.str() << std::endl;
+
+ if (printMetadata_) {
+ const ControlList &requestMetadata = request->metadata();
+ for (const auto &[key, value] : requestMetadata) {
+ const ControlId *id = controls::controls.at(key);
+ std::cout << "\t" << id->name() << " = "
+ << value.toString() << std::endl;
+ }
+ }
+
+ /*
+ * Notify the user that capture is complete if the limit has just been
+ * reached.
+ */
+ captureCount_++;
+ if (captureLimit_ && captureCount_ >= captureLimit_) {
+ captureDone.emit();
+ return;
+ }
+
+ /*
+ * If the frame sink holds on the request, we'll requeue it later in the
+ * complete handler.
+ */
+ if (!requeue)
+ return;
+
+ request->reuse(Request::ReuseBuffers);
+ queueRequest(request);
+}
+
+void CameraSession::sinkRelease(Request *request)
+{
+ request->reuse(Request::ReuseBuffers);
+ queueRequest(request);
+}
diff --git a/src/apps/cam/camera_session.h b/src/apps/cam/camera_session.h
new file mode 100644
index 00000000..d562caae
--- /dev/null
+++ b/src/apps/cam/camera_session.h
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * camera_session.h - Camera capture session
+ */
+
+#pragma once
+
+#include <memory>
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+#include <libcamera/base/signal.h>
+
+#include <libcamera/camera.h>
+#include <libcamera/camera_manager.h>
+#include <libcamera/framebuffer.h>
+#include <libcamera/framebuffer_allocator.h>
+#include <libcamera/request.h>
+#include <libcamera/stream.h>
+
+#include "options.h"
+
+class CaptureScript;
+class FrameSink;
+
+class CameraSession
+{
+public:
+ CameraSession(libcamera::CameraManager *cm,
+ const std::string &cameraId, unsigned int cameraIndex,
+ const OptionsParser::Options &options);
+ ~CameraSession();
+
+ bool isValid() const { return config_ != nullptr; }
+ const OptionsParser::Options &options() { return options_; }
+
+ libcamera::Camera *camera() { return camera_.get(); }
+ libcamera::CameraConfiguration *config() { return config_.get(); }
+
+ void listControls() const;
+ void listProperties() const;
+ void infoConfiguration() const;
+
+ int start();
+ void stop();
+
+ libcamera::Signal<> captureDone;
+
+private:
+ int startCapture();
+
+ int queueRequest(libcamera::Request *request);
+ void requestComplete(libcamera::Request *request);
+ void processRequest(libcamera::Request *request);
+ void sinkRelease(libcamera::Request *request);
+
+ const OptionsParser::Options &options_;
+ std::shared_ptr<libcamera::Camera> camera_;
+ std::unique_ptr<libcamera::CameraConfiguration> config_;
+
+ std::unique_ptr<CaptureScript> script_;
+
+ std::map<const libcamera::Stream *, std::string> streamNames_;
+ std::unique_ptr<FrameSink> sink_;
+ unsigned int cameraIndex_;
+
+ uint64_t last_;
+
+ unsigned int queueCount_;
+ unsigned int captureCount_;
+ unsigned int captureLimit_;
+ bool printMetadata_;
+
+ std::unique_ptr<libcamera::FrameBufferAllocator> allocator_;
+ std::vector<std::unique_ptr<libcamera::Request>> requests_;
+};
diff --git a/src/apps/cam/capture-script.yaml b/src/apps/cam/capture-script.yaml
new file mode 100644
index 00000000..7118865e
--- /dev/null
+++ b/src/apps/cam/capture-script.yaml
@@ -0,0 +1,71 @@
+# SPDX-License-Identifier: CC0-1.0
+
+# Capture script example
+#
+# A capture script allows to associate a list of controls and their values
+# to frame numbers.
+#
+# The script allows defining a list of frames associated with controls
+# and an optional list of properties that can control the script behaviour.
+
+# properties:
+# # Repeat the controls every 'idx' frames.
+# - loop: idx
+#
+# # List of frame number with associated a list of controls to be applied
+# frames:
+# - frame-number:
+# Control1: value1
+# Control2: value2
+
+# \todo Formally define the capture script structure with a schema
+
+# Notes:
+# - Controls have to be specified by name, as defined in the
+# libcamera::controls:: enumeration
+# - Controls not supported by the camera currently operated are ignored
+# - Frame numbers shall be monotonically incrementing, gaps are allowed
+# - If a loop limit is specified, frame numbers in the 'frames' list shall be
+# less than the loop control
+
+# Example: Turn brightness up and down every 460 frames
+
+properties:
+ - loop: 460
+
+frames:
+ - 0:
+ Brightness: 0.0
+
+ - 40:
+ Brightness: 0.2
+
+ - 80:
+ Brightness: 0.4
+
+ - 120:
+ Brightness: 0.8
+
+ - 160:
+ Brightness: 0.4
+
+ - 200:
+ Brightness: 0.2
+
+ - 240:
+ Brightness: 0.0
+
+ - 280:
+ Brightness: -0.2
+
+ - 300:
+ Brightness: -0.4
+
+ - 340:
+ Brightness: -0.8
+
+ - 380:
+ Brightness: -0.4
+
+ - 420:
+ Brightness: -0.2
diff --git a/src/apps/cam/capture_script.cpp b/src/apps/cam/capture_script.cpp
new file mode 100644
index 00000000..5a27361c
--- /dev/null
+++ b/src/apps/cam/capture_script.cpp
@@ -0,0 +1,535 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Ideas on Board Oy
+ *
+ * capture_script.cpp - Capture session configuration script
+ */
+
+#include "capture_script.h"
+
+#include <iostream>
+#include <stdio.h>
+#include <stdlib.h>
+
+using namespace libcamera;
+
+CaptureScript::CaptureScript(std::shared_ptr<Camera> camera,
+ const std::string &fileName)
+ : camera_(camera), loop_(0), valid_(false)
+{
+ FILE *fh = fopen(fileName.c_str(), "r");
+ if (!fh) {
+ int ret = -errno;
+ std::cerr << "Failed to open capture script " << fileName
+ << ": " << strerror(-ret) << std::endl;
+ return;
+ }
+
+ /*
+ * Map the camera's controls to their name so that they can be
+ * easily identified when parsing the script file.
+ */
+ for (const auto &[control, info] : camera_->controls())
+ controls_[control->name()] = control;
+
+ int ret = parseScript(fh);
+ fclose(fh);
+ if (ret)
+ return;
+
+ valid_ = true;
+}
+
+/* Retrieve the control list associated with a frame number. */
+const ControlList &CaptureScript::frameControls(unsigned int frame)
+{
+ static ControlList controls{};
+ unsigned int idx = frame;
+
+ /* If we loop, repeat the controls every 'loop_' frames. */
+ if (loop_)
+ idx = frame % loop_;
+
+ auto it = frameControls_.find(idx);
+ if (it == frameControls_.end())
+ return controls;
+
+ return it->second;
+}
+
+CaptureScript::EventPtr CaptureScript::nextEvent(yaml_event_type_t expectedType)
+{
+ EventPtr event(new yaml_event_t);
+
+ if (!yaml_parser_parse(&parser_, event.get()))
+ return nullptr;
+
+ if (expectedType != YAML_NO_EVENT && !checkEvent(event, expectedType))
+ return nullptr;
+
+ return event;
+}
+
+bool CaptureScript::checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const
+{
+ if (event->type != expectedType) {
+ std::cerr << "Capture script error on line " << event->start_mark.line
+ << " column " << event->start_mark.column << ": "
+ << "Expected " << eventTypeName(expectedType)
+ << " event, got " << eventTypeName(event->type)
+ << std::endl;
+ return false;
+ }
+
+ return true;
+}
+
+std::string CaptureScript::eventScalarValue(const EventPtr &event)
+{
+ return std::string(reinterpret_cast<char *>(event->data.scalar.value),
+ event->data.scalar.length);
+}
+
+std::string CaptureScript::eventTypeName(yaml_event_type_t type)
+{
+ static const std::map<yaml_event_type_t, std::string> typeNames = {
+ { YAML_STREAM_START_EVENT, "stream-start" },
+ { YAML_STREAM_END_EVENT, "stream-end" },
+ { YAML_DOCUMENT_START_EVENT, "document-start" },
+ { YAML_DOCUMENT_END_EVENT, "document-end" },
+ { YAML_ALIAS_EVENT, "alias" },
+ { YAML_SCALAR_EVENT, "scalar" },
+ { YAML_SEQUENCE_START_EVENT, "sequence-start" },
+ { YAML_SEQUENCE_END_EVENT, "sequence-end" },
+ { YAML_MAPPING_START_EVENT, "mapping-start" },
+ { YAML_MAPPING_END_EVENT, "mapping-end" },
+ };
+
+ auto it = typeNames.find(type);
+ if (it == typeNames.end())
+ return "[type " + std::to_string(type) + "]";
+
+ return it->second;
+}
+
+int CaptureScript::parseScript(FILE *script)
+{
+ int ret = yaml_parser_initialize(&parser_);
+ if (!ret) {
+ std::cerr << "Failed to initialize yaml parser" << std::endl;
+ return ret;
+ }
+
+ /* Delete the parser upon function exit. */
+ struct ParserDeleter {
+ ParserDeleter(yaml_parser_t *parser) : parser_(parser) { }
+ ~ParserDeleter() { yaml_parser_delete(parser_); }
+ yaml_parser_t *parser_;
+ } deleter(&parser_);
+
+ yaml_parser_set_input_file(&parser_, script);
+
+ EventPtr event = nextEvent(YAML_STREAM_START_EVENT);
+ if (!event)
+ return -EINVAL;
+
+ event = nextEvent(YAML_DOCUMENT_START_EVENT);
+ if (!event)
+ return -EINVAL;
+
+ event = nextEvent(YAML_MAPPING_START_EVENT);
+ if (!event)
+ return -EINVAL;
+
+ while (1) {
+ event = nextEvent();
+ if (!event)
+ return -EINVAL;
+
+ if (event->type == YAML_MAPPING_END_EVENT)
+ return 0;
+
+ if (!checkEvent(event, YAML_SCALAR_EVENT))
+ return -EINVAL;
+
+ std::string section = eventScalarValue(event);
+
+ if (section == "properties") {
+ ret = parseProperties();
+ if (ret)
+ return ret;
+ } else if (section == "frames") {
+ ret = parseFrames();
+ if (ret)
+ return ret;
+ } else {
+ std::cerr << "Unsupported section '" << section << "'"
+ << std::endl;
+ return -EINVAL;
+ }
+ }
+}
+
+int CaptureScript::parseProperty()
+{
+ EventPtr event = nextEvent(YAML_MAPPING_START_EVENT);
+ if (!event)
+ return -EINVAL;
+
+ std::string prop = parseScalar();
+ if (prop.empty())
+ return -EINVAL;
+
+ if (prop == "loop") {
+ event = nextEvent();
+ if (!event)
+ return -EINVAL;
+
+ std::string value = eventScalarValue(event);
+ if (value.empty())
+ return -EINVAL;
+
+ loop_ = atoi(value.c_str());
+ if (!loop_) {
+ std::cerr << "Invalid loop limit '" << loop_ << "'"
+ << std::endl;
+ return -EINVAL;
+ }
+ } else {
+ std::cerr << "Unsupported property '" << prop << "'" << std::endl;
+ return -EINVAL;
+ }
+
+ event = nextEvent(YAML_MAPPING_END_EVENT);
+ if (!event)
+ return -EINVAL;
+
+ return 0;
+}
+
+int CaptureScript::parseProperties()
+{
+ EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT);
+ if (!event)
+ return -EINVAL;
+
+ while (1) {
+ if (event->type == YAML_SEQUENCE_END_EVENT)
+ return 0;
+
+ int ret = parseProperty();
+ if (ret)
+ return ret;
+
+ event = nextEvent();
+ if (!event)
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+int CaptureScript::parseFrames()
+{
+ EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT);
+ if (!event)
+ return -EINVAL;
+
+ while (1) {
+ event = nextEvent();
+ if (!event)
+ return -EINVAL;
+
+ if (event->type == YAML_SEQUENCE_END_EVENT)
+ return 0;
+
+ int ret = parseFrame(std::move(event));
+ if (ret)
+ return ret;
+ }
+}
+
+int CaptureScript::parseFrame(EventPtr event)
+{
+ if (!checkEvent(event, YAML_MAPPING_START_EVENT))
+ return -EINVAL;
+
+ std::string key = parseScalar();
+ if (key.empty())
+ return -EINVAL;
+
+ unsigned int frameId = atoi(key.c_str());
+ if (loop_ && frameId >= loop_) {
+ std::cerr
+ << "Frame id (" << frameId << ") shall be smaller than"
+ << "loop limit (" << loop_ << ")" << std::endl;
+ return -EINVAL;
+ }
+
+ event = nextEvent(YAML_MAPPING_START_EVENT);
+ if (!event)
+ return -EINVAL;
+
+ ControlList controls{};
+
+ while (1) {
+ event = nextEvent();
+ if (!event)
+ return -EINVAL;
+
+ if (event->type == YAML_MAPPING_END_EVENT)
+ break;
+
+ int ret = parseControl(std::move(event), controls);
+ if (ret)
+ return ret;
+ }
+
+ frameControls_[frameId] = std::move(controls);
+
+ event = nextEvent(YAML_MAPPING_END_EVENT);
+ if (!event)
+ return -EINVAL;
+
+ return 0;
+}
+
+int CaptureScript::parseControl(EventPtr event, ControlList &controls)
+{
+ /* We expect a value after a key. */
+ std::string name = eventScalarValue(event);
+ if (name.empty())
+ return -EINVAL;
+
+ /* If the camera does not support the control just ignore it. */
+ auto it = controls_.find(name);
+ if (it == controls_.end()) {
+ std::cerr << "Unsupported control '" << name << "'" << std::endl;
+ return -EINVAL;
+ }
+
+ const ControlId *controlId = it->second;
+
+ ControlValue val = unpackControl(controlId);
+ if (val.isNone()) {
+ std::cerr << "Error unpacking control '" << name << "'"
+ << std::endl;
+ return -EINVAL;
+ }
+
+ controls.set(controlId->id(), val);
+
+ return 0;
+}
+
+std::string CaptureScript::parseScalar()
+{
+ EventPtr event = nextEvent(YAML_SCALAR_EVENT);
+ if (!event)
+ return "";
+
+ return eventScalarValue(event);
+}
+
+ControlValue CaptureScript::parseRectangles()
+{
+ std::vector<libcamera::Rectangle> rectangles;
+
+ std::vector<std::vector<std::string>> arrays = parseArrays();
+ if (arrays.empty())
+ return {};
+
+ for (const std::vector<std::string> &values : arrays) {
+ if (values.size() != 4) {
+ std::cerr << "Error parsing Rectangle: expected "
+ << "array with 4 parameters" << std::endl;
+ return {};
+ }
+
+ Rectangle rect = unpackRectangle(values);
+ rectangles.push_back(rect);
+ }
+
+ ControlValue controlValue;
+ controlValue.set(Span<const Rectangle>(rectangles));
+
+ return controlValue;
+}
+
+std::vector<std::vector<std::string>> CaptureScript::parseArrays()
+{
+ EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT);
+ if (!event)
+ return {};
+
+ event = nextEvent();
+ if (!event)
+ return {};
+
+ std::vector<std::vector<std::string>> valueArrays;
+
+ /* Parse single array. */
+ if (event->type == YAML_SCALAR_EVENT) {
+ std::string firstValue = eventScalarValue(event);
+ if (firstValue.empty())
+ return {};
+
+ std::vector<std::string> remaining = parseSingleArray();
+
+ std::vector<std::string> values = { firstValue };
+ values.insert(std::end(values),
+ std::begin(remaining), std::end(remaining));
+ valueArrays.push_back(values);
+
+ return valueArrays;
+ }
+
+ /* Parse array of arrays. */
+ while (1) {
+ switch (event->type) {
+ case YAML_SEQUENCE_START_EVENT: {
+ std::vector<std::string> values = parseSingleArray();
+ valueArrays.push_back(values);
+ break;
+ }
+ case YAML_SEQUENCE_END_EVENT:
+ return valueArrays;
+ default:
+ return {};
+ }
+
+ event = nextEvent();
+ if (!event)
+ return {};
+ }
+}
+
+std::vector<std::string> CaptureScript::parseSingleArray()
+{
+ std::vector<std::string> values;
+
+ while (1) {
+ EventPtr event = nextEvent();
+ if (!event)
+ return {};
+
+ switch (event->type) {
+ case YAML_SCALAR_EVENT: {
+ std::string value = eventScalarValue(event);
+ if (value.empty())
+ return {};
+ values.push_back(value);
+ break;
+ }
+ case YAML_SEQUENCE_END_EVENT:
+ return values;
+ default:
+ return {};
+ }
+ }
+}
+
+void CaptureScript::unpackFailure(const ControlId *id, const std::string &repr)
+{
+ static const std::map<unsigned int, const char *> typeNames = {
+ { ControlTypeNone, "none" },
+ { ControlTypeBool, "bool" },
+ { ControlTypeByte, "byte" },
+ { ControlTypeInteger32, "int32" },
+ { ControlTypeInteger64, "int64" },
+ { ControlTypeFloat, "float" },
+ { ControlTypeString, "string" },
+ { ControlTypeRectangle, "Rectangle" },
+ { ControlTypeSize, "Size" },
+ };
+
+ const char *typeName;
+ auto it = typeNames.find(id->type());
+ if (it != typeNames.end())
+ typeName = it->second;
+ else
+ typeName = "unknown";
+
+ std::cerr << "Unsupported control '" << repr << "' for "
+ << typeName << " control " << id->name() << std::endl;
+}
+
+ControlValue CaptureScript::unpackControl(const ControlId *id)
+{
+ /* Parse complex types. */
+ switch (id->type()) {
+ case ControlTypeRectangle:
+ return parseRectangles();
+ case ControlTypeSize:
+ /* \todo Parse Sizes. */
+ return {};
+ default:
+ break;
+ }
+
+ /* Parse basic types represented by a single scalar. */
+ const std::string repr = parseScalar();
+ if (repr.empty())
+ return {};
+
+ ControlValue value{};
+
+ switch (id->type()) {
+ case ControlTypeNone:
+ break;
+ case ControlTypeBool: {
+ bool val;
+
+ if (repr == "true") {
+ val = true;
+ } else if (repr == "false") {
+ val = false;
+ } else {
+ unpackFailure(id, repr);
+ return value;
+ }
+
+ value.set<bool>(val);
+ break;
+ }
+ case ControlTypeByte: {
+ uint8_t val = strtol(repr.c_str(), NULL, 10);
+ value.set<uint8_t>(val);
+ break;
+ }
+ case ControlTypeInteger32: {
+ int32_t val = strtol(repr.c_str(), NULL, 10);
+ value.set<int32_t>(val);
+ break;
+ }
+ case ControlTypeInteger64: {
+ int64_t val = strtoll(repr.c_str(), NULL, 10);
+ value.set<int64_t>(val);
+ break;
+ }
+ case ControlTypeFloat: {
+ float val = strtof(repr.c_str(), NULL);
+ value.set<float>(val);
+ break;
+ }
+ case ControlTypeString: {
+ value.set<std::string>(repr);
+ break;
+ }
+ default:
+ std::cerr << "Unsupported control type" << std::endl;
+ break;
+ }
+
+ return value;
+}
+
+libcamera::Rectangle CaptureScript::unpackRectangle(const std::vector<std::string> &strVec)
+{
+ int x = strtol(strVec[0].c_str(), NULL, 10);
+ int y = strtol(strVec[1].c_str(), NULL, 10);
+ unsigned int width = strtoul(strVec[2].c_str(), NULL, 10);
+ unsigned int height = strtoul(strVec[3].c_str(), NULL, 10);
+
+ return Rectangle(x, y, width, height);
+}
diff --git a/src/apps/cam/capture_script.h b/src/apps/cam/capture_script.h
new file mode 100644
index 00000000..7a0ddebb
--- /dev/null
+++ b/src/apps/cam/capture_script.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Ideas on Board Oy
+ *
+ * capture_script.h - Capture session configuration script
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include <libcamera/camera.h>
+#include <libcamera/controls.h>
+
+#include <yaml.h>
+
+class CaptureScript
+{
+public:
+ CaptureScript(std::shared_ptr<libcamera::Camera> camera,
+ const std::string &fileName);
+
+ bool valid() const { return valid_; }
+
+ const libcamera::ControlList &frameControls(unsigned int frame);
+
+private:
+ struct EventDeleter {
+ void operator()(yaml_event_t *event) const
+ {
+ yaml_event_delete(event);
+ delete event;
+ }
+ };
+ using EventPtr = std::unique_ptr<yaml_event_t, EventDeleter>;
+
+ std::map<std::string, const libcamera::ControlId *> controls_;
+ std::map<unsigned int, libcamera::ControlList> frameControls_;
+ std::shared_ptr<libcamera::Camera> camera_;
+ yaml_parser_t parser_;
+ unsigned int loop_;
+ bool valid_;
+
+ EventPtr nextEvent(yaml_event_type_t expectedType = YAML_NO_EVENT);
+ bool checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const;
+ static std::string eventScalarValue(const EventPtr &event);
+ static std::string eventTypeName(yaml_event_type_t type);
+
+ int parseScript(FILE *script);
+
+ int parseProperties();
+ int parseProperty();
+ int parseFrames();
+ int parseFrame(EventPtr event);
+ int parseControl(EventPtr event, libcamera::ControlList &controls);
+
+ std::string parseScalar();
+ libcamera::ControlValue parseRectangles();
+ std::vector<std::vector<std::string>> parseArrays();
+ std::vector<std::string> parseSingleArray();
+
+ void unpackFailure(const libcamera::ControlId *id,
+ const std::string &repr);
+ libcamera::ControlValue unpackControl(const libcamera::ControlId *id);
+ libcamera::Rectangle unpackRectangle(const std::vector<std::string> &strVec);
+};
diff --git a/src/apps/cam/dng_writer.cpp b/src/apps/cam/dng_writer.cpp
new file mode 100644
index 00000000..c945edce
--- /dev/null
+++ b/src/apps/cam/dng_writer.cpp
@@ -0,0 +1,653 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * dng_writer.cpp - DNG writer
+ */
+
+#include "dng_writer.h"
+
+#include <algorithm>
+#include <iostream>
+#include <map>
+
+#include <tiffio.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/formats.h>
+#include <libcamera/property_ids.h>
+
+using namespace libcamera;
+
+enum CFAPatternColour : uint8_t {
+ CFAPatternRed = 0,
+ CFAPatternGreen = 1,
+ CFAPatternBlue = 2,
+};
+
+struct FormatInfo {
+ uint8_t bitsPerSample;
+ CFAPatternColour pattern[4];
+ void (*packScanline)(void *output, const void *input,
+ unsigned int width);
+ void (*thumbScanline)(const FormatInfo &info, void *output,
+ const void *input, unsigned int width,
+ unsigned int stride);
+};
+
+struct Matrix3d {
+ Matrix3d()
+ {
+ }
+
+ Matrix3d(float m0, float m1, float m2,
+ float m3, float m4, float m5,
+ float m6, float m7, float m8)
+ {
+ m[0] = m0, m[1] = m1, m[2] = m2;
+ m[3] = m3, m[4] = m4, m[5] = m5;
+ m[6] = m6, m[7] = m7, m[8] = m8;
+ }
+
+ Matrix3d(const Span<const float> &span)
+ : Matrix3d(span[0], span[1], span[2],
+ span[3], span[4], span[5],
+ span[6], span[7], span[8])
+ {
+ }
+
+ static Matrix3d diag(float diag0, float diag1, float diag2)
+ {
+ return Matrix3d(diag0, 0, 0, 0, diag1, 0, 0, 0, diag2);
+ }
+
+ static Matrix3d identity()
+ {
+ return Matrix3d(1, 0, 0, 0, 1, 0, 0, 0, 1);
+ }
+
+ Matrix3d transpose() const
+ {
+ return { m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8] };
+ }
+
+ Matrix3d cofactors() const
+ {
+ return { m[4] * m[8] - m[5] * m[7],
+ -(m[3] * m[8] - m[5] * m[6]),
+ m[3] * m[7] - m[4] * m[6],
+ -(m[1] * m[8] - m[2] * m[7]),
+ m[0] * m[8] - m[2] * m[6],
+ -(m[0] * m[7] - m[1] * m[6]),
+ m[1] * m[5] - m[2] * m[4],
+ -(m[0] * m[5] - m[2] * m[3]),
+ m[0] * m[4] - m[1] * m[3] };
+ }
+
+ Matrix3d adjugate() const
+ {
+ return cofactors().transpose();
+ }
+
+ float determinant() const
+ {
+ return m[0] * (m[4] * m[8] - m[5] * m[7]) -
+ m[1] * (m[3] * m[8] - m[5] * m[6]) +
+ m[2] * (m[3] * m[7] - m[4] * m[6]);
+ }
+
+ Matrix3d inverse() const
+ {
+ return adjugate() * (1.0 / determinant());
+ }
+
+ Matrix3d operator*(const Matrix3d &other) const
+ {
+ Matrix3d result;
+ for (unsigned int i = 0; i < 3; i++) {
+ for (unsigned int j = 0; j < 3; j++) {
+ result.m[i * 3 + j] =
+ m[i * 3 + 0] * other.m[0 + j] +
+ m[i * 3 + 1] * other.m[3 + j] +
+ m[i * 3 + 2] * other.m[6 + j];
+ }
+ }
+ return result;
+ }
+
+ Matrix3d operator*(float f) const
+ {
+ Matrix3d result;
+ for (unsigned int i = 0; i < 9; i++)
+ result.m[i] = m[i] * f;
+ return result;
+ }
+
+ float m[9];
+};
+
+void packScanlineSBGGR8(void *output, const void *input, unsigned int width)
+{
+ const uint8_t *in = static_cast<const uint8_t *>(input);
+ uint8_t *out = static_cast<uint8_t *>(output);
+
+ std::copy(in, in + width, out);
+}
+
+void packScanlineSBGGR10P(void *output, const void *input, unsigned int width)
+{
+ const uint8_t *in = static_cast<const uint8_t *>(input);
+ uint8_t *out = static_cast<uint8_t *>(output);
+
+ /* \todo Can this be made more efficient? */
+ for (unsigned int x = 0; x < width; x += 4) {
+ *out++ = in[0];
+ *out++ = (in[4] & 0x03) << 6 | in[1] >> 2;
+ *out++ = (in[1] & 0x03) << 6 | (in[4] & 0x0c) << 2 | in[2] >> 4;
+ *out++ = (in[2] & 0x0f) << 4 | (in[4] & 0x30) >> 2 | in[3] >> 6;
+ *out++ = (in[3] & 0x3f) << 2 | (in[4] & 0xc0) >> 6;
+ in += 5;
+ }
+}
+
+void packScanlineSBGGR12P(void *output, const void *input, unsigned int width)
+{
+ const uint8_t *in = static_cast<const uint8_t *>(input);
+ uint8_t *out = static_cast<uint8_t *>(output);
+
+ /* \todo Can this be made more efficient? */
+ for (unsigned int i = 0; i < width; i += 2) {
+ *out++ = in[0];
+ *out++ = (in[2] & 0x0f) << 4 | in[1] >> 4;
+ *out++ = (in[1] & 0x0f) << 4 | in[2] >> 4;
+ in += 3;
+ }
+}
+
+void thumbScanlineSBGGRxxP(const FormatInfo &info, void *output,
+ const void *input, unsigned int width,
+ unsigned int stride)
+{
+ const uint8_t *in = static_cast<const uint8_t *>(input);
+ uint8_t *out = static_cast<uint8_t *>(output);
+
+ /* Number of bytes corresponding to 16 pixels. */
+ unsigned int skip = info.bitsPerSample * 16 / 8;
+
+ for (unsigned int x = 0; x < width; x++) {
+ uint8_t value = (in[0] + in[1] + in[stride] + in[stride + 1]) >> 2;
+ *out++ = value;
+ *out++ = value;
+ *out++ = value;
+ in += skip;
+ }
+}
+
+void packScanlineIPU3(void *output, const void *input, unsigned int width)
+{
+ const uint8_t *in = static_cast<const uint8_t *>(input);
+ uint16_t *out = static_cast<uint16_t *>(output);
+
+ /*
+ * Upscale the 10-bit format to 16-bit as it's not trivial to pack it
+ * as 10-bit without gaps.
+ *
+ * \todo Improve packing to keep the 10-bit sample size.
+ */
+ unsigned int x = 0;
+ while (true) {
+ for (unsigned int i = 0; i < 6; i++) {
+ *out++ = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
+ if (++x >= width)
+ return;
+
+ *out++ = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
+ if (++x >= width)
+ return;
+
+ *out++ = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
+ if (++x >= width)
+ return;
+
+ *out++ = (in[4] & 0xff) << 8 | (in[3] & 0xc0) << 0;
+ if (++x >= width)
+ return;
+
+ in += 5;
+ }
+
+ *out++ = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
+ if (++x >= width)
+ return;
+
+ in += 2;
+ }
+}
+
+void thumbScanlineIPU3([[maybe_unused]] const FormatInfo &info, void *output,
+ const void *input, unsigned int width,
+ unsigned int stride)
+{
+ uint8_t *out = static_cast<uint8_t *>(output);
+
+ for (unsigned int x = 0; x < width; x++) {
+ unsigned int pixel = x * 16;
+ unsigned int block = pixel / 25;
+ unsigned int pixelInBlock = pixel - block * 25;
+
+ /*
+ * If the pixel is the last in the block cheat a little and
+ * move one pixel backward to avoid reading between two blocks
+ * and having to deal with the padding bits.
+ */
+ if (pixelInBlock == 24)
+ pixelInBlock--;
+
+ const uint8_t *in = static_cast<const uint8_t *>(input)
+ + block * 32 + (pixelInBlock / 4) * 5;
+
+ uint16_t val1, val2, val3, val4;
+ switch (pixelInBlock % 4) {
+ case 0:
+ val1 = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
+ val2 = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
+ val3 = (in[stride + 1] & 0x03) << 14 | (in[stride + 0] & 0xff) << 6;
+ val4 = (in[stride + 2] & 0x0f) << 12 | (in[stride + 1] & 0xfc) << 4;
+ break;
+ case 1:
+ val1 = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
+ val2 = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
+ val3 = (in[stride + 2] & 0x0f) << 12 | (in[stride + 1] & 0xfc) << 4;
+ val4 = (in[stride + 3] & 0x3f) << 10 | (in[stride + 2] & 0xf0) << 2;
+ break;
+ case 2:
+ val1 = (in[3] & 0x3f) << 10 | (in[2] & 0xf0) << 2;
+ val2 = (in[4] & 0xff) << 8 | (in[3] & 0xc0) << 0;
+ val3 = (in[stride + 3] & 0x3f) << 10 | (in[stride + 2] & 0xf0) << 2;
+ val4 = (in[stride + 4] & 0xff) << 8 | (in[stride + 3] & 0xc0) << 0;
+ break;
+ case 3:
+ val1 = (in[4] & 0xff) << 8 | (in[3] & 0xc0) << 0;
+ val2 = (in[6] & 0x03) << 14 | (in[5] & 0xff) << 6;
+ val3 = (in[stride + 4] & 0xff) << 8 | (in[stride + 3] & 0xc0) << 0;
+ val4 = (in[stride + 6] & 0x03) << 14 | (in[stride + 5] & 0xff) << 6;
+ break;
+ }
+
+ uint8_t value = (val1 + val2 + val3 + val4) >> 10;
+ *out++ = value;
+ *out++ = value;
+ *out++ = value;
+ }
+}
+
+static const std::map<PixelFormat, FormatInfo> formatInfo = {
+ { formats::SBGGR8, {
+ .bitsPerSample = 8,
+ .pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+ .packScanline = packScanlineSBGGR8,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGBRG8, {
+ .bitsPerSample = 8,
+ .pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR8,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGRBG8, {
+ .bitsPerSample = 8,
+ .pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR8,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SRGGB8, {
+ .bitsPerSample = 8,
+ .pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+ .packScanline = packScanlineSBGGR8,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SBGGR10_CSI2P, {
+ .bitsPerSample = 10,
+ .pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+ .packScanline = packScanlineSBGGR10P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGBRG10_CSI2P, {
+ .bitsPerSample = 10,
+ .pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR10P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGRBG10_CSI2P, {
+ .bitsPerSample = 10,
+ .pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR10P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SRGGB10_CSI2P, {
+ .bitsPerSample = 10,
+ .pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+ .packScanline = packScanlineSBGGR10P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SBGGR12_CSI2P, {
+ .bitsPerSample = 12,
+ .pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+ .packScanline = packScanlineSBGGR12P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGBRG12_CSI2P, {
+ .bitsPerSample = 12,
+ .pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR12P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SGRBG12_CSI2P, {
+ .bitsPerSample = 12,
+ .pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+ .packScanline = packScanlineSBGGR12P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SRGGB12_CSI2P, {
+ .bitsPerSample = 12,
+ .pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+ .packScanline = packScanlineSBGGR12P,
+ .thumbScanline = thumbScanlineSBGGRxxP,
+ } },
+ { formats::SBGGR10_IPU3, {
+ .bitsPerSample = 16,
+ .pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+ .packScanline = packScanlineIPU3,
+ .thumbScanline = thumbScanlineIPU3,
+ } },
+ { formats::SGBRG10_IPU3, {
+ .bitsPerSample = 16,
+ .pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+ .packScanline = packScanlineIPU3,
+ .thumbScanline = thumbScanlineIPU3,
+ } },
+ { formats::SGRBG10_IPU3, {
+ .bitsPerSample = 16,
+ .pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+ .packScanline = packScanlineIPU3,
+ .thumbScanline = thumbScanlineIPU3,
+ } },
+ { formats::SRGGB10_IPU3, {
+ .bitsPerSample = 16,
+ .pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+ .packScanline = packScanlineIPU3,
+ .thumbScanline = thumbScanlineIPU3,
+ } },
+};
+
+int DNGWriter::write(const char *filename, const Camera *camera,
+ const StreamConfiguration &config,
+ const ControlList &metadata,
+ [[maybe_unused]] const FrameBuffer *buffer,
+ const void *data)
+{
+ const ControlList &cameraProperties = camera->properties();
+
+ const auto it = formatInfo.find(config.pixelFormat);
+ if (it == formatInfo.cend()) {
+ std::cerr << "Unsupported pixel format" << std::endl;
+ return -EINVAL;
+ }
+ const FormatInfo *info = &it->second;
+
+ TIFF *tif = TIFFOpen(filename, "w");
+ if (!tif) {
+ std::cerr << "Failed to open tiff file" << std::endl;
+ return -EINVAL;
+ }
+
+ /*
+ * Scanline buffer, has to be large enough to store both a RAW scanline
+ * or a thumbnail scanline. The latter will always be much smaller than
+ * the former as we downscale by 16 in both directions.
+ */
+ uint8_t scanline[(config.size.width * info->bitsPerSample + 7) / 8];
+
+ toff_t rawIFDOffset = 0;
+ toff_t exifIFDOffset = 0;
+
+ /*
+ * Start with a thumbnail in IFD 0 for compatibility with TIFF baseline
+ * readers, as required by the TIFF/EP specification. Tags that apply to
+ * the whole file are stored here.
+ */
+ const uint8_t version[] = { 1, 2, 0, 0 };
+
+ TIFFSetField(tif, TIFFTAG_DNGVERSION, version);
+ TIFFSetField(tif, TIFFTAG_DNGBACKWARDVERSION, version);
+ TIFFSetField(tif, TIFFTAG_FILLORDER, FILLORDER_MSB2LSB);
+ TIFFSetField(tif, TIFFTAG_MAKE, "libcamera");
+
+ const auto &model = cameraProperties.get(properties::Model);
+ if (model) {
+ TIFFSetField(tif, TIFFTAG_MODEL, model->c_str());
+ /* \todo set TIFFTAG_UNIQUECAMERAMODEL. */
+ }
+
+ TIFFSetField(tif, TIFFTAG_SOFTWARE, "qcam");
+ TIFFSetField(tif, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT);
+
+ /*
+ * Thumbnail-specific tags. The thumbnail is stored as an RGB image
+ * with 1/16 of the raw image resolution. Greyscale would save space,
+ * but doesn't seem well supported by RawTherapee.
+ */
+ TIFFSetField(tif, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE);
+ TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, config.size.width / 16);
+ TIFFSetField(tif, TIFFTAG_IMAGELENGTH, config.size.height / 16);
+ TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8);
+ TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
+ TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
+ TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3);
+ TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
+ TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
+
+ /*
+ * Fill in some reasonable colour information in the DNG. We supply
+ * the "neutral" colour values which determine the white balance, and the
+ * "ColorMatrix1" which converts XYZ to (un-white-balanced) camera RGB.
+ * Note that this is not a "proper" colour calibration for the DNG,
+ * nonetheless, many tools should be able to render the colours better.
+ */
+ float neutral[3] = { 1, 1, 1 };
+ Matrix3d wbGain = Matrix3d::identity();
+ /* From http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html */
+ const Matrix3d rgb2xyz(0.4124564, 0.3575761, 0.1804375,
+ 0.2126729, 0.7151522, 0.0721750,
+ 0.0193339, 0.1191920, 0.9503041);
+ Matrix3d ccm = Matrix3d::identity();
+ /*
+ * Pick a reasonable number eps to protect against singularities. It
+ * should be comfortably larger than the point at which we run into
+ * numerical trouble, yet smaller than any plausible gain that we might
+ * apply to a colour, either explicitly or as part of the colour matrix.
+ */
+ const double eps = 1e-2;
+
+ const auto &colourGains = metadata.get(controls::ColourGains);
+ if (colourGains) {
+ if ((*colourGains)[0] > eps && (*colourGains)[1] > eps) {
+ wbGain = Matrix3d::diag((*colourGains)[0], 1, (*colourGains)[1]);
+ neutral[0] = 1.0 / (*colourGains)[0]; /* red */
+ neutral[2] = 1.0 / (*colourGains)[1]; /* blue */
+ }
+ }
+
+ const auto &ccmControl = metadata.get(controls::ColourCorrectionMatrix);
+ if (ccmControl) {
+ Matrix3d ccmSupplied(*ccmControl);
+ if (ccmSupplied.determinant() > eps)
+ ccm = ccmSupplied;
+ }
+
+ /*
+ * rgb2xyz is known to be invertible, and we've ensured above that both
+ * the ccm and wbGain matrices are non-singular, so the product of all
+ * three is guaranteed to be invertible too.
+ */
+ Matrix3d colorMatrix1 = (rgb2xyz * ccm * wbGain).inverse();
+
+ TIFFSetField(tif, TIFFTAG_COLORMATRIX1, 9, colorMatrix1.m);
+ TIFFSetField(tif, TIFFTAG_ASSHOTNEUTRAL, 3, neutral);
+
+ /*
+ * Reserve space for the SubIFD and ExifIFD tags, pointing to the IFD
+ * for the raw image and EXIF data respectively. The real offsets will
+ * be set later.
+ */
+ TIFFSetField(tif, TIFFTAG_SUBIFD, 1, &rawIFDOffset);
+ TIFFSetField(tif, TIFFTAG_EXIFIFD, exifIFDOffset);
+
+ /* Write the thumbnail. */
+ const uint8_t *row = static_cast<const uint8_t *>(data);
+ for (unsigned int y = 0; y < config.size.height / 16; y++) {
+ info->thumbScanline(*info, &scanline, row,
+ config.size.width / 16, config.stride);
+
+ if (TIFFWriteScanline(tif, &scanline, y, 0) != 1) {
+ std::cerr << "Failed to write thumbnail scanline"
+ << std::endl;
+ TIFFClose(tif);
+ return -EINVAL;
+ }
+
+ row += config.stride * 16;
+ }
+
+ TIFFWriteDirectory(tif);
+
+ /* Create a new IFD for the RAW image. */
+ const uint16_t cfaRepeatPatternDim[] = { 2, 2 };
+ const uint8_t cfaPlaneColor[] = {
+ CFAPatternRed,
+ CFAPatternGreen,
+ CFAPatternBlue
+ };
+
+ TIFFSetField(tif, TIFFTAG_SUBFILETYPE, 0);
+ TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, config.size.width);
+ TIFFSetField(tif, TIFFTAG_IMAGELENGTH, config.size.height);
+ TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, info->bitsPerSample);
+ TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
+ TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_CFA);
+ TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1);
+ TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
+ TIFFSetField(tif, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT);
+ TIFFSetField(tif, TIFFTAG_CFAREPEATPATTERNDIM, cfaRepeatPatternDim);
+ if (TIFFLIB_VERSION < 20201219)
+ TIFFSetField(tif, TIFFTAG_CFAPATTERN, info->pattern);
+ else
+ TIFFSetField(tif, TIFFTAG_CFAPATTERN, 4, info->pattern);
+ TIFFSetField(tif, TIFFTAG_CFAPLANECOLOR, 3, cfaPlaneColor);
+ TIFFSetField(tif, TIFFTAG_CFALAYOUT, 1);
+
+ const uint16_t blackLevelRepeatDim[] = { 2, 2 };
+ float blackLevel[] = { 0.0f, 0.0f, 0.0f, 0.0f };
+ uint32_t whiteLevel = (1 << info->bitsPerSample) - 1;
+
+ const auto &blackLevels = metadata.get(controls::SensorBlackLevels);
+ if (blackLevels) {
+ Span<const int32_t, 4> levels = *blackLevels;
+
+ /*
+ * The black levels control is specified in R, Gr, Gb, B order.
+ * Map it to the TIFF tag that is specified in CFA pattern
+ * order.
+ */
+ unsigned int green = (info->pattern[0] == CFAPatternRed ||
+ info->pattern[1] == CFAPatternRed)
+ ? 0 : 1;
+
+ for (unsigned int i = 0; i < 4; ++i) {
+ unsigned int level;
+
+ switch (info->pattern[i]) {
+ case CFAPatternRed:
+ level = levels[0];
+ break;
+ case CFAPatternGreen:
+ level = levels[green + 1];
+ green = (green + 1) % 2;
+ break;
+ case CFAPatternBlue:
+ default:
+ level = levels[3];
+ break;
+ }
+
+ /* Map the 16-bit value to the bits per sample range. */
+ blackLevel[i] = level >> (16 - info->bitsPerSample);
+ }
+ }
+
+ TIFFSetField(tif, TIFFTAG_BLACKLEVELREPEATDIM, &blackLevelRepeatDim);
+ TIFFSetField(tif, TIFFTAG_BLACKLEVEL, 4, &blackLevel);
+ TIFFSetField(tif, TIFFTAG_WHITELEVEL, 1, &whiteLevel);
+
+ /* Write RAW content. */
+ row = static_cast<const uint8_t *>(data);
+ for (unsigned int y = 0; y < config.size.height; y++) {
+ info->packScanline(&scanline, row, config.size.width);
+
+ if (TIFFWriteScanline(tif, &scanline, y, 0) != 1) {
+ std::cerr << "Failed to write RAW scanline"
+ << std::endl;
+ TIFFClose(tif);
+ return -EINVAL;
+ }
+
+ row += config.stride;
+ }
+
+ /* Checkpoint the IFD to retrieve its offset, and write it out. */
+ TIFFCheckpointDirectory(tif);
+ rawIFDOffset = TIFFCurrentDirOffset(tif);
+ TIFFWriteDirectory(tif);
+
+ /* Create a new IFD for the EXIF data and fill it. */
+ TIFFCreateEXIFDirectory(tif);
+
+ /* Store creation time. */
+ time_t rawtime;
+ struct tm *timeinfo;
+ char strTime[20];
+
+ time(&rawtime);
+ timeinfo = localtime(&rawtime);
+ strftime(strTime, 20, "%Y:%m:%d %H:%M:%S", timeinfo);
+
+ /*
+ * \todo Handle timezone information by setting OffsetTimeOriginal and
+ * OffsetTimeDigitized once libtiff catches up to the specification and
+ * has EXIFTAG_ defines to handle them.
+ */
+ TIFFSetField(tif, EXIFTAG_DATETIMEORIGINAL, strTime);
+ TIFFSetField(tif, EXIFTAG_DATETIMEDIGITIZED, strTime);
+
+ const auto &analogGain = metadata.get(controls::AnalogueGain);
+ if (analogGain) {
+ uint16_t iso = std::min(std::max(*analogGain * 100, 0.0f), 65535.0f);
+ TIFFSetField(tif, EXIFTAG_ISOSPEEDRATINGS, 1, &iso);
+ }
+
+ const auto &exposureTime = metadata.get(controls::ExposureTime);
+ if (exposureTime)
+ TIFFSetField(tif, EXIFTAG_EXPOSURETIME, *exposureTime / 1e6);
+
+ TIFFWriteCustomDirectory(tif, &exifIFDOffset);
+
+ /* Update the IFD offsets and close the file. */
+ TIFFSetDirectory(tif, 0);
+ TIFFSetField(tif, TIFFTAG_SUBIFD, 1, &rawIFDOffset);
+ TIFFSetField(tif, TIFFTAG_EXIFIFD, exifIFDOffset);
+ TIFFWriteDirectory(tif);
+
+ TIFFClose(tif);
+
+ return 0;
+}
diff --git a/src/apps/cam/dng_writer.h b/src/apps/cam/dng_writer.h
new file mode 100644
index 00000000..38f38f62
--- /dev/null
+++ b/src/apps/cam/dng_writer.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * dng_writer.h - DNG writer
+ */
+
+#pragma once
+
+#ifdef HAVE_TIFF
+#define HAVE_DNG
+
+#include <libcamera/camera.h>
+#include <libcamera/controls.h>
+#include <libcamera/framebuffer.h>
+#include <libcamera/stream.h>
+
+class DNGWriter
+{
+public:
+ static int write(const char *filename, const libcamera::Camera *camera,
+ const libcamera::StreamConfiguration &config,
+ const libcamera::ControlList &metadata,
+ const libcamera::FrameBuffer *buffer, const void *data);
+};
+
+#endif /* HAVE_TIFF */
diff --git a/src/apps/cam/drm.cpp b/src/apps/cam/drm.cpp
new file mode 100644
index 00000000..2e4d7985
--- /dev/null
+++ b/src/apps/cam/drm.cpp
@@ -0,0 +1,717 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Ideas on Board Oy
+ *
+ * drm.cpp - DRM/KMS Helpers
+ */
+
+#include "drm.h"
+
+#include <algorithm>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <iostream>
+#include <set>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <libcamera/framebuffer.h>
+#include <libcamera/geometry.h>
+#include <libcamera/pixel_format.h>
+
+#include <libdrm/drm_mode.h>
+
+#include "event_loop.h"
+
+namespace DRM {
+
+Object::Object(Device *dev, uint32_t id, Type type)
+ : id_(id), dev_(dev), type_(type)
+{
+ /* Retrieve properties from the objects that support them. */
+ if (type != TypeConnector && type != TypeCrtc &&
+ type != TypeEncoder && type != TypePlane)
+ return;
+
+ /*
+ * We can't distinguish between failures due to the object having no
+ * property and failures due to other conditions. Assume we use the API
+ * correctly and consider the object has no property.
+ */
+ drmModeObjectProperties *properties = drmModeObjectGetProperties(dev->fd(), id, type);
+ if (!properties)
+ return;
+
+ properties_.reserve(properties->count_props);
+ for (uint32_t i = 0; i < properties->count_props; ++i)
+ properties_.emplace_back(properties->props[i],
+ properties->prop_values[i]);
+
+ drmModeFreeObjectProperties(properties);
+}
+
+Object::~Object()
+{
+}
+
+const Property *Object::property(const std::string &name) const
+{
+ for (const PropertyValue &pv : properties_) {
+ const Property *property = static_cast<const Property *>(dev_->object(pv.id()));
+ if (property && property->name() == name)
+ return property;
+ }
+
+ return nullptr;
+}
+
+const PropertyValue *Object::propertyValue(const std::string &name) const
+{
+ for (const PropertyValue &pv : properties_) {
+ const Property *property = static_cast<const Property *>(dev_->object(pv.id()));
+ if (property && property->name() == name)
+ return &pv;
+ }
+
+ return nullptr;
+}
+
+Property::Property(Device *dev, drmModePropertyRes *property)
+ : Object(dev, property->prop_id, TypeProperty),
+ name_(property->name), flags_(property->flags),
+ values_(property->values, property->values + property->count_values),
+ blobs_(property->blob_ids, property->blob_ids + property->count_blobs)
+{
+ if (drm_property_type_is(property, DRM_MODE_PROP_RANGE))
+ type_ = TypeRange;
+ else if (drm_property_type_is(property, DRM_MODE_PROP_ENUM))
+ type_ = TypeEnum;
+ else if (drm_property_type_is(property, DRM_MODE_PROP_BLOB))
+ type_ = TypeBlob;
+ else if (drm_property_type_is(property, DRM_MODE_PROP_BITMASK))
+ type_ = TypeBitmask;
+ else if (drm_property_type_is(property, DRM_MODE_PROP_OBJECT))
+ type_ = TypeObject;
+ else if (drm_property_type_is(property, DRM_MODE_PROP_SIGNED_RANGE))
+ type_ = TypeSignedRange;
+ else
+ type_ = TypeUnknown;
+
+ for (int i = 0; i < property->count_enums; ++i)
+ enums_[property->enums[i].value] = property->enums[i].name;
+}
+
+Blob::Blob(Device *dev, const libcamera::Span<const uint8_t> &data)
+ : Object(dev, 0, Object::TypeBlob)
+{
+ drmModeCreatePropertyBlob(dev->fd(), data.data(), data.size(), &id_);
+}
+
+Blob::~Blob()
+{
+ if (isValid())
+ drmModeDestroyPropertyBlob(device()->fd(), id());
+}
+
+Mode::Mode(const drmModeModeInfo &mode)
+ : drmModeModeInfo(mode)
+{
+}
+
+std::unique_ptr<Blob> Mode::toBlob(Device *dev) const
+{
+ libcamera::Span<const uint8_t> data{ reinterpret_cast<const uint8_t *>(this),
+ sizeof(*this) };
+ return std::make_unique<Blob>(dev, data);
+}
+
+Crtc::Crtc(Device *dev, const drmModeCrtc *crtc, unsigned int index)
+ : Object(dev, crtc->crtc_id, Object::TypeCrtc), index_(index)
+{
+}
+
+Encoder::Encoder(Device *dev, const drmModeEncoder *encoder)
+ : Object(dev, encoder->encoder_id, Object::TypeEncoder),
+ type_(encoder->encoder_type)
+{
+ const std::list<Crtc> &crtcs = dev->crtcs();
+ possibleCrtcs_.reserve(crtcs.size());
+
+ for (const Crtc &crtc : crtcs) {
+ if (encoder->possible_crtcs & (1 << crtc.index()))
+ possibleCrtcs_.push_back(&crtc);
+ }
+
+ possibleCrtcs_.shrink_to_fit();
+}
+
+namespace {
+
+const std::map<uint32_t, const char *> connectorTypeNames{
+ { DRM_MODE_CONNECTOR_Unknown, "Unknown" },
+ { DRM_MODE_CONNECTOR_VGA, "VGA" },
+ { DRM_MODE_CONNECTOR_DVII, "DVI-I" },
+ { DRM_MODE_CONNECTOR_DVID, "DVI-D" },
+ { DRM_MODE_CONNECTOR_DVIA, "DVI-A" },
+ { DRM_MODE_CONNECTOR_Composite, "Composite" },
+ { DRM_MODE_CONNECTOR_SVIDEO, "S-Video" },
+ { DRM_MODE_CONNECTOR_LVDS, "LVDS" },
+ { DRM_MODE_CONNECTOR_Component, "Component" },
+ { DRM_MODE_CONNECTOR_9PinDIN, "9-Pin-DIN" },
+ { DRM_MODE_CONNECTOR_DisplayPort, "DP" },
+ { DRM_MODE_CONNECTOR_HDMIA, "HDMI-A" },
+ { DRM_MODE_CONNECTOR_HDMIB, "HDMI-B" },
+ { DRM_MODE_CONNECTOR_TV, "TV" },
+ { DRM_MODE_CONNECTOR_eDP, "eDP" },
+ { DRM_MODE_CONNECTOR_VIRTUAL, "Virtual" },
+ { DRM_MODE_CONNECTOR_DSI, "DSI" },
+ { DRM_MODE_CONNECTOR_DPI, "DPI" },
+};
+
+} /* namespace */
+
+Connector::Connector(Device *dev, const drmModeConnector *connector)
+ : Object(dev, connector->connector_id, Object::TypeConnector),
+ type_(connector->connector_type)
+{
+ auto typeName = connectorTypeNames.find(connector->connector_type);
+ if (typeName == connectorTypeNames.end()) {
+ std::cerr
+ << "Invalid connector type "
+ << connector->connector_type << std::endl;
+ typeName = connectorTypeNames.find(DRM_MODE_CONNECTOR_Unknown);
+ }
+
+ name_ = std::string(typeName->second) + "-"
+ + std::to_string(connector->connector_type_id);
+
+ switch (connector->connection) {
+ case DRM_MODE_CONNECTED:
+ status_ = Status::Connected;
+ break;
+
+ case DRM_MODE_DISCONNECTED:
+ status_ = Status::Disconnected;
+ break;
+
+ case DRM_MODE_UNKNOWNCONNECTION:
+ default:
+ status_ = Status::Unknown;
+ break;
+ }
+
+ const std::list<Encoder> &encoders = dev->encoders();
+
+ encoders_.reserve(connector->count_encoders);
+
+ for (int i = 0; i < connector->count_encoders; ++i) {
+ uint32_t encoderId = connector->encoders[i];
+ auto encoder = std::find_if(encoders.begin(), encoders.end(),
+ [=](const Encoder &e) {
+ return e.id() == encoderId;
+ });
+ if (encoder == encoders.end()) {
+ std::cerr
+ << "Encoder " << encoderId << " not found"
+ << std::endl;
+ continue;
+ }
+
+ encoders_.push_back(&*encoder);
+ }
+
+ encoders_.shrink_to_fit();
+
+ modes_ = { connector->modes, connector->modes + connector->count_modes };
+}
+
+Plane::Plane(Device *dev, const drmModePlane *plane)
+ : Object(dev, plane->plane_id, Object::TypePlane),
+ possibleCrtcsMask_(plane->possible_crtcs)
+{
+ formats_ = { plane->formats, plane->formats + plane->count_formats };
+
+ const std::list<Crtc> &crtcs = dev->crtcs();
+ possibleCrtcs_.reserve(crtcs.size());
+
+ for (const Crtc &crtc : crtcs) {
+ if (plane->possible_crtcs & (1 << crtc.index()))
+ possibleCrtcs_.push_back(&crtc);
+ }
+
+ possibleCrtcs_.shrink_to_fit();
+}
+
+bool Plane::supportsFormat(const libcamera::PixelFormat &format) const
+{
+ return std::find(formats_.begin(), formats_.end(), format.fourcc())
+ != formats_.end();
+}
+
+int Plane::setup()
+{
+ const PropertyValue *pv = propertyValue("type");
+ if (!pv)
+ return -EINVAL;
+
+ switch (pv->value()) {
+ case DRM_PLANE_TYPE_OVERLAY:
+ type_ = TypeOverlay;
+ break;
+
+ case DRM_PLANE_TYPE_PRIMARY:
+ type_ = TypePrimary;
+ break;
+
+ case DRM_PLANE_TYPE_CURSOR:
+ type_ = TypeCursor;
+ break;
+
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+FrameBuffer::FrameBuffer(Device *dev)
+ : Object(dev, 0, Object::TypeFb)
+{
+}
+
+FrameBuffer::~FrameBuffer()
+{
+ for (const auto &plane : planes_) {
+ struct drm_gem_close gem_close = {
+ .handle = plane.second.handle,
+ .pad = 0,
+ };
+ int ret;
+
+ do {
+ ret = ioctl(device()->fd(), DRM_IOCTL_GEM_CLOSE, &gem_close);
+ } while (ret == -1 && (errno == EINTR || errno == EAGAIN));
+
+ if (ret == -1) {
+ ret = -errno;
+ std::cerr
+ << "Failed to close GEM object: "
+ << strerror(-ret) << std::endl;
+ }
+ }
+
+ drmModeRmFB(device()->fd(), id());
+}
+
+AtomicRequest::AtomicRequest(Device *dev)
+ : dev_(dev), valid_(true)
+{
+ request_ = drmModeAtomicAlloc();
+ if (!request_)
+ valid_ = false;
+}
+
+AtomicRequest::~AtomicRequest()
+{
+ if (request_)
+ drmModeAtomicFree(request_);
+}
+
+int AtomicRequest::addProperty(const Object *object, const std::string &property,
+ uint64_t value)
+{
+ if (!valid_)
+ return -EINVAL;
+
+ const Property *prop = object->property(property);
+ if (!prop) {
+ valid_ = false;
+ return -EINVAL;
+ }
+
+ return addProperty(object->id(), prop->id(), value);
+}
+
+int AtomicRequest::addProperty(const Object *object, const std::string &property,
+ std::unique_ptr<Blob> blob)
+{
+ if (!valid_)
+ return -EINVAL;
+
+ const Property *prop = object->property(property);
+ if (!prop) {
+ valid_ = false;
+ return -EINVAL;
+ }
+
+ int ret = addProperty(object->id(), prop->id(), blob->id());
+ if (ret < 0)
+ return ret;
+
+ blobs_.emplace_back(std::move(blob));
+
+ return 0;
+}
+
+int AtomicRequest::addProperty(uint32_t object, uint32_t property, uint64_t value)
+{
+ int ret = drmModeAtomicAddProperty(request_, object, property, value);
+ if (ret < 0) {
+ valid_ = false;
+ return ret;
+ }
+
+ return 0;
+}
+
+int AtomicRequest::commit(unsigned int flags)
+{
+ if (!valid_)
+ return -EINVAL;
+
+ uint32_t drmFlags = 0;
+ if (flags & FlagAllowModeset)
+ drmFlags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
+ if (flags & FlagAsync)
+ drmFlags |= DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_ATOMIC_NONBLOCK;
+ if (flags & FlagTestOnly)
+ drmFlags |= DRM_MODE_ATOMIC_TEST_ONLY;
+
+ return drmModeAtomicCommit(dev_->fd(), request_, drmFlags, this);
+}
+
+Device::Device()
+ : fd_(-1)
+{
+}
+
+Device::~Device()
+{
+ if (fd_ != -1)
+ drmClose(fd_);
+}
+
+int Device::init()
+{
+ int ret = openCard();
+ if (ret < 0) {
+ std::cerr << "Failed to open any DRM/KMS device: "
+ << strerror(-ret) << std::endl;
+ return ret;
+ }
+
+ /*
+ * Enable the atomic APIs. This also automatically enables the
+ * universal planes API.
+ */
+ ret = drmSetClientCap(fd_, DRM_CLIENT_CAP_ATOMIC, 1);
+ if (ret < 0) {
+ ret = -errno;
+ std::cerr
+ << "Failed to enable atomic capability: "
+ << strerror(-ret) << std::endl;
+ return ret;
+ }
+
+ /* List all the resources. */
+ ret = getResources();
+ if (ret < 0)
+ return ret;
+
+ EventLoop::instance()->addFdEvent(fd_, EventLoop::Read,
+ std::bind(&Device::drmEvent, this));
+
+ return 0;
+}
+
+int Device::openCard()
+{
+ const std::string dirName = "/dev/dri/";
+ bool found = false;
+ int ret;
+
+ /*
+ * Open the first DRM/KMS device beginning with /dev/dri/card. The
+ * libdrm drmOpen*() functions require either a module name or a bus ID,
+ * which we don't have, so bypass them. The automatic module loading and
+ * device node creation from drmOpen() is of no practical use as any
+ * modern system will handle that through udev or an equivalent
+ * component.
+ */
+ DIR *folder = opendir(dirName.c_str());
+ if (!folder) {
+ ret = -errno;
+ std::cerr << "Failed to open " << dirName
+ << " directory: " << strerror(-ret) << std::endl;
+ return ret;
+ }
+
+ for (struct dirent *res; (res = readdir(folder));) {
+ uint64_t cap;
+
+ if (strncmp(res->d_name, "card", 4))
+ continue;
+
+ const std::string devName = dirName + res->d_name;
+ fd_ = open(devName.c_str(), O_RDWR | O_CLOEXEC);
+ if (fd_ < 0) {
+ ret = -errno;
+ std::cerr << "Failed to open DRM/KMS device " << devName << ": "
+ << strerror(-ret) << std::endl;
+ continue;
+ }
+
+ /*
+ * Skip devices that don't support the modeset API, to avoid
+ * selecting a DRM device corresponding to a GPU. There is no
+ * modeset capability, but the kernel returns an error for most
+ * caps if mode setting isn't support by the driver. The
+ * DRM_CAP_DUMB_BUFFER capability is one of those, other would
+ * do as well. The capability value itself isn't relevant.
+ */
+ ret = drmGetCap(fd_, DRM_CAP_DUMB_BUFFER, &cap);
+ if (ret < 0) {
+ drmClose(fd_);
+ fd_ = -1;
+ continue;
+ }
+
+ found = true;
+ break;
+ }
+
+ closedir(folder);
+
+ return found ? 0 : -ENOENT;
+}
+
+int Device::getResources()
+{
+ int ret;
+
+ std::unique_ptr<drmModeRes, decltype(&drmModeFreeResources)> resources{
+ drmModeGetResources(fd_),
+ &drmModeFreeResources
+ };
+ if (!resources) {
+ ret = -errno;
+ std::cerr
+ << "Failed to get DRM/KMS resources: "
+ << strerror(-ret) << std::endl;
+ return ret;
+ }
+
+ for (int i = 0; i < resources->count_crtcs; ++i) {
+ drmModeCrtc *crtc = drmModeGetCrtc(fd_, resources->crtcs[i]);
+ if (!crtc) {
+ ret = -errno;
+ std::cerr
+ << "Failed to get CRTC: " << strerror(-ret)
+ << std::endl;
+ return ret;
+ }
+
+ crtcs_.emplace_back(this, crtc, i);
+ drmModeFreeCrtc(crtc);
+
+ Crtc &obj = crtcs_.back();
+ objects_[obj.id()] = &obj;
+ }
+
+ for (int i = 0; i < resources->count_encoders; ++i) {
+ drmModeEncoder *encoder =
+ drmModeGetEncoder(fd_, resources->encoders[i]);
+ if (!encoder) {
+ ret = -errno;
+ std::cerr
+ << "Failed to get encoder: " << strerror(-ret)
+ << std::endl;
+ return ret;
+ }
+
+ encoders_.emplace_back(this, encoder);
+ drmModeFreeEncoder(encoder);
+
+ Encoder &obj = encoders_.back();
+ objects_[obj.id()] = &obj;
+ }
+
+ for (int i = 0; i < resources->count_connectors; ++i) {
+ drmModeConnector *connector =
+ drmModeGetConnector(fd_, resources->connectors[i]);
+ if (!connector) {
+ ret = -errno;
+ std::cerr
+ << "Failed to get connector: " << strerror(-ret)
+ << std::endl;
+ return ret;
+ }
+
+ connectors_.emplace_back(this, connector);
+ drmModeFreeConnector(connector);
+
+ Connector &obj = connectors_.back();
+ objects_[obj.id()] = &obj;
+ }
+
+ std::unique_ptr<drmModePlaneRes, decltype(&drmModeFreePlaneResources)> planes{
+ drmModeGetPlaneResources(fd_),
+ &drmModeFreePlaneResources
+ };
+ if (!planes) {
+ ret = -errno;
+ std::cerr
+ << "Failed to get DRM/KMS planes: "
+ << strerror(-ret) << std::endl;
+ return ret;
+ }
+
+ for (uint32_t i = 0; i < planes->count_planes; ++i) {
+ drmModePlane *plane =
+ drmModeGetPlane(fd_, planes->planes[i]);
+ if (!plane) {
+ ret = -errno;
+ std::cerr
+ << "Failed to get plane: " << strerror(-ret)
+ << std::endl;
+ return ret;
+ }
+
+ planes_.emplace_back(this, plane);
+ drmModeFreePlane(plane);
+
+ Plane &obj = planes_.back();
+ objects_[obj.id()] = &obj;
+ }
+
+ /* Set the possible planes for each CRTC. */
+ for (Crtc &crtc : crtcs_) {
+ for (const Plane &plane : planes_) {
+ if (plane.possibleCrtcsMask_ & (1 << crtc.index()))
+ crtc.planes_.push_back(&plane);
+ }
+ }
+
+ /* Collect all property IDs and create Property instances. */
+ std::set<uint32_t> properties;
+ for (const auto &object : objects_) {
+ for (const PropertyValue &value : object.second->properties())
+ properties.insert(value.id());
+ }
+
+ for (uint32_t id : properties) {
+ drmModePropertyRes *property = drmModeGetProperty(fd_, id);
+ if (!property) {
+ ret = -errno;
+ std::cerr
+ << "Failed to get property: " << strerror(-ret)
+ << std::endl;
+ continue;
+ }
+
+ properties_.emplace_back(this, property);
+ drmModeFreeProperty(property);
+
+ Property &obj = properties_.back();
+ objects_[obj.id()] = &obj;
+ }
+
+ /* Finally, perform all delayed setup of mode objects. */
+ for (auto &object : objects_) {
+ ret = object.second->setup();
+ if (ret < 0) {
+ std::cerr
+ << "Failed to setup object " << object.second->id()
+ << ": " << strerror(-ret) << std::endl;
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+const Object *Device::object(uint32_t id)
+{
+ const auto iter = objects_.find(id);
+ if (iter == objects_.end())
+ return nullptr;
+
+ return iter->second;
+}
+
+std::unique_ptr<FrameBuffer> Device::createFrameBuffer(
+ const libcamera::FrameBuffer &buffer,
+ const libcamera::PixelFormat &format,
+ const libcamera::Size &size,
+ const std::array<uint32_t, 4> &strides)
+{
+ std::unique_ptr<FrameBuffer> fb{ new FrameBuffer(this) };
+
+ uint32_t handles[4] = {};
+ uint32_t offsets[4] = {};
+ int ret;
+
+ const std::vector<libcamera::FrameBuffer::Plane> &planes = buffer.planes();
+
+ unsigned int i = 0;
+ for (const libcamera::FrameBuffer::Plane &plane : planes) {
+ int fd = plane.fd.get();
+ uint32_t handle;
+
+ auto iter = fb->planes_.find(fd);
+ if (iter == fb->planes_.end()) {
+ ret = drmPrimeFDToHandle(fd_, plane.fd.get(), &handle);
+ if (ret < 0) {
+ ret = -errno;
+ std::cerr
+ << "Unable to import framebuffer dmabuf: "
+ << strerror(-ret) << std::endl;
+ return nullptr;
+ }
+
+ fb->planes_[fd] = { handle };
+ } else {
+ handle = iter->second.handle;
+ }
+
+ handles[i] = handle;
+ offsets[i] = plane.offset;
+ ++i;
+ }
+
+ ret = drmModeAddFB2(fd_, size.width, size.height, format.fourcc(), handles,
+ strides.data(), offsets, &fb->id_, 0);
+ if (ret < 0) {
+ ret = -errno;
+ std::cerr
+ << "Failed to add framebuffer: "
+ << strerror(-ret) << std::endl;
+ return nullptr;
+ }
+
+ return fb;
+}
+
+void Device::drmEvent()
+{
+ drmEventContext ctx{};
+ ctx.version = DRM_EVENT_CONTEXT_VERSION;
+ ctx.page_flip_handler = &Device::pageFlipComplete;
+
+ drmHandleEvent(fd_, &ctx);
+}
+
+void Device::pageFlipComplete([[maybe_unused]] int fd,
+ [[maybe_unused]] unsigned int sequence,
+ [[maybe_unused]] unsigned int tv_sec,
+ [[maybe_unused]] unsigned int tv_usec,
+ void *user_data)
+{
+ AtomicRequest *request = static_cast<AtomicRequest *>(user_data);
+ request->device()->requestComplete.emit(request);
+}
+
+} /* namespace DRM */
diff --git a/src/apps/cam/drm.h b/src/apps/cam/drm.h
new file mode 100644
index 00000000..ebaea04d
--- /dev/null
+++ b/src/apps/cam/drm.h
@@ -0,0 +1,334 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Ideas on Board Oy
+ *
+ * drm.h - DRM/KMS Helpers
+ */
+
+#pragma once
+
+#include <array>
+#include <list>
+#include <map>
+#include <memory>
+#include <stdint.h>
+#include <string>
+#include <vector>
+
+#include <libcamera/base/signal.h>
+#include <libcamera/base/span.h>
+
+#include <libdrm/drm.h>
+#include <xf86drm.h>
+#include <xf86drmMode.h>
+
+namespace libcamera {
+class FrameBuffer;
+class PixelFormat;
+class Size;
+} /* namespace libcamera */
+
+namespace DRM {
+
+class Device;
+class Plane;
+class Property;
+class PropertyValue;
+
+class Object
+{
+public:
+ enum Type {
+ TypeCrtc = DRM_MODE_OBJECT_CRTC,
+ TypeConnector = DRM_MODE_OBJECT_CONNECTOR,
+ TypeEncoder = DRM_MODE_OBJECT_ENCODER,
+ TypeMode = DRM_MODE_OBJECT_MODE,
+ TypeProperty = DRM_MODE_OBJECT_PROPERTY,
+ TypeFb = DRM_MODE_OBJECT_FB,
+ TypeBlob = DRM_MODE_OBJECT_BLOB,
+ TypePlane = DRM_MODE_OBJECT_PLANE,
+ TypeAny = DRM_MODE_OBJECT_ANY,
+ };
+
+ Object(Device *dev, uint32_t id, Type type);
+ virtual ~Object();
+
+ Device *device() const { return dev_; }
+ uint32_t id() const { return id_; }
+ Type type() const { return type_; }
+
+ const Property *property(const std::string &name) const;
+ const PropertyValue *propertyValue(const std::string &name) const;
+ const std::vector<PropertyValue> &properties() const { return properties_; }
+
+protected:
+ virtual int setup()
+ {
+ return 0;
+ }
+
+ uint32_t id_;
+
+private:
+ friend Device;
+
+ Device *dev_;
+ Type type_;
+ std::vector<PropertyValue> properties_;
+};
+
+class Property : public Object
+{
+public:
+ enum Type {
+ TypeUnknown = 0,
+ TypeRange,
+ TypeEnum,
+ TypeBlob,
+ TypeBitmask,
+ TypeObject,
+ TypeSignedRange,
+ };
+
+ Property(Device *dev, drmModePropertyRes *property);
+
+ Type type() const { return type_; }
+ const std::string &name() const { return name_; }
+
+ bool isImmutable() const { return flags_ & DRM_MODE_PROP_IMMUTABLE; }
+
+ const std::vector<uint64_t> values() const { return values_; }
+ const std::map<uint32_t, std::string> &enums() const { return enums_; }
+ const std::vector<uint32_t> blobs() const { return blobs_; }
+
+private:
+ Type type_;
+ std::string name_;
+ uint32_t flags_;
+ std::vector<uint64_t> values_;
+ std::map<uint32_t, std::string> enums_;
+ std::vector<uint32_t> blobs_;
+};
+
+class PropertyValue
+{
+public:
+ PropertyValue(uint32_t id, uint64_t value)
+ : id_(id), value_(value)
+ {
+ }
+
+ uint32_t id() const { return id_; }
+ uint32_t value() const { return value_; }
+
+private:
+ uint32_t id_;
+ uint64_t value_;
+};
+
+class Blob : public Object
+{
+public:
+ Blob(Device *dev, const libcamera::Span<const uint8_t> &data);
+ ~Blob();
+
+ bool isValid() const { return id() != 0; }
+};
+
+class Mode : public drmModeModeInfo
+{
+public:
+ Mode(const drmModeModeInfo &mode);
+
+ std::unique_ptr<Blob> toBlob(Device *dev) const;
+};
+
+class Crtc : public Object
+{
+public:
+ Crtc(Device *dev, const drmModeCrtc *crtc, unsigned int index);
+
+ unsigned int index() const { return index_; }
+ const std::vector<const Plane *> &planes() const { return planes_; }
+
+private:
+ friend Device;
+
+ unsigned int index_;
+ std::vector<const Plane *> planes_;
+};
+
+class Encoder : public Object
+{
+public:
+ Encoder(Device *dev, const drmModeEncoder *encoder);
+
+ uint32_t type() const { return type_; }
+
+ const std::vector<const Crtc *> &possibleCrtcs() const { return possibleCrtcs_; }
+
+private:
+ uint32_t type_;
+ std::vector<const Crtc *> possibleCrtcs_;
+};
+
+class Connector : public Object
+{
+public:
+ enum Status {
+ Connected,
+ Disconnected,
+ Unknown,
+ };
+
+ Connector(Device *dev, const drmModeConnector *connector);
+
+ uint32_t type() const { return type_; }
+ const std::string &name() const { return name_; }
+
+ Status status() const { return status_; }
+
+ const std::vector<const Encoder *> &encoders() const { return encoders_; }
+ const std::vector<Mode> &modes() const { return modes_; }
+
+private:
+ uint32_t type_;
+ std::string name_;
+ Status status_;
+ std::vector<const Encoder *> encoders_;
+ std::vector<Mode> modes_;
+};
+
+class Plane : public Object
+{
+public:
+ enum Type {
+ TypeOverlay,
+ TypePrimary,
+ TypeCursor,
+ };
+
+ Plane(Device *dev, const drmModePlane *plane);
+
+ Type type() const { return type_; }
+ const std::vector<uint32_t> &formats() const { return formats_; }
+ const std::vector<const Crtc *> &possibleCrtcs() const { return possibleCrtcs_; }
+
+ bool supportsFormat(const libcamera::PixelFormat &format) const;
+
+protected:
+ int setup() override;
+
+private:
+ friend class Device;
+
+ Type type_;
+ std::vector<uint32_t> formats_;
+ std::vector<const Crtc *> possibleCrtcs_;
+ uint32_t possibleCrtcsMask_;
+};
+
+class FrameBuffer : public Object
+{
+public:
+ struct Plane {
+ uint32_t handle;
+ };
+
+ ~FrameBuffer();
+
+private:
+ friend class Device;
+
+ FrameBuffer(Device *dev);
+
+ std::map<int, Plane> planes_;
+};
+
+class AtomicRequest
+{
+public:
+ enum Flags {
+ FlagAllowModeset = (1 << 0),
+ FlagAsync = (1 << 1),
+ FlagTestOnly = (1 << 2),
+ };
+
+ AtomicRequest(Device *dev);
+ ~AtomicRequest();
+
+ Device *device() const { return dev_; }
+ bool isValid() const { return valid_; }
+
+ int addProperty(const Object *object, const std::string &property,
+ uint64_t value);
+ int addProperty(const Object *object, const std::string &property,
+ std::unique_ptr<Blob> blob);
+ int commit(unsigned int flags = 0);
+
+private:
+ AtomicRequest(const AtomicRequest &) = delete;
+ AtomicRequest(const AtomicRequest &&) = delete;
+ AtomicRequest &operator=(const AtomicRequest &) = delete;
+ AtomicRequest &operator=(const AtomicRequest &&) = delete;
+
+ int addProperty(uint32_t object, uint32_t property, uint64_t value);
+
+ Device *dev_;
+ bool valid_;
+ drmModeAtomicReq *request_;
+ std::list<std::unique_ptr<Blob>> blobs_;
+};
+
+class Device
+{
+public:
+ Device();
+ ~Device();
+
+ int init();
+
+ int fd() const { return fd_; }
+
+ const std::list<Crtc> &crtcs() const { return crtcs_; }
+ const std::list<Encoder> &encoders() const { return encoders_; }
+ const std::list<Connector> &connectors() const { return connectors_; }
+ const std::list<Plane> &planes() const { return planes_; }
+ const std::list<Property> &properties() const { return properties_; }
+
+ const Object *object(uint32_t id);
+
+ std::unique_ptr<FrameBuffer> createFrameBuffer(
+ const libcamera::FrameBuffer &buffer,
+ const libcamera::PixelFormat &format,
+ const libcamera::Size &size,
+ const std::array<uint32_t, 4> &strides);
+
+ libcamera::Signal<AtomicRequest *> requestComplete;
+
+private:
+ Device(const Device &) = delete;
+ Device(const Device &&) = delete;
+ Device &operator=(const Device &) = delete;
+ Device &operator=(const Device &&) = delete;
+
+ int openCard();
+ int getResources();
+
+ void drmEvent();
+ static void pageFlipComplete(int fd, unsigned int sequence,
+ unsigned int tv_sec, unsigned int tv_usec,
+ void *user_data);
+
+ int fd_;
+
+ std::list<Crtc> crtcs_;
+ std::list<Encoder> encoders_;
+ std::list<Connector> connectors_;
+ std::list<Plane> planes_;
+ std::list<Property> properties_;
+
+ std::map<uint32_t, Object *> objects_;
+};
+
+} /* namespace DRM */
diff --git a/src/apps/cam/event_loop.cpp b/src/apps/cam/event_loop.cpp
new file mode 100644
index 00000000..cb83845c
--- /dev/null
+++ b/src/apps/cam/event_loop.cpp
@@ -0,0 +1,150 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * event_loop.cpp - cam - Event loop
+ */
+
+#include "event_loop.h"
+
+#include <assert.h>
+#include <event2/event.h>
+#include <event2/thread.h>
+#include <iostream>
+
+EventLoop *EventLoop::instance_ = nullptr;
+
+EventLoop::EventLoop()
+{
+ assert(!instance_);
+
+ evthread_use_pthreads();
+ base_ = event_base_new();
+ instance_ = this;
+}
+
+EventLoop::~EventLoop()
+{
+ instance_ = nullptr;
+
+ events_.clear();
+ event_base_free(base_);
+ libevent_global_shutdown();
+}
+
+EventLoop *EventLoop::instance()
+{
+ return instance_;
+}
+
+int EventLoop::exec()
+{
+ exitCode_ = -1;
+ event_base_loop(base_, EVLOOP_NO_EXIT_ON_EMPTY);
+ return exitCode_;
+}
+
+void EventLoop::exit(int code)
+{
+ exitCode_ = code;
+ event_base_loopbreak(base_);
+}
+
+void EventLoop::callLater(const std::function<void()> &func)
+{
+ {
+ std::unique_lock<std::mutex> locker(lock_);
+ calls_.push_back(func);
+ }
+
+ event_base_once(base_, -1, EV_TIMEOUT, dispatchCallback, this, nullptr);
+}
+
+void EventLoop::addFdEvent(int fd, EventType type,
+ const std::function<void()> &callback)
+{
+ std::unique_ptr<Event> event = std::make_unique<Event>(callback);
+ short events = (type & Read ? EV_READ : 0)
+ | (type & Write ? EV_WRITE : 0)
+ | EV_PERSIST;
+
+ event->event_ = event_new(base_, fd, events, &EventLoop::Event::dispatch,
+ event.get());
+ if (!event->event_) {
+ std::cerr << "Failed to create event for fd " << fd << std::endl;
+ return;
+ }
+
+ int ret = event_add(event->event_, nullptr);
+ if (ret < 0) {
+ std::cerr << "Failed to add event for fd " << fd << std::endl;
+ return;
+ }
+
+ events_.push_back(std::move(event));
+}
+
+void EventLoop::addTimerEvent(const std::chrono::microseconds period,
+ const std::function<void()> &callback)
+{
+ std::unique_ptr<Event> event = std::make_unique<Event>(callback);
+ event->event_ = event_new(base_, -1, EV_PERSIST, &EventLoop::Event::dispatch,
+ event.get());
+ if (!event->event_) {
+ std::cerr << "Failed to create timer event" << std::endl;
+ return;
+ }
+
+ struct timeval tv;
+ tv.tv_sec = period.count() / 1000000ULL;
+ tv.tv_usec = period.count() % 1000000ULL;
+
+ int ret = event_add(event->event_, &tv);
+ if (ret < 0) {
+ std::cerr << "Failed to add timer event" << std::endl;
+ return;
+ }
+
+ events_.push_back(std::move(event));
+}
+
+void EventLoop::dispatchCallback([[maybe_unused]] evutil_socket_t fd,
+ [[maybe_unused]] short flags, void *param)
+{
+ EventLoop *loop = static_cast<EventLoop *>(param);
+ loop->dispatchCall();
+}
+
+void EventLoop::dispatchCall()
+{
+ std::function<void()> call;
+
+ {
+ std::unique_lock<std::mutex> locker(lock_);
+ if (calls_.empty())
+ return;
+
+ call = calls_.front();
+ calls_.pop_front();
+ }
+
+ call();
+}
+
+EventLoop::Event::Event(const std::function<void()> &callback)
+ : callback_(callback), event_(nullptr)
+{
+}
+
+EventLoop::Event::~Event()
+{
+ event_del(event_);
+ event_free(event_);
+}
+
+void EventLoop::Event::dispatch([[maybe_unused]] int fd,
+ [[maybe_unused]] short events, void *arg)
+{
+ Event *event = static_cast<Event *>(arg);
+ event->callback_();
+}
diff --git a/src/apps/cam/event_loop.h b/src/apps/cam/event_loop.h
new file mode 100644
index 00000000..ef79e8e5
--- /dev/null
+++ b/src/apps/cam/event_loop.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * event_loop.h - cam - Event loop
+ */
+
+#pragma once
+
+#include <chrono>
+#include <functional>
+#include <list>
+#include <memory>
+#include <mutex>
+
+#include <event2/util.h>
+
+struct event_base;
+
+class EventLoop
+{
+public:
+ enum EventType {
+ Read = 1,
+ Write = 2,
+ };
+
+ EventLoop();
+ ~EventLoop();
+
+ static EventLoop *instance();
+
+ int exec();
+ void exit(int code = 0);
+
+ void callLater(const std::function<void()> &func);
+
+ void addFdEvent(int fd, EventType type,
+ const std::function<void()> &handler);
+
+ using duration = std::chrono::steady_clock::duration;
+ void addTimerEvent(const std::chrono::microseconds period,
+ const std::function<void()> &handler);
+
+private:
+ struct Event {
+ Event(const std::function<void()> &callback);
+ ~Event();
+
+ static void dispatch(int fd, short events, void *arg);
+
+ std::function<void()> callback_;
+ struct event *event_;
+ };
+
+ static EventLoop *instance_;
+
+ struct event_base *base_;
+ int exitCode_;
+
+ std::list<std::function<void()>> calls_;
+ std::list<std::unique_ptr<Event>> events_;
+ std::mutex lock_;
+
+ static void dispatchCallback(evutil_socket_t fd, short flags,
+ void *param);
+ void dispatchCall();
+};
diff --git a/src/apps/cam/file_sink.cpp b/src/apps/cam/file_sink.cpp
new file mode 100644
index 00000000..9d60c04e
--- /dev/null
+++ b/src/apps/cam/file_sink.cpp
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * file_sink.cpp - File Sink
+ */
+
+#include <assert.h>
+#include <fcntl.h>
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+#include <string.h>
+#include <unistd.h>
+
+#include <libcamera/camera.h>
+
+#include "dng_writer.h"
+#include "file_sink.h"
+#include "image.h"
+
+using namespace libcamera;
+
+FileSink::FileSink(const libcamera::Camera *camera,
+ const std::map<const libcamera::Stream *, std::string> &streamNames,
+ const std::string &pattern)
+ : camera_(camera), streamNames_(streamNames), pattern_(pattern)
+{
+}
+
+FileSink::~FileSink()
+{
+}
+
+int FileSink::configure(const libcamera::CameraConfiguration &config)
+{
+ int ret = FrameSink::configure(config);
+ if (ret < 0)
+ return ret;
+
+ return 0;
+}
+
+void FileSink::mapBuffer(FrameBuffer *buffer)
+{
+ std::unique_ptr<Image> image =
+ Image::fromFrameBuffer(buffer, Image::MapMode::ReadOnly);
+ assert(image != nullptr);
+
+ mappedBuffers_[buffer] = std::move(image);
+}
+
+bool FileSink::processRequest(Request *request)
+{
+ for (auto [stream, buffer] : request->buffers())
+ writeBuffer(stream, buffer, request->metadata());
+
+ return true;
+}
+
+void FileSink::writeBuffer(const Stream *stream, FrameBuffer *buffer,
+ [[maybe_unused]] const ControlList &metadata)
+{
+ std::string filename;
+ size_t pos;
+ int fd, ret = 0;
+
+ if (!pattern_.empty())
+ filename = pattern_;
+
+#ifdef HAVE_TIFF
+ bool dng = filename.find(".dng", filename.size() - 4) != std::string::npos;
+#endif /* HAVE_TIFF */
+
+ if (filename.empty() || filename.back() == '/')
+ filename += "frame-#.bin";
+
+ pos = filename.find_first_of('#');
+ if (pos != std::string::npos) {
+ std::stringstream ss;
+ ss << streamNames_[stream] << "-" << std::setw(6)
+ << std::setfill('0') << buffer->metadata().sequence;
+ filename.replace(pos, 1, ss.str());
+ }
+
+ Image *image = mappedBuffers_[buffer].get();
+
+#ifdef HAVE_TIFF
+ if (dng) {
+ ret = DNGWriter::write(filename.c_str(), camera_,
+ stream->configuration(), metadata,
+ buffer, image->data(0).data());
+ if (ret < 0)
+ std::cerr << "failed to write DNG file `" << filename
+ << "'" << std::endl;
+
+ return;
+ }
+#endif /* HAVE_TIFF */
+
+ fd = open(filename.c_str(), O_CREAT | O_WRONLY |
+ (pos == std::string::npos ? O_APPEND : O_TRUNC),
+ S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
+ if (fd == -1) {
+ ret = -errno;
+ std::cerr << "failed to open file " << filename << ": "
+ << strerror(-ret) << std::endl;
+ return;
+ }
+
+ for (unsigned int i = 0; i < buffer->planes().size(); ++i) {
+ const FrameMetadata::Plane &meta = buffer->metadata().planes()[i];
+
+ Span<uint8_t> data = image->data(i);
+ unsigned int length = std::min<unsigned int>(meta.bytesused, data.size());
+
+ if (meta.bytesused > data.size())
+ std::cerr << "payload size " << meta.bytesused
+ << " larger than plane size " << data.size()
+ << std::endl;
+
+ ret = ::write(fd, data.data(), length);
+ if (ret < 0) {
+ ret = -errno;
+ std::cerr << "write error: " << strerror(-ret)
+ << std::endl;
+ break;
+ } else if (ret != (int)length) {
+ std::cerr << "write error: only " << ret
+ << " bytes written instead of "
+ << length << std::endl;
+ break;
+ }
+ }
+
+ close(fd);
+}
diff --git a/src/apps/cam/file_sink.h b/src/apps/cam/file_sink.h
new file mode 100644
index 00000000..9ce8b619
--- /dev/null
+++ b/src/apps/cam/file_sink.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * file_sink.h - File Sink
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include <libcamera/stream.h>
+
+#include "frame_sink.h"
+
+class Image;
+
+class FileSink : public FrameSink
+{
+public:
+ FileSink(const libcamera::Camera *camera,
+ const std::map<const libcamera::Stream *, std::string> &streamNames,
+ const std::string &pattern = "");
+ ~FileSink();
+
+ int configure(const libcamera::CameraConfiguration &config) override;
+
+ void mapBuffer(libcamera::FrameBuffer *buffer) override;
+
+ bool processRequest(libcamera::Request *request) override;
+
+private:
+ void writeBuffer(const libcamera::Stream *stream,
+ libcamera::FrameBuffer *buffer,
+ const libcamera::ControlList &metadata);
+
+ const libcamera::Camera *camera_;
+ std::map<const libcamera::Stream *, std::string> streamNames_;
+ std::string pattern_;
+ std::map<libcamera::FrameBuffer *, std::unique_ptr<Image>> mappedBuffers_;
+};
diff --git a/src/apps/cam/frame_sink.cpp b/src/apps/cam/frame_sink.cpp
new file mode 100644
index 00000000..af21d575
--- /dev/null
+++ b/src/apps/cam/frame_sink.cpp
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Ideas on Board Oy
+ *
+ * frame_sink.cpp - Base Frame Sink Class
+ */
+
+#include "frame_sink.h"
+
+/**
+ * \class FrameSink
+ * \brief Abstract class to model a consumer of frames
+ *
+ * The FrameSink class models the consumer that processes frames after a request
+ * completes. It receives requests through processRequest(), and processes them
+ * synchronously or asynchronously. This allows frame sinks to hold onto frames
+ * for an extended period of time, for instance to display them until a new
+ * frame arrives.
+ *
+ * A frame sink processes whole requests, and is solely responsible for deciding
+ * how to handle different frame buffers in case multiple streams are captured.
+ */
+
+FrameSink::~FrameSink()
+{
+}
+
+int FrameSink::configure([[maybe_unused]] const libcamera::CameraConfiguration &config)
+{
+ return 0;
+}
+
+void FrameSink::mapBuffer([[maybe_unused]] libcamera::FrameBuffer *buffer)
+{
+}
+
+int FrameSink::start()
+{
+ return 0;
+}
+
+int FrameSink::stop()
+{
+ return 0;
+}
+
+/**
+ * \fn FrameSink::processRequest()
+ * \param[in] request The request
+ *
+ * This function is called to instruct the sink to process a request. The sink
+ * may process the request synchronously or queue it for asynchronous
+ * processing.
+ *
+ * When the request is processed synchronously, this function shall return true.
+ * The \a request shall not be accessed by the FrameSink after the function
+ * returns.
+ *
+ * When the request is processed asynchronously, the FrameSink temporarily takes
+ * ownership of the \a request. The function shall return false, and the
+ * FrameSink shall emit the requestProcessed signal when the request processing
+ * completes. If the stop() function is called before the request processing
+ * completes, it shall release the request synchronously.
+ *
+ * \return True if the request has been processed synchronously, false if
+ * processing has been queued
+ */
diff --git a/src/apps/cam/frame_sink.h b/src/apps/cam/frame_sink.h
new file mode 100644
index 00000000..ca4347cb
--- /dev/null
+++ b/src/apps/cam/frame_sink.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Ideas on Board Oy
+ *
+ * frame_sink.h - Base Frame Sink Class
+ */
+
+#pragma once
+
+#include <libcamera/base/signal.h>
+
+namespace libcamera {
+class CameraConfiguration;
+class FrameBuffer;
+class Request;
+} /* namespace libcamera */
+
+class FrameSink
+{
+public:
+ virtual ~FrameSink();
+
+ virtual int configure(const libcamera::CameraConfiguration &config);
+
+ virtual void mapBuffer(libcamera::FrameBuffer *buffer);
+
+ virtual int start();
+ virtual int stop();
+
+ virtual bool processRequest(libcamera::Request *request) = 0;
+ libcamera::Signal<libcamera::Request *> requestProcessed;
+};
diff --git a/src/apps/cam/image.cpp b/src/apps/cam/image.cpp
new file mode 100644
index 00000000..fe2cc6da
--- /dev/null
+++ b/src/apps/cam/image.cpp
@@ -0,0 +1,109 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2021, Ideas on Board Oy
+ *
+ * image.cpp - Multi-planar image with access to pixel data
+ */
+
+#include "image.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <iostream>
+#include <map>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+using namespace libcamera;
+
+std::unique_ptr<Image> Image::fromFrameBuffer(const FrameBuffer *buffer, MapMode mode)
+{
+ std::unique_ptr<Image> image{ new Image() };
+
+ assert(!buffer->planes().empty());
+
+ int mmapFlags = 0;
+
+ if (mode & MapMode::ReadOnly)
+ mmapFlags |= PROT_READ;
+
+ if (mode & MapMode::WriteOnly)
+ mmapFlags |= PROT_WRITE;
+
+ struct MappedBufferInfo {
+ uint8_t *address = nullptr;
+ size_t mapLength = 0;
+ size_t dmabufLength = 0;
+ };
+ std::map<int, MappedBufferInfo> mappedBuffers;
+
+ for (const FrameBuffer::Plane &plane : buffer->planes()) {
+ const int fd = plane.fd.get();
+ if (mappedBuffers.find(fd) == mappedBuffers.end()) {
+ const size_t length = lseek(fd, 0, SEEK_END);
+ mappedBuffers[fd] = MappedBufferInfo{ nullptr, 0, length };
+ }
+
+ const size_t length = mappedBuffers[fd].dmabufLength;
+
+ if (plane.offset > length ||
+ plane.offset + plane.length > length) {
+ std::cerr << "plane is out of buffer: buffer length="
+ << length << ", plane offset=" << plane.offset
+ << ", plane length=" << plane.length
+ << std::endl;
+ return nullptr;
+ }
+ size_t &mapLength = mappedBuffers[fd].mapLength;
+ mapLength = std::max(mapLength,
+ static_cast<size_t>(plane.offset + plane.length));
+ }
+
+ for (const FrameBuffer::Plane &plane : buffer->planes()) {
+ const int fd = plane.fd.get();
+ auto &info = mappedBuffers[fd];
+ if (!info.address) {
+ void *address = mmap(nullptr, info.mapLength, mmapFlags,
+ MAP_SHARED, fd, 0);
+ if (address == MAP_FAILED) {
+ int error = -errno;
+ std::cerr << "Failed to mmap plane: "
+ << strerror(-error) << std::endl;
+ return nullptr;
+ }
+
+ info.address = static_cast<uint8_t *>(address);
+ image->maps_.emplace_back(info.address, info.mapLength);
+ }
+
+ image->planes_.emplace_back(info.address + plane.offset, plane.length);
+ }
+
+ return image;
+}
+
+Image::Image() = default;
+
+Image::~Image()
+{
+ for (Span<uint8_t> &map : maps_)
+ munmap(map.data(), map.size());
+}
+
+unsigned int Image::numPlanes() const
+{
+ return planes_.size();
+}
+
+Span<uint8_t> Image::data(unsigned int plane)
+{
+ assert(plane <= planes_.size());
+ return planes_[plane];
+}
+
+Span<const uint8_t> Image::data(unsigned int plane) const
+{
+ assert(plane <= planes_.size());
+ return planes_[plane];
+}
diff --git a/src/apps/cam/image.h b/src/apps/cam/image.h
new file mode 100644
index 00000000..7953b177
--- /dev/null
+++ b/src/apps/cam/image.h
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2021, Ideas on Board Oy
+ *
+ * image.h - Multi-planar image with access to pixel data
+ */
+
+#pragma once
+
+#include <memory>
+#include <stdint.h>
+#include <vector>
+
+#include <libcamera/base/class.h>
+#include <libcamera/base/flags.h>
+#include <libcamera/base/span.h>
+
+#include <libcamera/framebuffer.h>
+
+class Image
+{
+public:
+ enum class MapMode {
+ ReadOnly = 1 << 0,
+ WriteOnly = 1 << 1,
+ ReadWrite = ReadOnly | WriteOnly,
+ };
+
+ static std::unique_ptr<Image> fromFrameBuffer(const libcamera::FrameBuffer *buffer,
+ MapMode mode);
+
+ ~Image();
+
+ unsigned int numPlanes() const;
+
+ libcamera::Span<uint8_t> data(unsigned int plane);
+ libcamera::Span<const uint8_t> data(unsigned int plane) const;
+
+private:
+ LIBCAMERA_DISABLE_COPY(Image)
+
+ Image();
+
+ std::vector<libcamera::Span<uint8_t>> maps_;
+ std::vector<libcamera::Span<uint8_t>> planes_;
+};
+
+namespace libcamera {
+LIBCAMERA_FLAGS_ENABLE_OPERATORS(Image::MapMode)
+}
diff --git a/src/apps/cam/kms_sink.cpp b/src/apps/cam/kms_sink.cpp
new file mode 100644
index 00000000..754b061e
--- /dev/null
+++ b/src/apps/cam/kms_sink.cpp
@@ -0,0 +1,538 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Ideas on Board Oy
+ *
+ * kms_sink.cpp - KMS Sink
+ */
+
+#include "kms_sink.h"
+
+#include <array>
+#include <algorithm>
+#include <assert.h>
+#include <iostream>
+#include <limits.h>
+#include <memory>
+#include <stdint.h>
+#include <string.h>
+
+#include <libcamera/camera.h>
+#include <libcamera/formats.h>
+#include <libcamera/framebuffer.h>
+#include <libcamera/stream.h>
+
+#include "drm.h"
+
+KMSSink::KMSSink(const std::string &connectorName)
+ : connector_(nullptr), crtc_(nullptr), plane_(nullptr), mode_(nullptr)
+{
+ int ret = dev_.init();
+ if (ret < 0)
+ return;
+
+ /*
+ * Find the requested connector. If no specific connector is requested,
+ * pick the first connected connector or, if no connector is connected,
+ * the first connector with unknown status.
+ */
+ for (const DRM::Connector &conn : dev_.connectors()) {
+ if (!connectorName.empty()) {
+ if (conn.name() != connectorName)
+ continue;
+
+ connector_ = &conn;
+ break;
+ }
+
+ if (conn.status() == DRM::Connector::Connected) {
+ connector_ = &conn;
+ break;
+ }
+
+ if (!connector_ && conn.status() == DRM::Connector::Unknown)
+ connector_ = &conn;
+ }
+
+ if (!connector_) {
+ if (!connectorName.empty())
+ std::cerr
+ << "Connector " << connectorName << " not found"
+ << std::endl;
+ else
+ std::cerr << "No connected connector found" << std::endl;
+ return;
+ }
+
+ dev_.requestComplete.connect(this, &KMSSink::requestComplete);
+}
+
+void KMSSink::mapBuffer(libcamera::FrameBuffer *buffer)
+{
+ std::array<uint32_t, 4> strides = {};
+
+ /* \todo Should libcamera report per-plane strides ? */
+ unsigned int uvStrideMultiplier;
+
+ switch (format_) {
+ case libcamera::formats::NV24:
+ case libcamera::formats::NV42:
+ uvStrideMultiplier = 4;
+ break;
+ case libcamera::formats::YUV420:
+ case libcamera::formats::YVU420:
+ case libcamera::formats::YUV422:
+ uvStrideMultiplier = 1;
+ break;
+ default:
+ uvStrideMultiplier = 2;
+ break;
+ }
+
+ strides[0] = stride_;
+ for (unsigned int i = 1; i < buffer->planes().size(); ++i)
+ strides[i] = stride_ * uvStrideMultiplier / 2;
+
+ std::unique_ptr<DRM::FrameBuffer> drmBuffer =
+ dev_.createFrameBuffer(*buffer, format_, size_, strides);
+ if (!drmBuffer)
+ return;
+
+ buffers_.emplace(std::piecewise_construct,
+ std::forward_as_tuple(buffer),
+ std::forward_as_tuple(std::move(drmBuffer)));
+}
+
+int KMSSink::configure(const libcamera::CameraConfiguration &config)
+{
+ if (!connector_)
+ return -EINVAL;
+
+ crtc_ = nullptr;
+ plane_ = nullptr;
+ mode_ = nullptr;
+
+ const libcamera::StreamConfiguration &cfg = config.at(0);
+
+ /* Find the best mode for the stream size. */
+ const std::vector<DRM::Mode> &modes = connector_->modes();
+
+ unsigned int cfgArea = cfg.size.width * cfg.size.height;
+ unsigned int bestDistance = UINT_MAX;
+
+ for (const DRM::Mode &mode : modes) {
+ unsigned int modeArea = mode.hdisplay * mode.vdisplay;
+ unsigned int distance = modeArea > cfgArea ? modeArea - cfgArea
+ : cfgArea - modeArea;
+
+ if (distance < bestDistance) {
+ mode_ = &mode;
+ bestDistance = distance;
+
+ /*
+ * If the sizes match exactly, there will be no better
+ * match.
+ */
+ if (distance == 0)
+ break;
+ }
+ }
+
+ if (!mode_) {
+ std::cerr << "No modes\n";
+ return -EINVAL;
+ }
+
+ int ret = configurePipeline(cfg.pixelFormat);
+ if (ret < 0)
+ return ret;
+
+ size_ = cfg.size;
+ stride_ = cfg.stride;
+
+ /* Configure color space. */
+ colorEncoding_ = std::nullopt;
+ colorRange_ = std::nullopt;
+
+ if (cfg.colorSpace->ycbcrEncoding == libcamera::ColorSpace::YcbcrEncoding::None)
+ return 0;
+
+ /*
+ * The encoding and range enums are defined in the kernel but not
+ * exposed in public headers.
+ */
+ enum drm_color_encoding {
+ DRM_COLOR_YCBCR_BT601,
+ DRM_COLOR_YCBCR_BT709,
+ DRM_COLOR_YCBCR_BT2020,
+ };
+
+ enum drm_color_range {
+ DRM_COLOR_YCBCR_LIMITED_RANGE,
+ DRM_COLOR_YCBCR_FULL_RANGE,
+ };
+
+ const DRM::Property *colorEncoding = plane_->property("COLOR_ENCODING");
+ const DRM::Property *colorRange = plane_->property("COLOR_RANGE");
+
+ if (colorEncoding) {
+ drm_color_encoding encoding;
+
+ switch (cfg.colorSpace->ycbcrEncoding) {
+ case libcamera::ColorSpace::YcbcrEncoding::Rec601:
+ default:
+ encoding = DRM_COLOR_YCBCR_BT601;
+ break;
+ case libcamera::ColorSpace::YcbcrEncoding::Rec709:
+ encoding = DRM_COLOR_YCBCR_BT709;
+ break;
+ case libcamera::ColorSpace::YcbcrEncoding::Rec2020:
+ encoding = DRM_COLOR_YCBCR_BT2020;
+ break;
+ }
+
+ for (const auto &[id, name] : colorEncoding->enums()) {
+ if (id == encoding) {
+ colorEncoding_ = encoding;
+ break;
+ }
+ }
+ }
+
+ if (colorRange) {
+ drm_color_range range;
+
+ switch (cfg.colorSpace->range) {
+ case libcamera::ColorSpace::Range::Limited:
+ default:
+ range = DRM_COLOR_YCBCR_LIMITED_RANGE;
+ break;
+ case libcamera::ColorSpace::Range::Full:
+ range = DRM_COLOR_YCBCR_FULL_RANGE;
+ break;
+ }
+
+ for (const auto &[id, name] : colorRange->enums()) {
+ if (id == range) {
+ colorRange_ = range;
+ break;
+ }
+ }
+ }
+
+ if (!colorEncoding_ || !colorRange_)
+ std::cerr << "Color space " << cfg.colorSpace->toString()
+ << " not supported by the display device."
+ << " Colors may be wrong." << std::endl;
+
+ return 0;
+}
+
+int KMSSink::selectPipeline(const libcamera::PixelFormat &format)
+{
+ /*
+ * If the requested format has an alpha channel, also consider the X
+ * variant.
+ */
+ libcamera::PixelFormat xFormat;
+
+ switch (format) {
+ case libcamera::formats::ABGR8888:
+ xFormat = libcamera::formats::XBGR8888;
+ break;
+ case libcamera::formats::ARGB8888:
+ xFormat = libcamera::formats::XRGB8888;
+ break;
+ case libcamera::formats::BGRA8888:
+ xFormat = libcamera::formats::BGRX8888;
+ break;
+ case libcamera::formats::RGBA8888:
+ xFormat = libcamera::formats::RGBX8888;
+ break;
+ }
+
+ /*
+ * Find a CRTC and plane suitable for the request format and the
+ * connector at the end of the pipeline. Restrict the search to primary
+ * planes for now.
+ */
+ for (const DRM::Encoder *encoder : connector_->encoders()) {
+ for (const DRM::Crtc *crtc : encoder->possibleCrtcs()) {
+ for (const DRM::Plane *plane : crtc->planes()) {
+ if (plane->type() != DRM::Plane::TypePrimary)
+ continue;
+
+ if (plane->supportsFormat(format)) {
+ crtc_ = crtc;
+ plane_ = plane;
+ format_ = format;
+ return 0;
+ }
+
+ if (plane->supportsFormat(xFormat)) {
+ crtc_ = crtc;
+ plane_ = plane;
+ format_ = xFormat;
+ return 0;
+ }
+ }
+ }
+ }
+
+ return -EPIPE;
+}
+
+int KMSSink::configurePipeline(const libcamera::PixelFormat &format)
+{
+ const int ret = selectPipeline(format);
+ if (ret) {
+ std::cerr
+ << "Unable to find display pipeline for format "
+ << format << std::endl;
+
+ return ret;
+ }
+
+ std::cout
+ << "Using KMS plane " << plane_->id() << ", CRTC " << crtc_->id()
+ << ", connector " << connector_->name()
+ << " (" << connector_->id() << "), mode " << mode_->hdisplay
+ << "x" << mode_->vdisplay << "@" << mode_->vrefresh << std::endl;
+
+ return 0;
+}
+
+int KMSSink::start()
+{
+ std::unique_ptr<DRM::AtomicRequest> request;
+
+ int ret = FrameSink::start();
+ if (ret < 0)
+ return ret;
+
+ /* Disable all CRTCs and planes to start from a known valid state. */
+ request = std::make_unique<DRM::AtomicRequest>(&dev_);
+
+ for (const DRM::Crtc &crtc : dev_.crtcs())
+ request->addProperty(&crtc, "ACTIVE", 0);
+
+ for (const DRM::Plane &plane : dev_.planes()) {
+ request->addProperty(&plane, "CRTC_ID", 0);
+ request->addProperty(&plane, "FB_ID", 0);
+ }
+
+ ret = request->commit(DRM::AtomicRequest::FlagAllowModeset);
+ if (ret < 0) {
+ std::cerr
+ << "Failed to disable CRTCs and planes: "
+ << strerror(-ret) << std::endl;
+ return ret;
+ }
+
+ return 0;
+}
+
+int KMSSink::stop()
+{
+ /* Display pipeline. */
+ DRM::AtomicRequest request(&dev_);
+
+ request.addProperty(connector_, "CRTC_ID", 0);
+ request.addProperty(crtc_, "ACTIVE", 0);
+ request.addProperty(crtc_, "MODE_ID", 0);
+ request.addProperty(plane_, "CRTC_ID", 0);
+ request.addProperty(plane_, "FB_ID", 0);
+
+ int ret = request.commit(DRM::AtomicRequest::FlagAllowModeset);
+ if (ret < 0) {
+ std::cerr
+ << "Failed to stop display pipeline: "
+ << strerror(-ret) << std::endl;
+ return ret;
+ }
+
+ /* Free all buffers. */
+ pending_.reset();
+ queued_.reset();
+ active_.reset();
+ buffers_.clear();
+
+ return FrameSink::stop();
+}
+
+bool KMSSink::testModeSet(DRM::FrameBuffer *drmBuffer,
+ const libcamera::Rectangle &src,
+ const libcamera::Rectangle &dst)
+{
+ DRM::AtomicRequest drmRequest{ &dev_ };
+
+ drmRequest.addProperty(connector_, "CRTC_ID", crtc_->id());
+
+ drmRequest.addProperty(crtc_, "ACTIVE", 1);
+ drmRequest.addProperty(crtc_, "MODE_ID", mode_->toBlob(&dev_));
+
+ drmRequest.addProperty(plane_, "CRTC_ID", crtc_->id());
+ drmRequest.addProperty(plane_, "FB_ID", drmBuffer->id());
+ drmRequest.addProperty(plane_, "SRC_X", src.x << 16);
+ drmRequest.addProperty(plane_, "SRC_Y", src.y << 16);
+ drmRequest.addProperty(plane_, "SRC_W", src.width << 16);
+ drmRequest.addProperty(plane_, "SRC_H", src.height << 16);
+ drmRequest.addProperty(plane_, "CRTC_X", dst.x);
+ drmRequest.addProperty(plane_, "CRTC_Y", dst.y);
+ drmRequest.addProperty(plane_, "CRTC_W", dst.width);
+ drmRequest.addProperty(plane_, "CRTC_H", dst.height);
+
+ return !drmRequest.commit(DRM::AtomicRequest::FlagAllowModeset |
+ DRM::AtomicRequest::FlagTestOnly);
+}
+
+bool KMSSink::setupComposition(DRM::FrameBuffer *drmBuffer)
+{
+ /*
+ * Test composition options, from most to least desirable, to select the
+ * best one.
+ */
+ const libcamera::Rectangle framebuffer{ size_ };
+ const libcamera::Rectangle display{ 0, 0, mode_->hdisplay, mode_->vdisplay };
+
+ /* 1. Scale the frame buffer to full screen, preserving aspect ratio. */
+ libcamera::Rectangle src = framebuffer;
+ libcamera::Rectangle dst = display.size().boundedToAspectRatio(framebuffer.size())
+ .centeredTo(display.center());
+
+ if (testModeSet(drmBuffer, src, dst)) {
+ std::cout << "KMS: full-screen scaled output, square pixels"
+ << std::endl;
+ src_ = src;
+ dst_ = dst;
+ return true;
+ }
+
+ /*
+ * 2. Scale the frame buffer to full screen, without preserving aspect
+ * ratio.
+ */
+ src = framebuffer;
+ dst = display;
+
+ if (testModeSet(drmBuffer, src, dst)) {
+ std::cout << "KMS: full-screen scaled output, non-square pixels"
+ << std::endl;
+ src_ = src;
+ dst_ = dst;
+ return true;
+ }
+
+ /* 3. Center the frame buffer on the display. */
+ src = display.size().centeredTo(framebuffer.center()).boundedTo(framebuffer);
+ dst = framebuffer.size().centeredTo(display.center()).boundedTo(display);
+
+ if (testModeSet(drmBuffer, src, dst)) {
+ std::cout << "KMS: centered output" << std::endl;
+ src_ = src;
+ dst_ = dst;
+ return true;
+ }
+
+ /* 4. Align the frame buffer on the top-left of the display. */
+ src = framebuffer.boundedTo(display);
+ dst = display.boundedTo(framebuffer);
+
+ if (testModeSet(drmBuffer, src, dst)) {
+ std::cout << "KMS: top-left aligned output" << std::endl;
+ src_ = src;
+ dst_ = dst;
+ return true;
+ }
+
+ return false;
+}
+
+bool KMSSink::processRequest(libcamera::Request *camRequest)
+{
+ /*
+ * Perform a very crude rate adaptation by simply dropping the request
+ * if the display queue is full.
+ */
+ if (pending_)
+ return true;
+
+ libcamera::FrameBuffer *buffer = camRequest->buffers().begin()->second;
+ auto iter = buffers_.find(buffer);
+ if (iter == buffers_.end())
+ return true;
+
+ DRM::FrameBuffer *drmBuffer = iter->second.get();
+
+ unsigned int flags = DRM::AtomicRequest::FlagAsync;
+ std::unique_ptr<DRM::AtomicRequest> drmRequest =
+ std::make_unique<DRM::AtomicRequest>(&dev_);
+ drmRequest->addProperty(plane_, "FB_ID", drmBuffer->id());
+
+ if (!active_ && !queued_) {
+ /* Enable the display pipeline on the first frame. */
+ if (!setupComposition(drmBuffer)) {
+ std::cerr << "Failed to setup composition" << std::endl;
+ return true;
+ }
+
+ drmRequest->addProperty(connector_, "CRTC_ID", crtc_->id());
+
+ drmRequest->addProperty(crtc_, "ACTIVE", 1);
+ drmRequest->addProperty(crtc_, "MODE_ID", mode_->toBlob(&dev_));
+
+ drmRequest->addProperty(plane_, "CRTC_ID", crtc_->id());
+ drmRequest->addProperty(plane_, "SRC_X", src_.x << 16);
+ drmRequest->addProperty(plane_, "SRC_Y", src_.y << 16);
+ drmRequest->addProperty(plane_, "SRC_W", src_.width << 16);
+ drmRequest->addProperty(plane_, "SRC_H", src_.height << 16);
+ drmRequest->addProperty(plane_, "CRTC_X", dst_.x);
+ drmRequest->addProperty(plane_, "CRTC_Y", dst_.y);
+ drmRequest->addProperty(plane_, "CRTC_W", dst_.width);
+ drmRequest->addProperty(plane_, "CRTC_H", dst_.height);
+
+ if (colorEncoding_)
+ drmRequest->addProperty(plane_, "COLOR_ENCODING", *colorEncoding_);
+ if (colorRange_)
+ drmRequest->addProperty(plane_, "COLOR_RANGE", *colorRange_);
+
+ flags |= DRM::AtomicRequest::FlagAllowModeset;
+ }
+
+ pending_ = std::make_unique<Request>(std::move(drmRequest), camRequest);
+
+ std::lock_guard<std::mutex> lock(lock_);
+
+ if (!queued_) {
+ int ret = pending_->drmRequest_->commit(flags);
+ if (ret < 0) {
+ std::cerr
+ << "Failed to commit atomic request: "
+ << strerror(-ret) << std::endl;
+ /* \todo Implement error handling */
+ }
+
+ queued_ = std::move(pending_);
+ }
+
+ return false;
+}
+
+void KMSSink::requestComplete(DRM::AtomicRequest *request)
+{
+ std::lock_guard<std::mutex> lock(lock_);
+
+ assert(queued_ && queued_->drmRequest_.get() == request);
+
+ /* Complete the active request, if any. */
+ if (active_)
+ requestProcessed.emit(active_->camRequest_);
+
+ /* The queued request becomes active. */
+ active_ = std::move(queued_);
+
+ /* Queue the pending request, if any. */
+ if (pending_) {
+ pending_->drmRequest_->commit(DRM::AtomicRequest::FlagAsync);
+ queued_ = std::move(pending_);
+ }
+}
diff --git a/src/apps/cam/kms_sink.h b/src/apps/cam/kms_sink.h
new file mode 100644
index 00000000..e2c618a1
--- /dev/null
+++ b/src/apps/cam/kms_sink.h
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Ideas on Board Oy
+ *
+ * kms_sink.h - KMS Sink
+ */
+
+#pragma once
+
+#include <list>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <string>
+#include <utility>
+
+#include <libcamera/base/signal.h>
+
+#include <libcamera/geometry.h>
+#include <libcamera/pixel_format.h>
+
+#include "drm.h"
+#include "frame_sink.h"
+
+class KMSSink : public FrameSink
+{
+public:
+ KMSSink(const std::string &connectorName);
+
+ void mapBuffer(libcamera::FrameBuffer *buffer) override;
+
+ int configure(const libcamera::CameraConfiguration &config) override;
+ int start() override;
+ int stop() override;
+
+ bool processRequest(libcamera::Request *request) override;
+
+private:
+ class Request
+ {
+ public:
+ Request(std::unique_ptr<DRM::AtomicRequest> drmRequest,
+ libcamera::Request *camRequest)
+ : drmRequest_(std::move(drmRequest)), camRequest_(camRequest)
+ {
+ }
+
+ std::unique_ptr<DRM::AtomicRequest> drmRequest_;
+ libcamera::Request *camRequest_;
+ };
+
+ int selectPipeline(const libcamera::PixelFormat &format);
+ int configurePipeline(const libcamera::PixelFormat &format);
+ bool testModeSet(DRM::FrameBuffer *drmBuffer,
+ const libcamera::Rectangle &src,
+ const libcamera::Rectangle &dst);
+ bool setupComposition(DRM::FrameBuffer *drmBuffer);
+
+ void requestComplete(DRM::AtomicRequest *request);
+
+ DRM::Device dev_;
+
+ const DRM::Connector *connector_;
+ const DRM::Crtc *crtc_;
+ const DRM::Plane *plane_;
+ const DRM::Mode *mode_;
+
+ libcamera::PixelFormat format_;
+ libcamera::Size size_;
+ unsigned int stride_;
+ std::optional<unsigned int> colorEncoding_;
+ std::optional<unsigned int> colorRange_;
+
+ libcamera::Rectangle src_;
+ libcamera::Rectangle dst_;
+
+ std::map<libcamera::FrameBuffer *, std::unique_ptr<DRM::FrameBuffer>> buffers_;
+
+ std::mutex lock_;
+ std::unique_ptr<Request> pending_;
+ std::unique_ptr<Request> queued_;
+ std::unique_ptr<Request> active_;
+};
diff --git a/src/apps/cam/main.cpp b/src/apps/cam/main.cpp
new file mode 100644
index 00000000..d70130e2
--- /dev/null
+++ b/src/apps/cam/main.cpp
@@ -0,0 +1,362 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * main.cpp - cam - The libcamera swiss army knife
+ */
+
+#include <atomic>
+#include <iomanip>
+#include <iostream>
+#include <signal.h>
+#include <string.h>
+
+#include <libcamera/libcamera.h>
+#include <libcamera/property_ids.h>
+
+#include "camera_session.h"
+#include "event_loop.h"
+#include "main.h"
+#include "options.h"
+#include "stream_options.h"
+
+using namespace libcamera;
+
+class CamApp
+{
+public:
+ CamApp();
+
+ static CamApp *instance();
+
+ int init(int argc, char **argv);
+ void cleanup();
+
+ int exec();
+ void quit();
+
+private:
+ void cameraAdded(std::shared_ptr<Camera> cam);
+ void cameraRemoved(std::shared_ptr<Camera> cam);
+ void captureDone();
+ int parseOptions(int argc, char *argv[]);
+ int run();
+
+ static std::string cameraName(const Camera *camera);
+
+ static CamApp *app_;
+ OptionsParser::Options options_;
+
+ std::unique_ptr<CameraManager> cm_;
+
+ std::atomic_uint loopUsers_;
+ EventLoop loop_;
+};
+
+CamApp *CamApp::app_ = nullptr;
+
+CamApp::CamApp()
+ : loopUsers_(0)
+{
+ CamApp::app_ = this;
+}
+
+CamApp *CamApp::instance()
+{
+ return CamApp::app_;
+}
+
+int CamApp::init(int argc, char **argv)
+{
+ int ret;
+
+ ret = parseOptions(argc, argv);
+ if (ret < 0)
+ return ret;
+
+ cm_ = std::make_unique<CameraManager>();
+
+ ret = cm_->start();
+ if (ret) {
+ std::cout << "Failed to start camera manager: "
+ << strerror(-ret) << std::endl;
+ return ret;
+ }
+
+ return 0;
+}
+
+void CamApp::cleanup()
+{
+ cm_->stop();
+}
+
+int CamApp::exec()
+{
+ int ret;
+
+ ret = run();
+ cleanup();
+
+ return ret;
+}
+
+void CamApp::quit()
+{
+ loop_.exit();
+}
+
+int CamApp::parseOptions(int argc, char *argv[])
+{
+ StreamKeyValueParser streamKeyValue;
+
+ OptionsParser parser;
+ parser.addOption(OptCamera, OptionString,
+ "Specify which camera to operate on, by id or by index", "camera",
+ ArgumentRequired, "camera", true);
+ parser.addOption(OptHelp, OptionNone, "Display this help message",
+ "help");
+ parser.addOption(OptInfo, OptionNone,
+ "Display information about stream(s)", "info");
+ parser.addOption(OptList, OptionNone, "List all cameras", "list");
+ parser.addOption(OptListControls, OptionNone, "List cameras controls",
+ "list-controls");
+ parser.addOption(OptListProperties, OptionNone, "List cameras properties",
+ "list-properties");
+ parser.addOption(OptMonitor, OptionNone,
+ "Monitor for hotplug and unplug camera events",
+ "monitor");
+
+ /* Sub-options of OptCamera: */
+ parser.addOption(OptCapture, OptionInteger,
+ "Capture until interrupted by user or until <count> frames captured",
+ "capture", ArgumentOptional, "count", false,
+ OptCamera);
+#ifdef HAVE_KMS
+ parser.addOption(OptDisplay, OptionString,
+ "Display viewfinder through DRM/KMS on specified connector",
+ "display", ArgumentOptional, "connector", false,
+ OptCamera);
+#endif
+ parser.addOption(OptFile, OptionString,
+ "Write captured frames to disk\n"
+ "If the file name ends with a '/', it sets the directory in which\n"
+ "to write files, using the default file name. Otherwise it sets the\n"
+ "full file path and name. The first '#' character in the file name\n"
+ "is expanded to the camera index, stream name and frame sequence number.\n"
+#ifdef HAVE_TIFF
+ "If the file name ends with '.dng', then the frame will be written to\n"
+ "the output file(s) in DNG format.\n"
+#endif
+ "The default file name is 'frame-#.bin'.",
+ "file", ArgumentOptional, "filename", false,
+ OptCamera);
+#ifdef HAVE_SDL
+ parser.addOption(OptSDL, OptionNone, "Display viewfinder through SDL",
+ "sdl", ArgumentNone, "", false, OptCamera);
+#endif
+ parser.addOption(OptStream, &streamKeyValue,
+ "Set configuration of a camera stream", "stream", true,
+ OptCamera);
+ parser.addOption(OptStrictFormats, OptionNone,
+ "Do not allow requested stream format(s) to be adjusted",
+ "strict-formats", ArgumentNone, nullptr, false,
+ OptCamera);
+ parser.addOption(OptMetadata, OptionNone,
+ "Print the metadata for completed requests",
+ "metadata", ArgumentNone, nullptr, false,
+ OptCamera);
+ parser.addOption(OptCaptureScript, OptionString,
+ "Load a capture session configuration script from a file",
+ "script", ArgumentRequired, "script", false,
+ OptCamera);
+
+ options_ = parser.parse(argc, argv);
+ if (!options_.valid())
+ return -EINVAL;
+
+ if (options_.empty() || options_.isSet(OptHelp)) {
+ parser.usage();
+ return options_.empty() ? -EINVAL : -EINTR;
+ }
+
+ return 0;
+}
+
+void CamApp::cameraAdded(std::shared_ptr<Camera> cam)
+{
+ std::cout << "Camera Added: " << cam->id() << std::endl;
+}
+
+void CamApp::cameraRemoved(std::shared_ptr<Camera> cam)
+{
+ std::cout << "Camera Removed: " << cam->id() << std::endl;
+}
+
+void CamApp::captureDone()
+{
+ if (--loopUsers_ == 0)
+ EventLoop::instance()->exit(0);
+}
+
+int CamApp::run()
+{
+ int ret;
+
+ /* 1. List all cameras. */
+ if (options_.isSet(OptList)) {
+ std::cout << "Available cameras:" << std::endl;
+
+ unsigned int index = 1;
+ for (const std::shared_ptr<Camera> &cam : cm_->cameras()) {
+ std::cout << index << ": " << cameraName(cam.get()) << std::endl;
+ index++;
+ }
+ }
+
+ /* 2. Create the camera sessions. */
+ std::vector<std::unique_ptr<CameraSession>> sessions;
+
+ if (options_.isSet(OptCamera)) {
+ unsigned int index = 0;
+
+ for (const OptionValue &camera : options_[OptCamera].toArray()) {
+ std::unique_ptr<CameraSession> session =
+ std::make_unique<CameraSession>(cm_.get(),
+ camera.toString(),
+ index,
+ camera.children());
+ if (!session->isValid()) {
+ std::cout << "Failed to create camera session" << std::endl;
+ return -EINVAL;
+ }
+
+ std::cout << "Using camera " << session->camera()->id()
+ << " as cam" << index << std::endl;
+
+ session->captureDone.connect(this, &CamApp::captureDone);
+
+ sessions.push_back(std::move(session));
+ index++;
+ }
+ }
+
+ /* 3. Print camera information. */
+ if (options_.isSet(OptListControls) ||
+ options_.isSet(OptListProperties) ||
+ options_.isSet(OptInfo)) {
+ for (const auto &session : sessions) {
+ if (options_.isSet(OptListControls))
+ session->listControls();
+ if (options_.isSet(OptListProperties))
+ session->listProperties();
+ if (options_.isSet(OptInfo))
+ session->infoConfiguration();
+ }
+ }
+
+ /* 4. Start capture. */
+ for (const auto &session : sessions) {
+ if (!session->options().isSet(OptCapture))
+ continue;
+
+ ret = session->start();
+ if (ret) {
+ std::cout << "Failed to start camera session" << std::endl;
+ return ret;
+ }
+
+ loopUsers_++;
+ }
+
+ /* 5. Enable hotplug monitoring. */
+ if (options_.isSet(OptMonitor)) {
+ std::cout << "Monitoring new hotplug and unplug events" << std::endl;
+ std::cout << "Press Ctrl-C to interrupt" << std::endl;
+
+ cm_->cameraAdded.connect(this, &CamApp::cameraAdded);
+ cm_->cameraRemoved.connect(this, &CamApp::cameraRemoved);
+
+ loopUsers_++;
+ }
+
+ if (loopUsers_)
+ loop_.exec();
+
+ /* 6. Stop capture. */
+ for (const auto &session : sessions) {
+ if (!session->options().isSet(OptCapture))
+ continue;
+
+ session->stop();
+ }
+
+ return 0;
+}
+
+std::string CamApp::cameraName(const Camera *camera)
+{
+ const ControlList &props = camera->properties();
+ bool addModel = true;
+ std::string name;
+
+ /*
+ * Construct the name from the camera location, model and ID. The model
+ * is only used if the location isn't present or is set to External.
+ */
+ const auto &location = props.get(properties::Location);
+ if (location) {
+ switch (*location) {
+ case properties::CameraLocationFront:
+ addModel = false;
+ name = "Internal front camera ";
+ break;
+ case properties::CameraLocationBack:
+ addModel = false;
+ name = "Internal back camera ";
+ break;
+ case properties::CameraLocationExternal:
+ name = "External camera ";
+ break;
+ }
+ }
+
+ if (addModel) {
+ /*
+ * If the camera location is not availble use the camera model
+ * to build the camera name.
+ */
+ const auto &model = props.get(properties::Model);
+ if (model)
+ name = "'" + *model + "' ";
+ }
+
+ name += "(" + camera->id() + ")";
+
+ return name;
+}
+
+void signalHandler([[maybe_unused]] int signal)
+{
+ std::cout << "Exiting" << std::endl;
+ CamApp::instance()->quit();
+}
+
+int main(int argc, char **argv)
+{
+ CamApp app;
+ int ret;
+
+ ret = app.init(argc, argv);
+ if (ret)
+ return ret == -EINTR ? 0 : EXIT_FAILURE;
+
+ struct sigaction sa = {};
+ sa.sa_handler = &signalHandler;
+ sigaction(SIGINT, &sa, nullptr);
+
+ if (app.exec())
+ return EXIT_FAILURE;
+
+ return 0;
+}
diff --git a/src/apps/cam/main.h b/src/apps/cam/main.h
new file mode 100644
index 00000000..526aecec
--- /dev/null
+++ b/src/apps/cam/main.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * main.h - Cam application
+ */
+
+#pragma once
+
+enum {
+ OptCamera = 'c',
+ OptCapture = 'C',
+ OptDisplay = 'D',
+ OptFile = 'F',
+ OptHelp = 'h',
+ OptInfo = 'I',
+ OptList = 'l',
+ OptListProperties = 'p',
+ OptMonitor = 'm',
+ OptSDL = 'S',
+ OptStream = 's',
+ OptListControls = 256,
+ OptStrictFormats = 257,
+ OptMetadata = 258,
+ OptCaptureScript = 259,
+};
diff --git a/src/apps/cam/meson.build b/src/apps/cam/meson.build
new file mode 100644
index 00000000..06dbea06
--- /dev/null
+++ b/src/apps/cam/meson.build
@@ -0,0 +1,74 @@
+# SPDX-License-Identifier: CC0-1.0
+
+libevent = dependency('libevent_pthreads', required : get_option('cam'))
+
+if not libevent.found()
+ cam_enabled = false
+ subdir_done()
+endif
+
+cam_enabled = true
+
+cam_sources = files([
+ 'camera_session.cpp',
+ 'capture_script.cpp',
+ 'event_loop.cpp',
+ 'file_sink.cpp',
+ 'frame_sink.cpp',
+ 'image.cpp',
+ 'main.cpp',
+ 'options.cpp',
+ 'stream_options.cpp',
+])
+
+cam_cpp_args = []
+
+libdrm = dependency('libdrm', required : false)
+libjpeg = dependency('libjpeg', required : false)
+libsdl2 = dependency('SDL2', required : false)
+libtiff = dependency('libtiff-4', required : false)
+
+if libdrm.found()
+ cam_cpp_args += [ '-DHAVE_KMS' ]
+ cam_sources += files([
+ 'drm.cpp',
+ 'kms_sink.cpp'
+ ])
+endif
+
+if libsdl2.found()
+ cam_cpp_args += ['-DHAVE_SDL']
+ cam_sources += files([
+ 'sdl_sink.cpp',
+ 'sdl_texture.cpp',
+ 'sdl_texture_yuv.cpp',
+ ])
+
+ if libjpeg.found()
+ cam_cpp_args += ['-DHAVE_LIBJPEG']
+ cam_sources += files([
+ 'sdl_texture_mjpg.cpp'
+ ])
+ endif
+endif
+
+if libtiff.found()
+ cam_cpp_args += ['-DHAVE_TIFF']
+ cam_sources += files([
+ 'dng_writer.cpp',
+ ])
+endif
+
+cam = executable('cam', cam_sources,
+ dependencies : [
+ libatomic,
+ libcamera_public,
+ libdrm,
+ libevent,
+ libjpeg,
+ libsdl2,
+ libtiff,
+ libyaml,
+ ],
+ cpp_args : cam_cpp_args,
+ install : true)
diff --git a/src/apps/cam/options.cpp b/src/apps/cam/options.cpp
new file mode 100644
index 00000000..4f7e8691
--- /dev/null
+++ b/src/apps/cam/options.cpp
@@ -0,0 +1,1141 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * options.cpp - cam - Options parsing
+ */
+
+#include <assert.h>
+#include <getopt.h>
+#include <iomanip>
+#include <iostream>
+#include <string.h>
+
+#include "options.h"
+
+/**
+ * \enum OptionArgument
+ * \brief Indicate if an option takes an argument
+ *
+ * \var OptionArgument::ArgumentNone
+ * \brief The option doesn't accept any argument
+ *
+ * \var OptionArgument::ArgumentRequired
+ * \brief The option requires an argument
+ *
+ * \var OptionArgument::ArgumentOptional
+ * \brief The option accepts an optional argument
+ */
+
+/**
+ * \enum OptionType
+ * \brief The type of argument for an option
+ *
+ * \var OptionType::OptionNone
+ * \brief No argument type, used for options that take no argument
+ *
+ * \var OptionType::OptionInteger
+ * \brief Integer argument type, with an optional base prefix (`0` for base 8,
+ * `0x` for base 16, none for base 10)
+ *
+ * \var OptionType::OptionString
+ * \brief String argument
+ *
+ * \var OptionType::OptionKeyValue
+ * \brief key=value list argument
+ */
+
+/* -----------------------------------------------------------------------------
+ * Option
+ */
+
+/**
+ * \struct Option
+ * \brief Store metadata about an option
+ *
+ * \var Option::opt
+ * \brief The option identifier
+ *
+ * \var Option::type
+ * \brief The type of the option argument
+ *
+ * \var Option::name
+ * \brief The option name
+ *
+ * \var Option::argument
+ * \brief Whether the option accepts an optional argument, a mandatory
+ * argument, or no argument at all
+ *
+ * \var Option::argumentName
+ * \brief The argument name used in the help text
+ *
+ * \var Option::help
+ * \brief The help text (may be a multi-line string)
+ *
+ * \var Option::keyValueParser
+ * \brief For options of type OptionType::OptionKeyValue, the key-value parser
+ * to parse the argument
+ *
+ * \var Option::isArray
+ * \brief Whether the option can appear once or multiple times
+ *
+ * \var Option::parent
+ * \brief The parent option
+ *
+ * \var Option::children
+ * \brief List of child options, storing all options whose parent is this option
+ *
+ * \fn Option::hasShortOption()
+ * \brief Tell if the option has a short option specifier (e.g. `-f`)
+ * \return True if the option has a short option specifier, false otherwise
+ *
+ * \fn Option::hasLongOption()
+ * \brief Tell if the option has a long option specifier (e.g. `--foo`)
+ * \return True if the option has a long option specifier, false otherwise
+ */
+struct Option {
+ int opt;
+ OptionType type;
+ const char *name;
+ OptionArgument argument;
+ const char *argumentName;
+ const char *help;
+ KeyValueParser *keyValueParser;
+ bool isArray;
+ Option *parent;
+ std::list<Option> children;
+
+ bool hasShortOption() const { return isalnum(opt); }
+ bool hasLongOption() const { return name != nullptr; }
+ const char *typeName() const;
+ std::string optionName() const;
+};
+
+/**
+ * \brief Retrieve a string describing the option type
+ * \return A string describing the option type
+ */
+const char *Option::typeName() const
+{
+ switch (type) {
+ case OptionNone:
+ return "none";
+
+ case OptionInteger:
+ return "integer";
+
+ case OptionString:
+ return "string";
+
+ case OptionKeyValue:
+ return "key=value";
+ }
+
+ return "unknown";
+}
+
+/**
+ * \brief Retrieve a string describing the option name, with leading dashes
+ * \return A string describing the option name, as a long option identifier
+ * (double dash) if the option has a name, or a short option identifier (single
+ * dash) otherwise
+ */
+std::string Option::optionName() const
+{
+ if (name)
+ return "--" + std::string(name);
+ else
+ return "-" + std::string(1, opt);
+}
+
+/* -----------------------------------------------------------------------------
+ * OptionBase<T>
+ */
+
+/**
+ * \class template<typename T> OptionBase
+ * \brief Container to store the values of parsed options
+ * \tparam T The type through which options are identified
+ *
+ * The OptionsBase class is generated by a parser (either OptionsParser or
+ * KeyValueParser) when parsing options. It stores values for all the options
+ * found, and exposes accessor functions to retrieve them. The options are
+ * accessed through an identifier to type \a T, which is an int referencing an
+ * Option::opt for OptionsParser, or a std::string referencing an Option::name
+ * for KeyValueParser.
+ */
+
+/**
+ * \fn OptionsBase::OptionsBase()
+ * \brief Construct an OptionsBase instance
+ *
+ * The constructed instance is initially invalid, and will be populated by the
+ * options parser.
+ */
+
+/**
+ * \brief Tell if the stored options list is empty
+ * \return True if the container is empty, false otherwise
+ */
+template<typename T>
+bool OptionsBase<T>::empty() const
+{
+ return values_.empty();
+}
+
+/**
+ * \brief Tell if the options parsing completed successfully
+ * \return True if the container is returned after successfully parsing
+ * options, false if it is returned after an error was detected during parsing
+ */
+template<typename T>
+bool OptionsBase<T>::valid() const
+{
+ return valid_;
+}
+
+/**
+ * \brief Tell if the option \a opt is specified
+ * \param[in] opt The option to search for
+ * \return True if the \a opt option is set, false otherwise
+ */
+template<typename T>
+bool OptionsBase<T>::isSet(const T &opt) const
+{
+ return values_.find(opt) != values_.end();
+}
+
+/**
+ * \brief Retrieve the value of option \a opt
+ * \param[in] opt The option to retrieve
+ * \return The value of option \a opt if found, an empty OptionValue otherwise
+ */
+template<typename T>
+const OptionValue &OptionsBase<T>::operator[](const T &opt) const
+{
+ static const OptionValue empty;
+
+ auto it = values_.find(opt);
+ if (it != values_.end())
+ return it->second;
+ return empty;
+}
+
+/**
+ * \brief Mark the container as invalid
+ *
+ * This function can be used in a key-value parser's override of the
+ * KeyValueParser::parse() function to mark the returned options as invalid if
+ * a validation error occurs.
+ */
+template<typename T>
+void OptionsBase<T>::invalidate()
+{
+ valid_ = false;
+}
+
+template<typename T>
+bool OptionsBase<T>::parseValue(const T &opt, const Option &option,
+ const char *arg)
+{
+ OptionValue value;
+
+ switch (option.type) {
+ case OptionNone:
+ break;
+
+ case OptionInteger:
+ unsigned int integer;
+
+ if (arg) {
+ char *endptr;
+ integer = strtoul(arg, &endptr, 0);
+ if (*endptr != '\0')
+ return false;
+ } else {
+ integer = 0;
+ }
+
+ value = OptionValue(integer);
+ break;
+
+ case OptionString:
+ value = OptionValue(arg ? arg : "");
+ break;
+
+ case OptionKeyValue:
+ KeyValueParser *kvParser = option.keyValueParser;
+ KeyValueParser::Options keyValues = kvParser->parse(arg);
+ if (!keyValues.valid())
+ return false;
+
+ value = OptionValue(keyValues);
+ break;
+ }
+
+ if (option.isArray)
+ values_[opt].addValue(value);
+ else
+ values_[opt] = value;
+
+ return true;
+}
+
+template class OptionsBase<int>;
+template class OptionsBase<std::string>;
+
+/* -----------------------------------------------------------------------------
+ * KeyValueParser
+ */
+
+/**
+ * \class KeyValueParser
+ * \brief A specialized parser for list of key-value pairs
+ *
+ * The KeyValueParser is an options parser for comma-separated lists of
+ * `key=value` pairs. The supported keys are added to the parser with
+ * addOption(). A given key can only appear once in the parsed list.
+ *
+ * Instances of this class can be passed to the OptionsParser::addOption()
+ * function to create options that take key-value pairs as an option argument.
+ * Specialized versions of the key-value parser can be created by inheriting
+ * from this class, to pre-build the options list in the constructor, and to add
+ * custom validation by overriding the parse() function.
+ */
+
+/**
+ * \class KeyValueParser::Options
+ * \brief An option list generated by the key-value parser
+ *
+ * This is a specialization of OptionsBase with the option reference type set to
+ * std::string.
+ */
+
+KeyValueParser::KeyValueParser() = default;
+KeyValueParser::~KeyValueParser() = default;
+
+/**
+ * \brief Add a supported option to the parser
+ * \param[in] name The option name, corresponding to the key name in the
+ * key=value pair. The name shall be unique.
+ * \param[in] type The type of the value in the key=value pair
+ * \param[in] help The help text
+ * \param[in] argument Whether the value is optional, mandatory or not allowed.
+ * Shall be ArgumentNone if \a type is OptionNone.
+ *
+ * \sa OptionsParser
+ *
+ * \return True if the option was added successfully, false if an error
+ * occurred.
+ */
+bool KeyValueParser::addOption(const char *name, OptionType type,
+ const char *help, OptionArgument argument)
+{
+ if (!name)
+ return false;
+ if (!help || help[0] == '\0')
+ return false;
+ if (argument != ArgumentNone && type == OptionNone)
+ return false;
+
+ /* Reject duplicate options. */
+ if (optionsMap_.find(name) != optionsMap_.end())
+ return false;
+
+ optionsMap_[name] = Option({ 0, type, name, argument, nullptr,
+ help, nullptr, false, nullptr, {} });
+ return true;
+}
+
+/**
+ * \brief Parse a string containing a list of key-value pairs
+ * \param[in] arguments The key-value pairs string to parse
+ *
+ * If a parsing error occurs, the parsing stops and the function returns an
+ * invalid container. The container is populated with the options successfully
+ * parsed so far.
+ *
+ * \return A valid container with the list of parsed options on success, or an
+ * invalid container otherwise
+ */
+KeyValueParser::Options KeyValueParser::parse(const char *arguments)
+{
+ Options options;
+
+ for (const char *pair = arguments; *arguments != '\0'; pair = arguments) {
+ const char *comma = strchrnul(arguments, ',');
+ size_t len = comma - pair;
+
+ /* Skip over the comma. */
+ arguments = *comma == ',' ? comma + 1 : comma;
+
+ /* Skip to the next pair if the pair is empty. */
+ if (!len)
+ continue;
+
+ std::string key;
+ std::string value;
+
+ const char *separator = static_cast<const char *>(memchr(pair, '=', len));
+ if (!separator) {
+ key = std::string(pair, len);
+ value = "";
+ } else {
+ key = std::string(pair, separator - pair);
+ value = std::string(separator + 1, comma - separator - 1);
+ }
+
+ /* The key is mandatory, the value might be optional. */
+ if (key.empty())
+ continue;
+
+ if (optionsMap_.find(key) == optionsMap_.end()) {
+ std::cerr << "Invalid option " << key << std::endl;
+ return options;
+ }
+
+ OptionArgument arg = optionsMap_[key].argument;
+ if (value.empty() && arg == ArgumentRequired) {
+ std::cerr << "Option " << key << " requires an argument"
+ << std::endl;
+ return options;
+ } else if (!value.empty() && arg == ArgumentNone) {
+ std::cerr << "Option " << key << " takes no argument"
+ << std::endl;
+ return options;
+ }
+
+ const Option &option = optionsMap_[key];
+ if (!options.parseValue(key, option, value.c_str())) {
+ std::cerr << "Failed to parse '" << value << "' as "
+ << option.typeName() << " for option " << key
+ << std::endl;
+ return options;
+ }
+ }
+
+ options.valid_ = true;
+ return options;
+}
+
+unsigned int KeyValueParser::maxOptionLength() const
+{
+ unsigned int maxLength = 0;
+
+ for (auto const &iter : optionsMap_) {
+ const Option &option = iter.second;
+ unsigned int length = 10 + strlen(option.name);
+ if (option.argument != ArgumentNone)
+ length += 1 + strlen(option.typeName());
+ if (option.argument == ArgumentOptional)
+ length += 2;
+
+ if (length > maxLength)
+ maxLength = length;
+ }
+
+ return maxLength;
+}
+
+void KeyValueParser::usage(int indent)
+{
+ for (auto const &iter : optionsMap_) {
+ const Option &option = iter.second;
+ std::string argument = std::string(" ") + option.name;
+
+ if (option.argument != ArgumentNone) {
+ if (option.argument == ArgumentOptional)
+ argument += "[=";
+ else
+ argument += "=";
+ argument += option.typeName();
+ if (option.argument == ArgumentOptional)
+ argument += "]";
+ }
+
+ std::cerr << std::setw(indent) << argument;
+
+ for (const char *help = option.help, *end = help; end;) {
+ end = strchr(help, '\n');
+ if (end) {
+ std::cerr << std::string(help, end - help + 1);
+ std::cerr << std::setw(indent) << " ";
+ help = end + 1;
+ } else {
+ std::cerr << help << std::endl;
+ }
+ }
+ }
+}
+
+/* -----------------------------------------------------------------------------
+ * OptionValue
+ */
+
+/**
+ * \class OptionValue
+ * \brief Container to store the value of an option
+ *
+ * The OptionValue class is a variant-type container to store the value of an
+ * option. It supports empty values, integers, strings, key-value lists, as well
+ * as arrays of those types. For array values, all array elements shall have the
+ * same type.
+ *
+ * OptionValue instances are organized in a tree-based structure that matches
+ * the parent-child relationship of the options added to the parser. Children
+ * are retrieved with the children() function, and are stored as an
+ * OptionsBase<int>.
+ */
+
+/**
+ * \enum OptionValue::ValueType
+ * \brief The option value type
+ *
+ * \var OptionValue::ValueType::ValueNone
+ * \brief Empty value
+ *
+ * \var OptionValue::ValueType::ValueInteger
+ * \brief Integer value (int)
+ *
+ * \var OptionValue::ValueType::ValueString
+ * \brief String value (std::string)
+ *
+ * \var OptionValue::ValueType::ValueKeyValue
+ * \brief Key-value list value (KeyValueParser::Options)
+ *
+ * \var OptionValue::ValueType::ValueArray
+ * \brief Array value
+ */
+
+/**
+ * \brief Construct an empty OptionValue instance
+ *
+ * The value type is set to ValueType::ValueNone.
+ */
+OptionValue::OptionValue()
+ : type_(ValueNone), integer_(0)
+{
+}
+
+/**
+ * \brief Construct an integer OptionValue instance
+ * \param[in] value The integer value
+ *
+ * The value type is set to ValueType::ValueInteger.
+ */
+OptionValue::OptionValue(int value)
+ : type_(ValueInteger), integer_(value)
+{
+}
+
+/**
+ * \brief Construct a string OptionValue instance
+ * \param[in] value The string value
+ *
+ * The value type is set to ValueType::ValueString.
+ */
+OptionValue::OptionValue(const char *value)
+ : type_(ValueString), integer_(0), string_(value)
+{
+}
+
+/**
+ * \brief Construct a string OptionValue instance
+ * \param[in] value The string value
+ *
+ * The value type is set to ValueType::ValueString.
+ */
+OptionValue::OptionValue(const std::string &value)
+ : type_(ValueString), integer_(0), string_(value)
+{
+}
+
+/**
+ * \brief Construct a key-value OptionValue instance
+ * \param[in] value The key-value list
+ *
+ * The value type is set to ValueType::ValueKeyValue.
+ */
+OptionValue::OptionValue(const KeyValueParser::Options &value)
+ : type_(ValueKeyValue), integer_(0), keyValues_(value)
+{
+}
+
+/**
+ * \brief Add an entry to an array value
+ * \param[in] value The entry value
+ *
+ * This function can only be called if the OptionValue type is
+ * ValueType::ValueNone or ValueType::ValueArray. Upon return, the type will be
+ * set to ValueType::ValueArray.
+ */
+void OptionValue::addValue(const OptionValue &value)
+{
+ assert(type_ == ValueNone || type_ == ValueArray);
+
+ type_ = ValueArray;
+ array_.push_back(value);
+}
+
+/**
+ * \fn OptionValue::type()
+ * \brief Retrieve the value type
+ * \return The value type
+ */
+
+/**
+ * \fn OptionValue::empty()
+ * \brief Check if the value is empty
+ * \return True if the value is empty (type set to ValueType::ValueNone), or
+ * false otherwise
+ */
+
+/**
+ * \brief Cast the value to an int
+ * \return The option value as an int, or 0 if the value type isn't
+ * ValueType::ValueInteger
+ */
+OptionValue::operator int() const
+{
+ return toInteger();
+}
+
+/**
+ * \brief Cast the value to a std::string
+ * \return The option value as an std::string, or an empty string if the value
+ * type isn't ValueType::ValueString
+ */
+OptionValue::operator std::string() const
+{
+ return toString();
+}
+
+/**
+ * \brief Retrieve the value as an int
+ * \return The option value as an int, or 0 if the value type isn't
+ * ValueType::ValueInteger
+ */
+int OptionValue::toInteger() const
+{
+ if (type_ != ValueInteger)
+ return 0;
+
+ return integer_;
+}
+
+/**
+ * \brief Retrieve the value as a std::string
+ * \return The option value as a std::string, or an empty string if the value
+ * type isn't ValueType::ValueString
+ */
+std::string OptionValue::toString() const
+{
+ if (type_ != ValueString)
+ return std::string();
+
+ return string_;
+}
+
+/**
+ * \brief Retrieve the value as a key-value list
+ *
+ * The behaviour is undefined if the value type isn't ValueType::ValueKeyValue.
+ *
+ * \return The option value as a KeyValueParser::Options
+ */
+const KeyValueParser::Options &OptionValue::toKeyValues() const
+{
+ assert(type_ == ValueKeyValue);
+ return keyValues_;
+}
+
+/**
+ * \brief Retrieve the value as an array
+ *
+ * The behaviour is undefined if the value type isn't ValueType::ValueArray.
+ *
+ * \return The option value as a std::vector of OptionValue
+ */
+const std::vector<OptionValue> &OptionValue::toArray() const
+{
+ assert(type_ == ValueArray);
+ return array_;
+}
+
+/**
+ * \brief Retrieve the list of child values
+ * \return The list of child values
+ */
+const OptionsParser::Options &OptionValue::children() const
+{
+ return children_;
+}
+
+/* -----------------------------------------------------------------------------
+ * OptionsParser
+ */
+
+/**
+ * \class OptionsParser
+ * \brief A command line options parser
+ *
+ * The OptionsParser class is an easy to use options parser for POSIX-style
+ * command line options. Supports short (e.g. `-f`) and long (e.g. `--foo`)
+ * options, optional and mandatory arguments, automatic parsing arguments for
+ * integer types and comma-separated list of key=value pairs, and multi-value
+ * arguments. It handles help text generation automatically.
+ *
+ * An OptionsParser instance is initialized by adding supported options with
+ * addOption(). Options are specified by an identifier and a name. If the
+ * identifier is an alphanumeric character, it will be used by the parser as a
+ * short option identifier (e.g. `-f`). The name, if specified, will be used as
+ * a long option identifier (e.g. `--foo`). It should not include the double
+ * dashes. The name is optional if the option identifier is an alphanumeric
+ * character and mandatory otherwise.
+ *
+ * An option has a mandatory help text, which is used to print the full options
+ * list with the usage() function. The help text may be a multi-line string.
+ * Correct indentation of the help text is handled automatically.
+ *
+ * Options accept arguments when created with OptionArgument::ArgumentRequired
+ * or OptionArgument::ArgumentOptional. If the argument is required, it can be
+ * specified as a positional argument after the option (e.g. `-f bar`,
+ * `--foo bar`), collated with the short option (e.g. `-fbar`) or separated from
+ * the long option by an equal sign (e.g. `--foo=bar`'). When the argument is
+ * optional, it must be collated with the short option or separated from the
+ * long option by an equal sign.
+ *
+ * If an option has a required or optional argument, an argument name must be
+ * set when adding the option. The argument name is used in the help text as a
+ * place holder for an argument value. For instance, a `--write` option that
+ * takes a file name as an argument could set the argument name to `filename`,
+ * and the help text would display `--write filename`. This is only used to
+ * clarify the help text and has no effect on option parsing.
+ *
+ * The option type tells the parser how to process the argument. Arguments for
+ * string options (OptionType::OptionString) are stored as-is without any
+ * processing. Arguments for integer options (OptionType::OptionInteger) are
+ * converted to an integer value, using an optional base prefix (`0` for base 8,
+ * `0x` for base 16, none for base 10). Arguments for key-value options are
+ * parsed by a KeyValueParser given to addOption().
+ *
+ * By default, a given option can appear once only in the parsed command line.
+ * If the option is created as an array option, the parser will accept multiple
+ * instances of the option. The order in which identical options are specified
+ * is preserved in the values of an array option.
+ *
+ * After preparing the parser, it can be used any number of times to parse
+ * command line options with the parse() function. The function returns an
+ * Options instance that stores the values for the parsed options. The
+ * Options::isSet() function can be used to test if an option has been found,
+ * and is the only way to access options that take no argument (specified by
+ * OptionType::OptionNone and OptionArgument::ArgumentNone). For options that
+ * accept an argument, the option value can be access by Options::operator[]()
+ * using the option identifier as the key. The order in which different options
+ * are specified on the command line isn't preserved.
+ *
+ * Options can be created with parent-child relationships to organize them as a
+ * tree instead of a flat list. When parsing a command line, the child options
+ * are considered related to the parent option that precedes them. This is
+ * useful when the parent is an array option. The Options values list generated
+ * by the parser then turns into a tree, which each parent value storing the
+ * values of child options that follow that instance of the parent option.
+ * For instance, with a `capture` option specified as a child of a `camera`
+ * array option, parsing the command line
+ *
+ * `--camera 1 --capture=10 --camera 2 --capture=20`
+ *
+ * will return an Options instance containing a single OptionValue instance of
+ * array type, for the `camera` option. The OptionValue will contain two
+ * entries, with the first entry containing the integer value 1 and the second
+ * entry the integer value 2. Each of those entries will in turn store an
+ * Options instance that contains the respective children. The first entry will
+ * store in its children a `capture` option of value 10, and the second entry a
+ * `capture` option of value 20.
+ *
+ * The command line
+ *
+ * `--capture=10 --camera 1`
+ *
+ * would result in a parsing error, as the `capture` option has no preceding
+ * `camera` option on the command line.
+ */
+
+/**
+ * \class OptionsParser::Options
+ * \brief An option list generated by the options parser
+ *
+ * This is a specialization of OptionsBase with the option reference type set to
+ * int.
+ */
+
+OptionsParser::OptionsParser() = default;
+OptionsParser::~OptionsParser() = default;
+
+/**
+ * \brief Add an option to the parser
+ * \param[in] opt The option identifier
+ * \param[in] type The type of the option argument
+ * \param[in] help The help text (may be a multi-line string)
+ * \param[in] name The option name
+ * \param[in] argument Whether the option accepts an optional argument, a
+ * mandatory argument, or no argument at all
+ * \param[in] argumentName The argument name used in the help text
+ * \param[in] array Whether the option can appear once or multiple times
+ * \param[in] parent The identifier of the parent option (optional)
+ *
+ * \return True if the option was added successfully, false if an error
+ * occurred.
+ */
+bool OptionsParser::addOption(int opt, OptionType type, const char *help,
+ const char *name, OptionArgument argument,
+ const char *argumentName, bool array, int parent)
+{
+ /*
+ * Options must have at least a short or long name, and a text message.
+ * If an argument is accepted, it must be described by argumentName.
+ */
+ if (!isalnum(opt) && !name)
+ return false;
+ if (!help || help[0] == '\0')
+ return false;
+ if (argument != ArgumentNone && !argumentName)
+ return false;
+
+ /* Reject duplicate options. */
+ if (optionsMap_.find(opt) != optionsMap_.end())
+ return false;
+
+ /*
+ * If a parent is specified, create the option as a child of its parent.
+ * Otherwise, create it in the parser's options list.
+ */
+ Option *option;
+
+ if (parent) {
+ auto iter = optionsMap_.find(parent);
+ if (iter == optionsMap_.end())
+ return false;
+
+ Option *parentOpt = iter->second;
+ parentOpt->children.push_back({
+ opt, type, name, argument, argumentName, help, nullptr,
+ array, parentOpt, {}
+ });
+ option = &parentOpt->children.back();
+ } else {
+ options_.push_back({ opt, type, name, argument, argumentName,
+ help, nullptr, array, nullptr, {} });
+ option = &options_.back();
+ }
+
+ optionsMap_[opt] = option;
+
+ return true;
+}
+
+/**
+ * \brief Add a key-value pair option to the parser
+ * \param[in] opt The option identifier
+ * \param[in] parser The KeyValueParser for the option value
+ * \param[in] help The help text (may be a multi-line string)
+ * \param[in] name The option name
+ * \param[in] array Whether the option can appear once or multiple times
+ *
+ * \sa Option
+ *
+ * \return True if the option was added successfully, false if an error
+ * occurred.
+ */
+bool OptionsParser::addOption(int opt, KeyValueParser *parser, const char *help,
+ const char *name, bool array, int parent)
+{
+ if (!addOption(opt, OptionKeyValue, help, name, ArgumentRequired,
+ "key=value[,key=value,...]", array, parent))
+ return false;
+
+ optionsMap_[opt]->keyValueParser = parser;
+ return true;
+}
+
+/**
+ * \brief Parse command line arguments
+ * \param[in] argc The number of arguments in the \a argv array
+ * \param[in] argv The array of arguments
+ *
+ * If a parsing error occurs, the parsing stops, the function prints an error
+ * message that identifies the invalid argument, prints usage information with
+ * usage(), and returns an invalid container. The container is populated with
+ * the options successfully parsed so far.
+ *
+ * \return A valid container with the list of parsed options on success, or an
+ * invalid container otherwise
+ */
+OptionsParser::Options OptionsParser::parse(int argc, char **argv)
+{
+ OptionsParser::Options options;
+
+ /*
+ * Allocate short and long options arrays large enough to contain all
+ * options.
+ */
+ char shortOptions[optionsMap_.size() * 3 + 2];
+ struct option longOptions[optionsMap_.size() + 1];
+ unsigned int ids = 0;
+ unsigned int idl = 0;
+
+ shortOptions[ids++] = ':';
+
+ for (const auto [opt, option] : optionsMap_) {
+ if (option->hasShortOption()) {
+ shortOptions[ids++] = opt;
+ if (option->argument != ArgumentNone)
+ shortOptions[ids++] = ':';
+ if (option->argument == ArgumentOptional)
+ shortOptions[ids++] = ':';
+ }
+
+ if (option->hasLongOption()) {
+ longOptions[idl].name = option->name;
+
+ switch (option->argument) {
+ case ArgumentNone:
+ longOptions[idl].has_arg = no_argument;
+ break;
+ case ArgumentRequired:
+ longOptions[idl].has_arg = required_argument;
+ break;
+ case ArgumentOptional:
+ longOptions[idl].has_arg = optional_argument;
+ break;
+ }
+
+ longOptions[idl].flag = 0;
+ longOptions[idl].val = option->opt;
+ idl++;
+ }
+ }
+
+ shortOptions[ids] = '\0';
+ memset(&longOptions[idl], 0, sizeof(longOptions[idl]));
+
+ opterr = 0;
+
+ while (true) {
+ int c = getopt_long(argc, argv, shortOptions, longOptions, nullptr);
+
+ if (c == -1)
+ break;
+
+ if (c == '?' || c == ':') {
+ if (c == '?')
+ std::cerr << "Invalid option ";
+ else
+ std::cerr << "Missing argument for option ";
+ std::cerr << argv[optind - 1] << std::endl;
+
+ usage();
+ return options;
+ }
+
+ const Option &option = *optionsMap_[c];
+ if (!parseValue(option, optarg, &options)) {
+ usage();
+ return options;
+ }
+ }
+
+ if (optind < argc) {
+ std::cerr << "Invalid non-option argument '" << argv[optind]
+ << "'" << std::endl;
+ usage();
+ return options;
+ }
+
+ options.valid_ = true;
+ return options;
+}
+
+/**
+ * \brief Print usage text to std::cerr
+ *
+ * The usage text list all the supported option with their arguments. It is
+ * generated automatically from the options added to the parser. Caller of this
+ * function may print additional usage information for the application before
+ * the list of options.
+ */
+void OptionsParser::usage()
+{
+ unsigned int indent = 0;
+
+ for (const auto &opt : optionsMap_) {
+ const Option *option = opt.second;
+ unsigned int length = 14;
+ if (option->hasLongOption())
+ length += 2 + strlen(option->name);
+ if (option->argument != ArgumentNone)
+ length += 1 + strlen(option->argumentName);
+ if (option->argument == ArgumentOptional)
+ length += 2;
+ if (option->isArray)
+ length += 4;
+
+ if (length > indent)
+ indent = length;
+
+ if (option->keyValueParser) {
+ length = option->keyValueParser->maxOptionLength();
+ if (length > indent)
+ indent = length;
+ }
+ }
+
+ indent = (indent + 7) / 8 * 8;
+
+ std::cerr << "Options:" << std::endl;
+
+ std::ios_base::fmtflags f(std::cerr.flags());
+ std::cerr << std::left;
+
+ usageOptions(options_, indent);
+
+ std::cerr.flags(f);
+}
+
+void OptionsParser::usageOptions(const std::list<Option> &options,
+ unsigned int indent)
+{
+ std::vector<const Option *> parentOptions;
+
+ for (const Option &option : options) {
+ std::string argument;
+ if (option.hasShortOption())
+ argument = std::string(" -")
+ + static_cast<char>(option.opt);
+ else
+ argument = " ";
+
+ if (option.hasLongOption()) {
+ if (option.hasShortOption())
+ argument += ", ";
+ else
+ argument += " ";
+ argument += std::string("--") + option.name;
+ }
+
+ if (option.argument != ArgumentNone) {
+ if (option.argument == ArgumentOptional)
+ argument += "[=";
+ else
+ argument += " ";
+ argument += option.argumentName;
+ if (option.argument == ArgumentOptional)
+ argument += "]";
+ }
+
+ if (option.isArray)
+ argument += " ...";
+
+ std::cerr << std::setw(indent) << argument;
+
+ for (const char *help = option.help, *end = help; end; ) {
+ end = strchr(help, '\n');
+ if (end) {
+ std::cerr << std::string(help, end - help + 1);
+ std::cerr << std::setw(indent) << " ";
+ help = end + 1;
+ } else {
+ std::cerr << help << std::endl;
+ }
+ }
+
+ if (option.keyValueParser)
+ option.keyValueParser->usage(indent);
+
+ if (!option.children.empty())
+ parentOptions.push_back(&option);
+ }
+
+ if (parentOptions.empty())
+ return;
+
+ for (const Option *option : parentOptions) {
+ std::cerr << std::endl << "Options valid in the context of "
+ << option->optionName() << ":" << std::endl;
+ usageOptions(option->children, indent);
+ }
+}
+
+std::tuple<OptionsParser::Options *, const Option *>
+OptionsParser::childOption(const Option *parent, Options *options)
+{
+ /*
+ * The parent argument points to the parent of the leaf node Option,
+ * and the options argument to the root node of the Options tree. Use
+ * recursive calls to traverse the Option tree up to the root node while
+ * traversing the Options tree down to the leaf node:
+ */
+
+ /*
+ * - If we have no parent, we've reached the root node of the Option
+ * tree, the options argument is what we need.
+ */
+ if (!parent)
+ return { options, nullptr };
+
+ /*
+ * - If the parent has a parent, use recursion to move one level up the
+ * Option tree. This returns the Options corresponding to parent, or
+ * nullptr if a suitable Options child isn't found.
+ */
+ if (parent->parent) {
+ const Option *error;
+ std::tie(options, error) = childOption(parent->parent, options);
+
+ /* Propagate the error all the way back up the call stack. */
+ if (!error)
+ return { options, error };
+ }
+
+ /*
+ * - The parent has no parent, we're now one level down the root.
+ * Return the Options child corresponding to the parent. The child may
+ * not exist if options are specified in an incorrect order.
+ */
+ if (!options->isSet(parent->opt))
+ return { nullptr, parent };
+
+ /*
+ * If the child value is of array type, children are not stored in the
+ * value .children() list, but in the .children() of the value's array
+ * elements. Use the last array element in that case, as a child option
+ * relates to the last instance of its parent option.
+ */
+ const OptionValue *value = &(*options)[parent->opt];
+ if (value->type() == OptionValue::ValueArray)
+ value = &value->toArray().back();
+
+ return { const_cast<Options *>(&value->children()), nullptr };
+}
+
+bool OptionsParser::parseValue(const Option &option, const char *arg,
+ Options *options)
+{
+ const Option *error;
+
+ std::tie(options, error) = childOption(option.parent, options);
+ if (error) {
+ std::cerr << "Option " << option.optionName() << " requires a "
+ << error->optionName() << " context" << std::endl;
+ return false;
+ }
+
+ if (!options->parseValue(option.opt, option, arg)) {
+ std::cerr << "Can't parse " << option.typeName()
+ << " argument for option " << option.optionName()
+ << std::endl;
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/apps/cam/options.h b/src/apps/cam/options.h
new file mode 100644
index 00000000..4ddd4987
--- /dev/null
+++ b/src/apps/cam/options.h
@@ -0,0 +1,157 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * options.h - cam - Options parsing
+ */
+
+#pragma once
+
+#include <ctype.h>
+#include <list>
+#include <map>
+#include <tuple>
+#include <vector>
+
+class KeyValueParser;
+class OptionValue;
+struct Option;
+
+enum OptionArgument {
+ ArgumentNone,
+ ArgumentRequired,
+ ArgumentOptional,
+};
+
+enum OptionType {
+ OptionNone,
+ OptionInteger,
+ OptionString,
+ OptionKeyValue,
+};
+
+template<typename T>
+class OptionsBase
+{
+public:
+ OptionsBase() : valid_(false) {}
+
+ bool empty() const;
+ bool valid() const;
+ bool isSet(const T &opt) const;
+ const OptionValue &operator[](const T &opt) const;
+
+ void invalidate();
+
+private:
+ friend class KeyValueParser;
+ friend class OptionsParser;
+
+ bool parseValue(const T &opt, const Option &option, const char *value);
+
+ std::map<T, OptionValue> values_;
+ bool valid_;
+};
+
+class KeyValueParser
+{
+public:
+ class Options : public OptionsBase<std::string>
+ {
+ };
+
+ KeyValueParser();
+ virtual ~KeyValueParser();
+
+ bool addOption(const char *name, OptionType type, const char *help,
+ OptionArgument argument = ArgumentNone);
+
+ virtual Options parse(const char *arguments);
+
+private:
+ KeyValueParser(const KeyValueParser &) = delete;
+ KeyValueParser &operator=(const KeyValueParser &) = delete;
+
+ friend class OptionsParser;
+ unsigned int maxOptionLength() const;
+ void usage(int indent);
+
+ std::map<std::string, Option> optionsMap_;
+};
+
+class OptionsParser
+{
+public:
+ class Options : public OptionsBase<int>
+ {
+ };
+
+ OptionsParser();
+ ~OptionsParser();
+
+ bool addOption(int opt, OptionType type, const char *help,
+ const char *name = nullptr,
+ OptionArgument argument = ArgumentNone,
+ const char *argumentName = nullptr, bool array = false,
+ int parent = 0);
+ bool addOption(int opt, KeyValueParser *parser, const char *help,
+ const char *name = nullptr, bool array = false,
+ int parent = 0);
+
+ Options parse(int argc, char *argv[]);
+ void usage();
+
+private:
+ OptionsParser(const OptionsParser &) = delete;
+ OptionsParser &operator=(const OptionsParser &) = delete;
+
+ void usageOptions(const std::list<Option> &options, unsigned int indent);
+
+ std::tuple<OptionsParser::Options *, const Option *>
+ childOption(const Option *parent, Options *options);
+ bool parseValue(const Option &option, const char *arg, Options *options);
+
+ std::list<Option> options_;
+ std::map<unsigned int, Option *> optionsMap_;
+};
+
+class OptionValue
+{
+public:
+ enum ValueType {
+ ValueNone,
+ ValueInteger,
+ ValueString,
+ ValueKeyValue,
+ ValueArray,
+ };
+
+ OptionValue();
+ OptionValue(int value);
+ OptionValue(const char *value);
+ OptionValue(const std::string &value);
+ OptionValue(const KeyValueParser::Options &value);
+
+ void addValue(const OptionValue &value);
+
+ ValueType type() const { return type_; }
+ bool empty() const { return type_ == ValueType::ValueNone; }
+
+ operator int() const;
+ operator std::string() const;
+
+ int toInteger() const;
+ std::string toString() const;
+ const KeyValueParser::Options &toKeyValues() const;
+ const std::vector<OptionValue> &toArray() const;
+
+ const OptionsParser::Options &children() const;
+
+private:
+ ValueType type_;
+ int integer_;
+ std::string string_;
+ KeyValueParser::Options keyValues_;
+ std::vector<OptionValue> array_;
+ OptionsParser::Options children_;
+};
diff --git a/src/apps/cam/sdl_sink.cpp b/src/apps/cam/sdl_sink.cpp
new file mode 100644
index 00000000..ee177227
--- /dev/null
+++ b/src/apps/cam/sdl_sink.cpp
@@ -0,0 +1,214 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Ideas on Board Oy
+ *
+ * sdl_sink.h - SDL Sink
+ */
+
+#include "sdl_sink.h"
+
+#include <assert.h>
+#include <fcntl.h>
+#include <iomanip>
+#include <iostream>
+#include <signal.h>
+#include <sstream>
+#include <string.h>
+#include <unistd.h>
+
+#include <libcamera/camera.h>
+#include <libcamera/formats.h>
+
+#include "event_loop.h"
+#include "image.h"
+#ifdef HAVE_LIBJPEG
+#include "sdl_texture_mjpg.h"
+#endif
+#include "sdl_texture_yuv.h"
+
+using namespace libcamera;
+
+using namespace std::chrono_literals;
+
+SDLSink::SDLSink()
+ : window_(nullptr), renderer_(nullptr), rect_({}),
+ init_(false)
+{
+}
+
+SDLSink::~SDLSink()
+{
+ stop();
+}
+
+int SDLSink::configure(const libcamera::CameraConfiguration &config)
+{
+ int ret = FrameSink::configure(config);
+ if (ret < 0)
+ return ret;
+
+ if (config.size() > 1) {
+ std::cerr
+ << "SDL sink only supports one camera stream at present, streaming first camera stream"
+ << std::endl;
+ } else if (config.empty()) {
+ std::cerr << "Require at least one camera stream to process"
+ << std::endl;
+ return -EINVAL;
+ }
+
+ const libcamera::StreamConfiguration &cfg = config.at(0);
+ rect_.w = cfg.size.width;
+ rect_.h = cfg.size.height;
+
+ switch (cfg.pixelFormat) {
+#ifdef HAVE_LIBJPEG
+ case libcamera::formats::MJPEG:
+ texture_ = std::make_unique<SDLTextureMJPG>(rect_);
+ break;
+#endif
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+ case libcamera::formats::NV12:
+ texture_ = std::make_unique<SDLTextureNV12>(rect_, cfg.stride);
+ break;
+#endif
+ case libcamera::formats::YUYV:
+ texture_ = std::make_unique<SDLTextureYUYV>(rect_, cfg.stride);
+ break;
+ default:
+ std::cerr << "Unsupported pixel format "
+ << cfg.pixelFormat.toString() << std::endl;
+ return -EINVAL;
+ };
+
+ return 0;
+}
+
+int SDLSink::start()
+{
+ int ret = SDL_Init(SDL_INIT_VIDEO);
+ if (ret) {
+ std::cerr << "Failed to initialize SDL: " << SDL_GetError()
+ << std::endl;
+ return ret;
+ }
+
+ init_ = true;
+ window_ = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED,
+ SDL_WINDOWPOS_UNDEFINED, rect_.w,
+ rect_.h,
+ SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
+ if (!window_) {
+ std::cerr << "Failed to create SDL window: " << SDL_GetError()
+ << std::endl;
+ return -EINVAL;
+ }
+
+ renderer_ = SDL_CreateRenderer(window_, -1, 0);
+ if (!renderer_) {
+ std::cerr << "Failed to create SDL renderer: " << SDL_GetError()
+ << std::endl;
+ return -EINVAL;
+ }
+
+ /*
+ * Set for scaling purposes, not critical, don't return in case of
+ * error.
+ */
+ ret = SDL_RenderSetLogicalSize(renderer_, rect_.w, rect_.h);
+ if (ret)
+ std::cerr << "Failed to set SDL render logical size: "
+ << SDL_GetError() << std::endl;
+
+ ret = texture_->create(renderer_);
+ if (ret) {
+ return ret;
+ }
+
+ /* \todo Make the event cancellable to support stop/start cycles. */
+ EventLoop::instance()->addTimerEvent(
+ 10ms, std::bind(&SDLSink::processSDLEvents, this));
+
+ return 0;
+}
+
+int SDLSink::stop()
+{
+ texture_.reset();
+
+ if (renderer_) {
+ SDL_DestroyRenderer(renderer_);
+ renderer_ = nullptr;
+ }
+
+ if (window_) {
+ SDL_DestroyWindow(window_);
+ window_ = nullptr;
+ }
+
+ if (init_) {
+ SDL_Quit();
+ init_ = false;
+ }
+
+ return FrameSink::stop();
+}
+
+void SDLSink::mapBuffer(FrameBuffer *buffer)
+{
+ std::unique_ptr<Image> image =
+ Image::fromFrameBuffer(buffer, Image::MapMode::ReadOnly);
+ assert(image != nullptr);
+
+ mappedBuffers_[buffer] = std::move(image);
+}
+
+bool SDLSink::processRequest(Request *request)
+{
+ for (auto [stream, buffer] : request->buffers()) {
+ renderBuffer(buffer);
+ break; /* to be expanded to launch SDL window per buffer */
+ }
+
+ return true;
+}
+
+/*
+ * Process SDL events, required for things like window resize and quit button
+ */
+void SDLSink::processSDLEvents()
+{
+ for (SDL_Event e; SDL_PollEvent(&e);) {
+ if (e.type == SDL_QUIT) {
+ /* Click close icon then quit */
+ EventLoop::instance()->exit(0);
+ }
+ }
+}
+
+void SDLSink::renderBuffer(FrameBuffer *buffer)
+{
+ Image *image = mappedBuffers_[buffer].get();
+
+ std::vector<Span<const uint8_t>> planes;
+ unsigned int i = 0;
+
+ planes.reserve(buffer->metadata().planes().size());
+
+ for (const FrameMetadata::Plane &meta : buffer->metadata().planes()) {
+ Span<uint8_t> data = image->data(i);
+ if (meta.bytesused > data.size())
+ std::cerr << "payload size " << meta.bytesused
+ << " larger than plane size " << data.size()
+ << std::endl;
+
+ planes.push_back(data);
+ i++;
+ }
+
+ texture_->update(planes);
+
+ SDL_RenderClear(renderer_);
+ SDL_RenderCopy(renderer_, texture_->get(), nullptr, nullptr);
+ SDL_RenderPresent(renderer_);
+}
diff --git a/src/apps/cam/sdl_sink.h b/src/apps/cam/sdl_sink.h
new file mode 100644
index 00000000..6c19c663
--- /dev/null
+++ b/src/apps/cam/sdl_sink.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Ideas on Board Oy
+ *
+ * sdl_sink.h - SDL Sink
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+
+#include <libcamera/stream.h>
+
+#include <SDL2/SDL.h>
+
+#include "frame_sink.h"
+
+class Image;
+class SDLTexture;
+
+class SDLSink : public FrameSink
+{
+public:
+ SDLSink();
+ ~SDLSink();
+
+ int configure(const libcamera::CameraConfiguration &config) override;
+ int start() override;
+ int stop() override;
+ void mapBuffer(libcamera::FrameBuffer *buffer) override;
+
+ bool processRequest(libcamera::Request *request) override;
+
+private:
+ void renderBuffer(libcamera::FrameBuffer *buffer);
+ void processSDLEvents();
+
+ std::map<libcamera::FrameBuffer *, std::unique_ptr<Image>>
+ mappedBuffers_;
+
+ std::unique_ptr<SDLTexture> texture_;
+
+ SDL_Window *window_;
+ SDL_Renderer *renderer_;
+ SDL_Rect rect_;
+ bool init_;
+};
diff --git a/src/apps/cam/sdl_texture.cpp b/src/apps/cam/sdl_texture.cpp
new file mode 100644
index 00000000..e9040bc5
--- /dev/null
+++ b/src/apps/cam/sdl_texture.cpp
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Ideas on Board Oy
+ *
+ * sdl_texture.cpp - SDL Texture
+ */
+
+#include "sdl_texture.h"
+
+#include <iostream>
+
+SDLTexture::SDLTexture(const SDL_Rect &rect, uint32_t pixelFormat,
+ const int stride)
+ : ptr_(nullptr), rect_(rect), pixelFormat_(pixelFormat), stride_(stride)
+{
+}
+
+SDLTexture::~SDLTexture()
+{
+ if (ptr_)
+ SDL_DestroyTexture(ptr_);
+}
+
+int SDLTexture::create(SDL_Renderer *renderer)
+{
+ ptr_ = SDL_CreateTexture(renderer, pixelFormat_,
+ SDL_TEXTUREACCESS_STREAMING, rect_.w,
+ rect_.h);
+ if (!ptr_) {
+ std::cerr << "Failed to create SDL texture: " << SDL_GetError()
+ << std::endl;
+ return -ENOMEM;
+ }
+
+ return 0;
+}
diff --git a/src/apps/cam/sdl_texture.h b/src/apps/cam/sdl_texture.h
new file mode 100644
index 00000000..6ccd85ea
--- /dev/null
+++ b/src/apps/cam/sdl_texture.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Ideas on Board Oy
+ *
+ * sdl_texture.h - SDL Texture
+ */
+
+#pragma once
+
+#include <vector>
+
+#include <SDL2/SDL.h>
+
+#include "image.h"
+
+class SDLTexture
+{
+public:
+ SDLTexture(const SDL_Rect &rect, uint32_t pixelFormat, const int stride);
+ virtual ~SDLTexture();
+ int create(SDL_Renderer *renderer);
+ virtual void update(const std::vector<libcamera::Span<const uint8_t>> &data) = 0;
+ SDL_Texture *get() const { return ptr_; }
+
+protected:
+ SDL_Texture *ptr_;
+ const SDL_Rect rect_;
+ const uint32_t pixelFormat_;
+ const int stride_;
+};
diff --git a/src/apps/cam/sdl_texture_mjpg.cpp b/src/apps/cam/sdl_texture_mjpg.cpp
new file mode 100644
index 00000000..da958e03
--- /dev/null
+++ b/src/apps/cam/sdl_texture_mjpg.cpp
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Ideas on Board Oy
+ *
+ * sdl_texture_mjpg.cpp - SDL Texture MJPG
+ */
+
+#include "sdl_texture_mjpg.h"
+
+#include <iostream>
+#include <setjmp.h>
+#include <stdio.h>
+
+#include <jpeglib.h>
+
+using namespace libcamera;
+
+struct JpegErrorManager : public jpeg_error_mgr {
+ JpegErrorManager()
+ {
+ jpeg_std_error(this);
+ error_exit = errorExit;
+ output_message = outputMessage;
+ }
+
+ static void errorExit(j_common_ptr cinfo)
+ {
+ JpegErrorManager *self =
+ static_cast<JpegErrorManager *>(cinfo->err);
+ longjmp(self->escape_, 1);
+ }
+
+ static void outputMessage([[maybe_unused]] j_common_ptr cinfo)
+ {
+ }
+
+ jmp_buf escape_;
+};
+
+SDLTextureMJPG::SDLTextureMJPG(const SDL_Rect &rect)
+ : SDLTexture(rect, SDL_PIXELFORMAT_RGB24, rect.w * 3),
+ rgb_(std::make_unique<unsigned char[]>(stride_ * rect.h))
+{
+}
+
+int SDLTextureMJPG::decompress(Span<const uint8_t> data)
+{
+ struct jpeg_decompress_struct cinfo;
+
+ JpegErrorManager errorManager;
+ if (setjmp(errorManager.escape_)) {
+ /* libjpeg found an error */
+ jpeg_destroy_decompress(&cinfo);
+ std::cerr << "JPEG decompression error" << std::endl;
+ return -EINVAL;
+ }
+
+ cinfo.err = &errorManager;
+ jpeg_create_decompress(&cinfo);
+
+ jpeg_mem_src(&cinfo, data.data(), data.size());
+
+ jpeg_read_header(&cinfo, TRUE);
+
+ jpeg_start_decompress(&cinfo);
+
+ for (int i = 0; cinfo.output_scanline < cinfo.output_height; ++i) {
+ JSAMPROW rowptr = rgb_.get() + i * stride_;
+ jpeg_read_scanlines(&cinfo, &rowptr, 1);
+ }
+
+ jpeg_finish_decompress(&cinfo);
+
+ jpeg_destroy_decompress(&cinfo);
+
+ return 0;
+}
+
+void SDLTextureMJPG::update(const std::vector<libcamera::Span<const uint8_t>> &data)
+{
+ decompress(data[0]);
+ SDL_UpdateTexture(ptr_, nullptr, rgb_.get(), stride_);
+}
diff --git a/src/apps/cam/sdl_texture_mjpg.h b/src/apps/cam/sdl_texture_mjpg.h
new file mode 100644
index 00000000..814ca79a
--- /dev/null
+++ b/src/apps/cam/sdl_texture_mjpg.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Ideas on Board Oy
+ *
+ * sdl_texture_mjpg.h - SDL Texture MJPG
+ */
+
+#pragma once
+
+#include "sdl_texture.h"
+
+class SDLTextureMJPG : public SDLTexture
+{
+public:
+ SDLTextureMJPG(const SDL_Rect &rect);
+
+ void update(const std::vector<libcamera::Span<const uint8_t>> &data) override;
+
+private:
+ int decompress(libcamera::Span<const uint8_t> data);
+
+ std::unique_ptr<unsigned char[]> rgb_;
+};
diff --git a/src/apps/cam/sdl_texture_yuv.cpp b/src/apps/cam/sdl_texture_yuv.cpp
new file mode 100644
index 00000000..b29c3b93
--- /dev/null
+++ b/src/apps/cam/sdl_texture_yuv.cpp
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Ideas on Board Oy
+ *
+ * sdl_texture_yuv.cpp - SDL YUV Textures
+ */
+
+#include "sdl_texture_yuv.h"
+
+using namespace libcamera;
+
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+SDLTextureNV12::SDLTextureNV12(const SDL_Rect &rect, unsigned int stride)
+ : SDLTexture(rect, SDL_PIXELFORMAT_NV12, stride)
+{
+}
+
+void SDLTextureNV12::update(const std::vector<libcamera::Span<const uint8_t>> &data)
+{
+ SDL_UpdateNVTexture(ptr_, &rect_, data[0].data(), stride_,
+ data[1].data(), stride_);
+}
+#endif
+
+SDLTextureYUYV::SDLTextureYUYV(const SDL_Rect &rect, unsigned int stride)
+ : SDLTexture(rect, SDL_PIXELFORMAT_YUY2, stride)
+{
+}
+
+void SDLTextureYUYV::update(const std::vector<libcamera::Span<const uint8_t>> &data)
+{
+ SDL_UpdateTexture(ptr_, &rect_, data[0].data(), stride_);
+}
diff --git a/src/apps/cam/sdl_texture_yuv.h b/src/apps/cam/sdl_texture_yuv.h
new file mode 100644
index 00000000..310e4e50
--- /dev/null
+++ b/src/apps/cam/sdl_texture_yuv.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Ideas on Board Oy
+ *
+ * sdl_texture_yuv.h - SDL YUV Textures
+ */
+
+#pragma once
+
+#include "sdl_texture.h"
+
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+class SDLTextureNV12 : public SDLTexture
+{
+public:
+ SDLTextureNV12(const SDL_Rect &rect, unsigned int stride);
+ void update(const std::vector<libcamera::Span<const uint8_t>> &data) override;
+};
+#endif
+
+class SDLTextureYUYV : public SDLTexture
+{
+public:
+ SDLTextureYUYV(const SDL_Rect &rect, unsigned int stride);
+ void update(const std::vector<libcamera::Span<const uint8_t>> &data) override;
+};
diff --git a/src/apps/cam/stream_options.cpp b/src/apps/cam/stream_options.cpp
new file mode 100644
index 00000000..3a5625f5
--- /dev/null
+++ b/src/apps/cam/stream_options.cpp
@@ -0,0 +1,134 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * stream_options.cpp - Helper to parse options for streams
+ */
+#include "stream_options.h"
+
+#include <iostream>
+
+#include <libcamera/color_space.h>
+
+using namespace libcamera;
+
+StreamKeyValueParser::StreamKeyValueParser()
+{
+ addOption("role", OptionString,
+ "Role for the stream (viewfinder, video, still, raw)",
+ ArgumentRequired);
+ addOption("width", OptionInteger, "Width in pixels",
+ ArgumentRequired);
+ addOption("height", OptionInteger, "Height in pixels",
+ ArgumentRequired);
+ addOption("pixelformat", OptionString, "Pixel format name",
+ ArgumentRequired);
+ addOption("colorspace", OptionString, "Color space",
+ ArgumentRequired);
+}
+
+KeyValueParser::Options StreamKeyValueParser::parse(const char *arguments)
+{
+ KeyValueParser::Options options = KeyValueParser::parse(arguments);
+ StreamRole role;
+
+ if (options.valid() && options.isSet("role") &&
+ !parseRole(&role, options)) {
+ std::cerr << "Unknown stream role "
+ << options["role"].toString() << std::endl;
+ options.invalidate();
+ }
+
+ return options;
+}
+
+StreamRoles StreamKeyValueParser::roles(const OptionValue &values)
+{
+ /* If no configuration values to examine default to viewfinder. */
+ if (values.empty())
+ return { StreamRole::Viewfinder };
+
+ const std::vector<OptionValue> &streamParameters = values.toArray();
+
+ StreamRoles roles;
+ for (auto const &value : streamParameters) {
+ StreamRole role;
+
+ /* If role is invalid or not set default to viewfinder. */
+ if (!parseRole(&role, value.toKeyValues()))
+ role = StreamRole::Viewfinder;
+
+ roles.push_back(role);
+ }
+
+ return roles;
+}
+
+int StreamKeyValueParser::updateConfiguration(CameraConfiguration *config,
+ const OptionValue &values)
+{
+ if (!config) {
+ std::cerr << "No configuration provided" << std::endl;
+ return -EINVAL;
+ }
+
+ /* If no configuration values nothing to do. */
+ if (values.empty())
+ return 0;
+
+ const std::vector<OptionValue> &streamParameters = values.toArray();
+
+ if (config->size() != streamParameters.size()) {
+ std::cerr
+ << "Number of streams in configuration "
+ << config->size()
+ << " does not match number of streams parsed "
+ << streamParameters.size()
+ << std::endl;
+ return -EINVAL;
+ }
+
+ unsigned int i = 0;
+ for (auto const &value : streamParameters) {
+ KeyValueParser::Options opts = value.toKeyValues();
+ StreamConfiguration &cfg = config->at(i++);
+
+ if (opts.isSet("width") && opts.isSet("height")) {
+ cfg.size.width = opts["width"];
+ cfg.size.height = opts["height"];
+ }
+
+ if (opts.isSet("pixelformat"))
+ cfg.pixelFormat = PixelFormat::fromString(opts["pixelformat"].toString());
+
+ if (opts.isSet("colorspace"))
+ cfg.colorSpace = ColorSpace::fromString(opts["colorspace"].toString());
+ }
+
+ return 0;
+}
+
+bool StreamKeyValueParser::parseRole(StreamRole *role,
+ const KeyValueParser::Options &options)
+{
+ if (!options.isSet("role"))
+ return false;
+
+ std::string name = options["role"].toString();
+
+ if (name == "viewfinder") {
+ *role = StreamRole::Viewfinder;
+ return true;
+ } else if (name == "video") {
+ *role = StreamRole::VideoRecording;
+ return true;
+ } else if (name == "still") {
+ *role = StreamRole::StillCapture;
+ return true;
+ } else if (name == "raw") {
+ *role = StreamRole::Raw;
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/apps/cam/stream_options.h b/src/apps/cam/stream_options.h
new file mode 100644
index 00000000..35e4e7c0
--- /dev/null
+++ b/src/apps/cam/stream_options.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * stream_options.h - Helper to parse options for streams
+ */
+
+#pragma once
+
+#include <libcamera/camera.h>
+
+#include "options.h"
+
+class StreamKeyValueParser : public KeyValueParser
+{
+public:
+ StreamKeyValueParser();
+
+ KeyValueParser::Options parse(const char *arguments) override;
+
+ static libcamera::StreamRoles roles(const OptionValue &values);
+ static int updateConfiguration(libcamera::CameraConfiguration *config,
+ const OptionValue &values);
+
+private:
+ static bool parseRole(libcamera::StreamRole *role,
+ const KeyValueParser::Options &options);
+};
diff --git a/src/apps/lc-compliance/capture_test.cpp b/src/apps/lc-compliance/capture_test.cpp
new file mode 100644
index 00000000..52578207
--- /dev/null
+++ b/src/apps/lc-compliance/capture_test.cpp
@@ -0,0 +1,128 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ * Copyright (C) 2021, Collabora Ltd.
+ *
+ * capture_test.cpp - Test camera capture
+ */
+
+#include <iostream>
+
+#include <gtest/gtest.h>
+
+#include "environment.h"
+#include "simple_capture.h"
+
+using namespace libcamera;
+
+const std::vector<int> NUMREQUESTS = { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
+const std::vector<StreamRole> ROLES = { Raw, StillCapture, VideoRecording, Viewfinder };
+
+class SingleStream : public testing::TestWithParam<std::tuple<StreamRole, int>>
+{
+public:
+ static std::string nameParameters(const testing::TestParamInfo<SingleStream::ParamType> &info);
+
+protected:
+ void SetUp() override;
+ void TearDown() override;
+
+ std::shared_ptr<Camera> camera_;
+};
+
+/*
+ * We use gtest's SetUp() and TearDown() instead of constructor and destructor
+ * in order to be able to assert on them.
+ */
+void SingleStream::SetUp()
+{
+ Environment *env = Environment::get();
+
+ camera_ = env->cm()->get(env->cameraId());
+
+ ASSERT_EQ(camera_->acquire(), 0);
+}
+
+void SingleStream::TearDown()
+{
+ if (!camera_)
+ return;
+
+ camera_->release();
+ camera_.reset();
+}
+
+std::string SingleStream::nameParameters(const testing::TestParamInfo<SingleStream::ParamType> &info)
+{
+ std::map<StreamRole, std::string> rolesMap = { { Raw, "Raw" },
+ { StillCapture, "StillCapture" },
+ { VideoRecording, "VideoRecording" },
+ { Viewfinder, "Viewfinder" } };
+
+ std::string roleName = rolesMap[std::get<0>(info.param)];
+ std::string numRequestsName = std::to_string(std::get<1>(info.param));
+
+ return roleName + "_" + numRequestsName;
+}
+
+/*
+ * Test single capture cycles
+ *
+ * Makes sure the camera completes the exact number of requests queued. Example
+ * failure is a camera that completes less requests than the number of requests
+ * queued.
+ */
+TEST_P(SingleStream, Capture)
+{
+ auto [role, numRequests] = GetParam();
+
+ SimpleCaptureBalanced capture(camera_);
+
+ capture.configure(role);
+
+ capture.capture(numRequests);
+}
+
+/*
+ * Test multiple start/stop cycles
+ *
+ * Makes sure the camera supports multiple start/stop cycles. Example failure is
+ * a camera that does not clean up correctly in its error path but is only
+ * tested by single-capture applications.
+ */
+TEST_P(SingleStream, CaptureStartStop)
+{
+ auto [role, numRequests] = GetParam();
+ unsigned int numRepeats = 3;
+
+ SimpleCaptureBalanced capture(camera_);
+
+ capture.configure(role);
+
+ for (unsigned int starts = 0; starts < numRepeats; starts++)
+ capture.capture(numRequests);
+}
+
+/*
+ * Test unbalanced stop
+ *
+ * Makes sure the camera supports a stop with requests queued. Example failure
+ * is a camera that does not handle cancelation of buffers coming back from the
+ * video device while stopping.
+ */
+TEST_P(SingleStream, UnbalancedStop)
+{
+ auto [role, numRequests] = GetParam();
+
+ SimpleCaptureUnbalanced capture(camera_);
+
+ capture.configure(role);
+
+ capture.capture(numRequests);
+}
+
+INSTANTIATE_TEST_SUITE_P(CaptureTests,
+ SingleStream,
+ testing::Combine(testing::ValuesIn(ROLES),
+ testing::ValuesIn(NUMREQUESTS)),
+ SingleStream::nameParameters);
diff --git a/src/apps/lc-compliance/environment.cpp b/src/apps/lc-compliance/environment.cpp
new file mode 100644
index 00000000..5eb3775f
--- /dev/null
+++ b/src/apps/lc-compliance/environment.cpp
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Collabora Ltd.
+ *
+ * environment.cpp - Common environment for tests
+ */
+
+#include "environment.h"
+
+using namespace libcamera;
+
+Environment *Environment::get()
+{
+ static Environment instance;
+ return &instance;
+}
+
+void Environment::setup(CameraManager *cm, std::string cameraId)
+{
+ cm_ = cm;
+ cameraId_ = cameraId;
+}
diff --git a/src/apps/lc-compliance/environment.h b/src/apps/lc-compliance/environment.h
new file mode 100644
index 00000000..0debbcce
--- /dev/null
+++ b/src/apps/lc-compliance/environment.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2021, Collabora Ltd.
+ *
+ * environment.h - Common environment for tests
+ */
+
+#pragma once
+
+#include <libcamera/libcamera.h>
+
+class Environment
+{
+public:
+ static Environment *get();
+
+ void setup(libcamera::CameraManager *cm, std::string cameraId);
+
+ const std::string &cameraId() const { return cameraId_; }
+ libcamera::CameraManager *cm() const { return cm_; }
+
+private:
+ Environment() = default;
+
+ std::string cameraId_;
+ libcamera::CameraManager *cm_;
+};
diff --git a/src/apps/lc-compliance/main.cpp b/src/apps/lc-compliance/main.cpp
new file mode 100644
index 00000000..7eb52ae4
--- /dev/null
+++ b/src/apps/lc-compliance/main.cpp
@@ -0,0 +1,193 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ * Copyright (C) 2021, Collabora Ltd.
+ *
+ * main.cpp - lc-compliance - The libcamera compliance tool
+ */
+
+#include <iomanip>
+#include <iostream>
+#include <string.h>
+
+#include <gtest/gtest.h>
+
+#include <libcamera/libcamera.h>
+
+#include "environment.h"
+#include "../cam/options.h"
+
+using namespace libcamera;
+
+enum {
+ OptCamera = 'c',
+ OptList = 'l',
+ OptFilter = 'f',
+ OptHelp = 'h',
+};
+
+/*
+ * Make asserts act like exceptions, otherwise they only fail (or skip) the
+ * current function. From gtest documentation:
+ * https://google.github.io/googletest/advanced.html#asserting-on-subroutines-with-an-exception
+ */
+class ThrowListener : public testing::EmptyTestEventListener
+{
+ void OnTestPartResult(const testing::TestPartResult &result) override
+ {
+ if (result.type() == testing::TestPartResult::kFatalFailure ||
+ result.type() == testing::TestPartResult::kSkip)
+ throw testing::AssertionException(result);
+ }
+};
+
+static void listCameras(CameraManager *cm)
+{
+ for (const std::shared_ptr<Camera> &cam : cm->cameras())
+ std::cout << "- " << cam.get()->id() << std::endl;
+}
+
+static int initCamera(CameraManager *cm, OptionsParser::Options options)
+{
+ std::shared_ptr<Camera> camera;
+
+ int ret = cm->start();
+ if (ret) {
+ std::cout << "Failed to start camera manager: "
+ << strerror(-ret) << std::endl;
+ return ret;
+ }
+
+ if (!options.isSet(OptCamera)) {
+ std::cout << "No camera specified, available cameras:" << std::endl;
+ listCameras(cm);
+ return -ENODEV;
+ }
+
+ const std::string &cameraId = options[OptCamera];
+ camera = cm->get(cameraId);
+ if (!camera) {
+ std::cout << "Camera " << cameraId << " not found, available cameras:" << std::endl;
+ listCameras(cm);
+ return -ENODEV;
+ }
+
+ Environment::get()->setup(cm, cameraId);
+
+ std::cout << "Using camera " << cameraId << std::endl;
+
+ return 0;
+}
+
+static int initGtestParameters(char *arg0, OptionsParser::Options options)
+{
+ const std::map<std::string, std::string> gtestFlags = { { "list", "--gtest_list_tests" },
+ { "filter", "--gtest_filter" } };
+
+ int argc = 0;
+ std::string filterParam;
+
+ /*
+ * +2 to have space for both the 0th argument that is needed but not
+ * used and the null at the end.
+ */
+ char **argv = new char *[(gtestFlags.size() + 2)];
+ if (!argv)
+ return -ENOMEM;
+
+ argv[0] = arg0;
+ argc++;
+
+ if (options.isSet(OptList)) {
+ argv[argc] = const_cast<char *>(gtestFlags.at("list").c_str());
+ argc++;
+ }
+
+ if (options.isSet(OptFilter)) {
+ /*
+ * The filter flag needs to be passed as a single parameter, in
+ * the format --gtest_filter=filterStr
+ */
+ filterParam = gtestFlags.at("filter") + "=" +
+ static_cast<const std::string &>(options[OptFilter]);
+
+ argv[argc] = const_cast<char *>(filterParam.c_str());
+ argc++;
+ }
+
+ argv[argc] = nullptr;
+
+ ::testing::InitGoogleTest(&argc, argv);
+
+ delete[] argv;
+
+ return 0;
+}
+
+static int initGtest(char *arg0, OptionsParser::Options options)
+{
+ int ret = initGtestParameters(arg0, options);
+ if (ret)
+ return ret;
+
+ testing::UnitTest::GetInstance()->listeners().Append(new ThrowListener);
+
+ return 0;
+}
+
+static int parseOptions(int argc, char **argv, OptionsParser::Options *options)
+{
+ OptionsParser parser;
+ parser.addOption(OptCamera, OptionString,
+ "Specify which camera to operate on, by id", "camera",
+ ArgumentRequired, "camera");
+ parser.addOption(OptList, OptionNone, "List all tests and exit", "list");
+ parser.addOption(OptFilter, OptionString,
+ "Specify which tests to run", "filter",
+ ArgumentRequired, "filter");
+ parser.addOption(OptHelp, OptionNone, "Display this help message",
+ "help");
+
+ *options = parser.parse(argc, argv);
+ if (!options->valid())
+ return -EINVAL;
+
+ if (options->isSet(OptHelp)) {
+ parser.usage();
+ std::cerr << "Further options from Googletest can be passed as environment variables"
+ << std::endl;
+ return -EINTR;
+ }
+
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ OptionsParser::Options options;
+ int ret = parseOptions(argc, argv, &options);
+ if (ret == -EINTR)
+ return EXIT_SUCCESS;
+ if (ret < 0)
+ return EXIT_FAILURE;
+
+ std::unique_ptr<CameraManager> cm = std::make_unique<CameraManager>();
+
+ /* No need to initialize the camera if we'll just list tests */
+ if (!options.isSet(OptList)) {
+ ret = initCamera(cm.get(), options);
+ if (ret)
+ return ret;
+ }
+
+ ret = initGtest(argv[0], options);
+ if (ret)
+ return ret;
+
+ ret = RUN_ALL_TESTS();
+
+ if (!options.isSet(OptList))
+ cm->stop();
+
+ return ret;
+}
diff --git a/src/apps/lc-compliance/meson.build b/src/apps/lc-compliance/meson.build
new file mode 100644
index 00000000..8b57474b
--- /dev/null
+++ b/src/apps/lc-compliance/meson.build
@@ -0,0 +1,31 @@
+# SPDX-License-Identifier: CC0-1.0
+
+libevent = dependency('libevent_pthreads', required : get_option('lc-compliance'))
+libgtest = dependency('gtest', required : get_option('lc-compliance'),
+ fallback : ['gtest', 'gtest_dep'])
+
+if not (libevent.found() and libgtest.found())
+ lc_compliance_enabled = false
+ subdir_done()
+endif
+
+lc_compliance_enabled = true
+
+lc_compliance_sources = files([
+ '../cam/event_loop.cpp',
+ '../cam/options.cpp',
+ 'environment.cpp',
+ 'main.cpp',
+ 'simple_capture.cpp',
+ 'capture_test.cpp',
+])
+
+lc_compliance = executable('lc-compliance', lc_compliance_sources,
+ cpp_args : [ '-fexceptions' ],
+ dependencies : [
+ libatomic,
+ libcamera_public,
+ libevent,
+ libgtest,
+ ],
+ install : true)
diff --git a/src/apps/lc-compliance/simple_capture.cpp b/src/apps/lc-compliance/simple_capture.cpp
new file mode 100644
index 00000000..ab5cb35c
--- /dev/null
+++ b/src/apps/lc-compliance/simple_capture.cpp
@@ -0,0 +1,191 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020-2021, Google Inc.
+ *
+ * simple_capture.cpp - Simple capture helper
+ */
+
+#include <gtest/gtest.h>
+
+#include "simple_capture.h"
+
+using namespace libcamera;
+
+SimpleCapture::SimpleCapture(std::shared_ptr<Camera> camera)
+ : loop_(nullptr), camera_(camera),
+ allocator_(std::make_unique<FrameBufferAllocator>(camera))
+{
+}
+
+SimpleCapture::~SimpleCapture()
+{
+ stop();
+}
+
+void SimpleCapture::configure(StreamRole role)
+{
+ config_ = camera_->generateConfiguration({ role });
+
+ if (!config_) {
+ std::cout << "Role not supported by camera" << std::endl;
+ GTEST_SKIP();
+ }
+
+ if (config_->validate() != CameraConfiguration::Valid) {
+ config_.reset();
+ FAIL() << "Configuration not valid";
+ }
+
+ if (camera_->configure(config_.get())) {
+ config_.reset();
+ FAIL() << "Failed to configure camera";
+ }
+}
+
+void SimpleCapture::start()
+{
+ Stream *stream = config_->at(0).stream();
+ int count = allocator_->allocate(stream);
+
+ ASSERT_GE(count, 0) << "Failed to allocate buffers";
+ EXPECT_EQ(count, config_->at(0).bufferCount) << "Allocated less buffers than expected";
+
+ camera_->requestCompleted.connect(this, &SimpleCapture::requestComplete);
+
+ ASSERT_EQ(camera_->start(), 0) << "Failed to start camera";
+}
+
+void SimpleCapture::stop()
+{
+ if (!config_ || !allocator_->allocated())
+ return;
+
+ camera_->stop();
+
+ camera_->requestCompleted.disconnect(this);
+
+ Stream *stream = config_->at(0).stream();
+ allocator_->free(stream);
+}
+
+/* SimpleCaptureBalanced */
+
+SimpleCaptureBalanced::SimpleCaptureBalanced(std::shared_ptr<Camera> camera)
+ : SimpleCapture(camera)
+{
+}
+
+void SimpleCaptureBalanced::capture(unsigned int numRequests)
+{
+ start();
+
+ Stream *stream = config_->at(0).stream();
+ const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream);
+
+ /* No point in testing less requests then the camera depth. */
+ if (buffers.size() > numRequests) {
+ std::cout << "Camera needs " + std::to_string(buffers.size())
+ + " requests, can't test only "
+ + std::to_string(numRequests) << std::endl;
+ GTEST_SKIP();
+ }
+
+ queueCount_ = 0;
+ captureCount_ = 0;
+ captureLimit_ = numRequests;
+
+ /* Queue the recommended number of reqeuests. */
+ std::vector<std::unique_ptr<libcamera::Request>> requests;
+ for (const std::unique_ptr<FrameBuffer> &buffer : buffers) {
+ std::unique_ptr<Request> request = camera_->createRequest();
+ ASSERT_TRUE(request) << "Can't create request";
+
+ ASSERT_EQ(request->addBuffer(stream, buffer.get()), 0) << "Can't set buffer for request";
+
+ ASSERT_EQ(queueRequest(request.get()), 0) << "Failed to queue request";
+
+ requests.push_back(std::move(request));
+ }
+
+ /* Run capture session. */
+ loop_ = new EventLoop();
+ loop_->exec();
+ stop();
+ delete loop_;
+
+ ASSERT_EQ(captureCount_, captureLimit_);
+}
+
+int SimpleCaptureBalanced::queueRequest(Request *request)
+{
+ queueCount_++;
+ if (queueCount_ > captureLimit_)
+ return 0;
+
+ return camera_->queueRequest(request);
+}
+
+void SimpleCaptureBalanced::requestComplete(Request *request)
+{
+ captureCount_++;
+ if (captureCount_ >= captureLimit_) {
+ loop_->exit(0);
+ return;
+ }
+
+ request->reuse(Request::ReuseBuffers);
+ if (queueRequest(request))
+ loop_->exit(-EINVAL);
+}
+
+/* SimpleCaptureUnbalanced */
+
+SimpleCaptureUnbalanced::SimpleCaptureUnbalanced(std::shared_ptr<Camera> camera)
+ : SimpleCapture(camera)
+{
+}
+
+void SimpleCaptureUnbalanced::capture(unsigned int numRequests)
+{
+ start();
+
+ Stream *stream = config_->at(0).stream();
+ const std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream);
+
+ captureCount_ = 0;
+ captureLimit_ = numRequests;
+
+ /* Queue the recommended number of reqeuests. */
+ std::vector<std::unique_ptr<libcamera::Request>> requests;
+ for (const std::unique_ptr<FrameBuffer> &buffer : buffers) {
+ std::unique_ptr<Request> request = camera_->createRequest();
+ ASSERT_TRUE(request) << "Can't create request";
+
+ ASSERT_EQ(request->addBuffer(stream, buffer.get()), 0) << "Can't set buffer for request";
+
+ ASSERT_EQ(camera_->queueRequest(request.get()), 0) << "Failed to queue request";
+
+ requests.push_back(std::move(request));
+ }
+
+ /* Run capture session. */
+ loop_ = new EventLoop();
+ int status = loop_->exec();
+ stop();
+ delete loop_;
+
+ ASSERT_EQ(status, 0);
+}
+
+void SimpleCaptureUnbalanced::requestComplete(Request *request)
+{
+ captureCount_++;
+ if (captureCount_ >= captureLimit_) {
+ loop_->exit(0);
+ return;
+ }
+
+ request->reuse(Request::ReuseBuffers);
+ if (camera_->queueRequest(request))
+ loop_->exit(-EINVAL);
+}
diff --git a/src/apps/lc-compliance/simple_capture.h b/src/apps/lc-compliance/simple_capture.h
new file mode 100644
index 00000000..9d31f7cb
--- /dev/null
+++ b/src/apps/lc-compliance/simple_capture.h
@@ -0,0 +1,65 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020-2021, Google Inc.
+ *
+ * simple_capture.h - Simple capture helper
+ */
+
+#pragma once
+
+#include <memory>
+
+#include <libcamera/libcamera.h>
+
+#include "../cam/event_loop.h"
+
+class SimpleCapture
+{
+public:
+ void configure(libcamera::StreamRole role);
+
+protected:
+ SimpleCapture(std::shared_ptr<libcamera::Camera> camera);
+ virtual ~SimpleCapture();
+
+ void start();
+ void stop();
+
+ virtual void requestComplete(libcamera::Request *request) = 0;
+
+ EventLoop *loop_;
+
+ std::shared_ptr<libcamera::Camera> camera_;
+ std::unique_ptr<libcamera::FrameBufferAllocator> allocator_;
+ std::unique_ptr<libcamera::CameraConfiguration> config_;
+};
+
+class SimpleCaptureBalanced : public SimpleCapture
+{
+public:
+ SimpleCaptureBalanced(std::shared_ptr<libcamera::Camera> camera);
+
+ void capture(unsigned int numRequests);
+
+private:
+ int queueRequest(libcamera::Request *request);
+ void requestComplete(libcamera::Request *request) override;
+
+ unsigned int queueCount_;
+ unsigned int captureCount_;
+ unsigned int captureLimit_;
+};
+
+class SimpleCaptureUnbalanced : public SimpleCapture
+{
+public:
+ SimpleCaptureUnbalanced(std::shared_ptr<libcamera::Camera> camera);
+
+ void capture(unsigned int numRequests);
+
+private:
+ void requestComplete(libcamera::Request *request) override;
+
+ unsigned int captureCount_;
+ unsigned int captureLimit_;
+};
diff --git a/src/apps/meson.build b/src/apps/meson.build
new file mode 100644
index 00000000..9e4388bd
--- /dev/null
+++ b/src/apps/meson.build
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: CC0-1.0
+
+subdir('lc-compliance')
+
+subdir('cam')
+subdir('qcam')
diff --git a/src/apps/qcam/assets/feathericons/activity.svg b/src/apps/qcam/assets/feathericons/activity.svg
new file mode 100644
index 00000000..669a57a7
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/activity.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-activity"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/airplay.svg b/src/apps/qcam/assets/feathericons/airplay.svg
new file mode 100644
index 00000000..7ce73022
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/airplay.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-airplay"><path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1"></path><polygon points="12 15 17 21 7 21 12 15"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/alert-circle.svg b/src/apps/qcam/assets/feathericons/alert-circle.svg
new file mode 100644
index 00000000..8d02b7d1
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/alert-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/alert-octagon.svg b/src/apps/qcam/assets/feathericons/alert-octagon.svg
new file mode 100644
index 00000000..de9b03f2
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/alert-octagon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-octagon"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/alert-triangle.svg b/src/apps/qcam/assets/feathericons/alert-triangle.svg
new file mode 100644
index 00000000..6dcb0963
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/alert-triangle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/align-center.svg b/src/apps/qcam/assets/feathericons/align-center.svg
new file mode 100644
index 00000000..5b8842ea
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/align-center.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-center"><line x1="18" y1="10" x2="6" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="18" y1="18" x2="6" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/align-justify.svg b/src/apps/qcam/assets/feathericons/align-justify.svg
new file mode 100644
index 00000000..0539876f
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/align-justify.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-justify"><line x1="21" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="3" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/align-left.svg b/src/apps/qcam/assets/feathericons/align-left.svg
new file mode 100644
index 00000000..9ac852a5
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/align-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/align-right.svg b/src/apps/qcam/assets/feathericons/align-right.svg
new file mode 100644
index 00000000..ef139ffa
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/align-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-right"><line x1="21" y1="10" x2="7" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="21" y1="18" x2="7" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/anchor.svg b/src/apps/qcam/assets/feathericons/anchor.svg
new file mode 100644
index 00000000..e01627a3
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/anchor.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-anchor"><circle cx="12" cy="5" r="3"></circle><line x1="12" y1="22" x2="12" y2="8"></line><path d="M5 12H2a10 10 0 0 0 20 0h-3"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/aperture.svg b/src/apps/qcam/assets/feathericons/aperture.svg
new file mode 100644
index 00000000..9936e868
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/aperture.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-aperture"><circle cx="12" cy="12" r="10"></circle><line x1="14.31" y1="8" x2="20.05" y2="17.94"></line><line x1="9.69" y1="8" x2="21.17" y2="8"></line><line x1="7.38" y1="12" x2="13.12" y2="2.06"></line><line x1="9.69" y1="16" x2="3.95" y2="6.06"></line><line x1="14.31" y1="16" x2="2.83" y2="16"></line><line x1="16.62" y1="12" x2="10.88" y2="21.94"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/archive.svg b/src/apps/qcam/assets/feathericons/archive.svg
new file mode 100644
index 00000000..428882c8
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/archive.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-archive"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-down-circle.svg b/src/apps/qcam/assets/feathericons/arrow-down-circle.svg
new file mode 100644
index 00000000..3238091b
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-down-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="8 12 12 16 16 12"></polyline><line x1="12" y1="8" x2="12" y2="16"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-down-left.svg b/src/apps/qcam/assets/feathericons/arrow-down-left.svg
new file mode 100644
index 00000000..72483584
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-down-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-left"><line x1="17" y1="7" x2="7" y2="17"></line><polyline points="17 17 7 17 7 7"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-down-right.svg b/src/apps/qcam/assets/feathericons/arrow-down-right.svg
new file mode 100644
index 00000000..81d9822b
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-down-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down-right"><line x1="7" y1="7" x2="17" y2="17"></line><polyline points="17 7 17 17 7 17"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-down.svg b/src/apps/qcam/assets/feathericons/arrow-down.svg
new file mode 100644
index 00000000..4f84f627
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-down"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-left-circle.svg b/src/apps/qcam/assets/feathericons/arrow-left-circle.svg
new file mode 100644
index 00000000..3b19ff8a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-left-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="12 8 8 12 12 16"></polyline><line x1="16" y1="12" x2="8" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-left.svg b/src/apps/qcam/assets/feathericons/arrow-left.svg
new file mode 100644
index 00000000..a5058fc7
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-right-circle.svg b/src/apps/qcam/assets/feathericons/arrow-right-circle.svg
new file mode 100644
index 00000000..ff01dd58
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-right-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="12 16 16 12 12 8"></polyline><line x1="8" y1="12" x2="16" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-right.svg b/src/apps/qcam/assets/feathericons/arrow-right.svg
new file mode 100644
index 00000000..939b57c5
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-up-circle.svg b/src/apps/qcam/assets/feathericons/arrow-up-circle.svg
new file mode 100644
index 00000000..044a75d3
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-up-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-circle"><circle cx="12" cy="12" r="10"></circle><polyline points="16 12 12 8 8 12"></polyline><line x1="12" y1="16" x2="12" y2="8"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-up-left.svg b/src/apps/qcam/assets/feathericons/arrow-up-left.svg
new file mode 100644
index 00000000..cea55e87
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-up-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-left"><line x1="17" y1="17" x2="7" y2="7"></line><polyline points="7 17 7 7 17 7"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-up-right.svg b/src/apps/qcam/assets/feathericons/arrow-up-right.svg
new file mode 100644
index 00000000..95678e00
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-up-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up-right"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/arrow-up.svg b/src/apps/qcam/assets/feathericons/arrow-up.svg
new file mode 100644
index 00000000..16b13aba
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/arrow-up.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-up"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/at-sign.svg b/src/apps/qcam/assets/feathericons/at-sign.svg
new file mode 100644
index 00000000..5a5e5d0d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/at-sign.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-at-sign"><circle cx="12" cy="12" r="4"></circle><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/award.svg b/src/apps/qcam/assets/feathericons/award.svg
new file mode 100644
index 00000000..be70d5a1
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/award.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-award"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/bar-chart-2.svg b/src/apps/qcam/assets/feathericons/bar-chart-2.svg
new file mode 100644
index 00000000..864167a6
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/bar-chart-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/bar-chart.svg b/src/apps/qcam/assets/feathericons/bar-chart.svg
new file mode 100644
index 00000000..074d7c1a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/bar-chart.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart"><line x1="12" y1="20" x2="12" y2="10"></line><line x1="18" y1="20" x2="18" y2="4"></line><line x1="6" y1="20" x2="6" y2="16"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/battery-charging.svg b/src/apps/qcam/assets/feathericons/battery-charging.svg
new file mode 100644
index 00000000..644cb59c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/battery-charging.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-battery-charging"><path d="M5 18H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3.19M15 6h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3.19"></path><line x1="23" y1="13" x2="23" y2="11"></line><polyline points="11 6 7 12 13 12 9 18"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/battery.svg b/src/apps/qcam/assets/feathericons/battery.svg
new file mode 100644
index 00000000..7fe87710
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/battery.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-battery"><rect x="1" y="6" width="18" height="12" rx="2" ry="2"></rect><line x1="23" y1="13" x2="23" y2="11"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/bell-off.svg b/src/apps/qcam/assets/feathericons/bell-off.svg
new file mode 100644
index 00000000..4b07c848
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/bell-off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bell-off"><path d="M13.73 21a2 2 0 0 1-3.46 0"></path><path d="M18.63 13A17.89 17.89 0 0 1 18 8"></path><path d="M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14"></path><path d="M18 8a6 6 0 0 0-9.33-5"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/bell.svg b/src/apps/qcam/assets/feathericons/bell.svg
new file mode 100644
index 00000000..bba561c1
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/bell.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bell"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/bluetooth.svg b/src/apps/qcam/assets/feathericons/bluetooth.svg
new file mode 100644
index 00000000..cebed7b1
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/bluetooth.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bluetooth"><polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/bold.svg b/src/apps/qcam/assets/feathericons/bold.svg
new file mode 100644
index 00000000..d1a4efd3
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/bold.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bold"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/book-open.svg b/src/apps/qcam/assets/feathericons/book-open.svg
new file mode 100644
index 00000000..5e0ca0ab
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/book-open.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book-open"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/book.svg b/src/apps/qcam/assets/feathericons/book.svg
new file mode 100644
index 00000000..12ffcbc4
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/book.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/bookmark.svg b/src/apps/qcam/assets/feathericons/bookmark.svg
new file mode 100644
index 00000000..2239cc58
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/bookmark.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bookmark"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/box.svg b/src/apps/qcam/assets/feathericons/box.svg
new file mode 100644
index 00000000..d89be30f
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/box.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-box"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/briefcase.svg b/src/apps/qcam/assets/feathericons/briefcase.svg
new file mode 100644
index 00000000..e3af0506
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/briefcase.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-briefcase"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/calendar.svg b/src/apps/qcam/assets/feathericons/calendar.svg
new file mode 100644
index 00000000..6c7fd870
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/calendar.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-calendar"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/camera-off.svg b/src/apps/qcam/assets/feathericons/camera-off.svg
new file mode 100644
index 00000000..daa3e25f
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/camera-off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M21 21H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3-3h6l2 3h4a2 2 0 0 1 2 2v9.34m-7.72-2.06a4 4 0 1 1-5.56-5.56"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/camera.svg b/src/apps/qcam/assets/feathericons/camera.svg
new file mode 100644
index 00000000..0e7f0603
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/camera.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/cast.svg b/src/apps/qcam/assets/feathericons/cast.svg
new file mode 100644
index 00000000..63c954d9
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/cast.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cast"><path d="M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path><line x1="2" y1="20" x2="2.01" y2="20"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/check-circle.svg b/src/apps/qcam/assets/feathericons/check-circle.svg
new file mode 100644
index 00000000..f2f4fd1a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/check-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-circle"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/check-square.svg b/src/apps/qcam/assets/feathericons/check-square.svg
new file mode 100644
index 00000000..72ab7a80
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/check-square.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-square"><polyline points="9 11 12 14 22 4"></polyline><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/check.svg b/src/apps/qcam/assets/feathericons/check.svg
new file mode 100644
index 00000000..1c209899
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/check.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/chevron-down.svg b/src/apps/qcam/assets/feathericons/chevron-down.svg
new file mode 100644
index 00000000..278c6a31
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/chevron-down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/chevron-left.svg b/src/apps/qcam/assets/feathericons/chevron-left.svg
new file mode 100644
index 00000000..747d46d9
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/chevron-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-left"><polyline points="15 18 9 12 15 6"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/chevron-right.svg b/src/apps/qcam/assets/feathericons/chevron-right.svg
new file mode 100644
index 00000000..258de414
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/chevron-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/chevron-up.svg b/src/apps/qcam/assets/feathericons/chevron-up.svg
new file mode 100644
index 00000000..4eb5ecc3
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/chevron-up.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/chevrons-down.svg b/src/apps/qcam/assets/feathericons/chevrons-down.svg
new file mode 100644
index 00000000..e67ef2fb
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/chevrons-down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-down"><polyline points="7 13 12 18 17 13"></polyline><polyline points="7 6 12 11 17 6"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/chevrons-left.svg b/src/apps/qcam/assets/feathericons/chevrons-left.svg
new file mode 100644
index 00000000..c32e3983
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/chevrons-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-left"><polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/chevrons-right.svg b/src/apps/qcam/assets/feathericons/chevrons-right.svg
new file mode 100644
index 00000000..f5068145
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/chevrons-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-right"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/chevrons-up.svg b/src/apps/qcam/assets/feathericons/chevrons-up.svg
new file mode 100644
index 00000000..0eaf5183
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/chevrons-up.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevrons-up"><polyline points="17 11 12 6 7 11"></polyline><polyline points="17 18 12 13 7 18"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/chrome.svg b/src/apps/qcam/assets/feathericons/chrome.svg
new file mode 100644
index 00000000..9189815e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/chrome.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chrome"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="21.17" y1="8" x2="12" y2="8"></line><line x1="3.95" y1="6.06" x2="8.54" y2="14"></line><line x1="10.88" y1="21.94" x2="15.46" y2="14"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/circle.svg b/src/apps/qcam/assets/feathericons/circle.svg
new file mode 100644
index 00000000..b0090882
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-circle"><circle cx="12" cy="12" r="10"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/clipboard.svg b/src/apps/qcam/assets/feathericons/clipboard.svg
new file mode 100644
index 00000000..ccee454d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/clipboard.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clipboard"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/clock.svg b/src/apps/qcam/assets/feathericons/clock.svg
new file mode 100644
index 00000000..ea3f5e50
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/clock.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/cloud-drizzle.svg b/src/apps/qcam/assets/feathericons/cloud-drizzle.svg
new file mode 100644
index 00000000..13af6bb5
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/cloud-drizzle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-drizzle"><line x1="8" y1="19" x2="8" y2="21"></line><line x1="8" y1="13" x2="8" y2="15"></line><line x1="16" y1="19" x2="16" y2="21"></line><line x1="16" y1="13" x2="16" y2="15"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="12" y1="15" x2="12" y2="17"></line><path d="M20 16.58A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/cloud-lightning.svg b/src/apps/qcam/assets/feathericons/cloud-lightning.svg
new file mode 100644
index 00000000..32d154cc
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/cloud-lightning.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-lightning"><path d="M19 16.9A5 5 0 0 0 18 7h-1.26a8 8 0 1 0-11.62 9"></path><polyline points="13 11 9 17 15 17 11 23"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/cloud-off.svg b/src/apps/qcam/assets/feathericons/cloud-off.svg
new file mode 100644
index 00000000..1e1e7d60
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/cloud-off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-off"><path d="M22.61 16.95A5 5 0 0 0 18 10h-1.26a8 8 0 0 0-7.05-6M5 5a8 8 0 0 0 4 15h9a5 5 0 0 0 1.7-.3"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/cloud-rain.svg b/src/apps/qcam/assets/feathericons/cloud-rain.svg
new file mode 100644
index 00000000..3e0b85b0
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/cloud-rain.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-rain"><line x1="16" y1="13" x2="16" y2="21"></line><line x1="8" y1="13" x2="8" y2="21"></line><line x1="12" y1="15" x2="12" y2="23"></line><path d="M20 16.58A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/cloud-snow.svg b/src/apps/qcam/assets/feathericons/cloud-snow.svg
new file mode 100644
index 00000000..e4eb8207
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/cloud-snow.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud-snow"><path d="M20 17.58A5 5 0 0 0 18 8h-1.26A8 8 0 1 0 4 16.25"></path><line x1="8" y1="16" x2="8.01" y2="16"></line><line x1="8" y1="20" x2="8.01" y2="20"></line><line x1="12" y1="18" x2="12.01" y2="18"></line><line x1="12" y1="22" x2="12.01" y2="22"></line><line x1="16" y1="16" x2="16.01" y2="16"></line><line x1="16" y1="20" x2="16.01" y2="20"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/cloud.svg b/src/apps/qcam/assets/feathericons/cloud.svg
new file mode 100644
index 00000000..0ee0c632
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/cloud.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cloud"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/code.svg b/src/apps/qcam/assets/feathericons/code.svg
new file mode 100644
index 00000000..c4954b55
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/code.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-code"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/codepen.svg b/src/apps/qcam/assets/feathericons/codepen.svg
new file mode 100644
index 00000000..ab2a815a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/codepen.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-codepen"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"></polygon><line x1="12" y1="22" x2="12" y2="15.5"></line><polyline points="22 8.5 12 15.5 2 8.5"></polyline><polyline points="2 15.5 12 8.5 22 15.5"></polyline><line x1="12" y1="2" x2="12" y2="8.5"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/codesandbox.svg b/src/apps/qcam/assets/feathericons/codesandbox.svg
new file mode 100644
index 00000000..49848f52
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/codesandbox.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-codesandbox"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="7.5 4.21 12 6.81 16.5 4.21"></polyline><polyline points="7.5 19.79 7.5 14.6 3 12"></polyline><polyline points="21 12 16.5 14.6 16.5 19.79"></polyline><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/coffee.svg b/src/apps/qcam/assets/feathericons/coffee.svg
new file mode 100644
index 00000000..32905e52
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/coffee.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-coffee"><path d="M18 8h1a4 4 0 0 1 0 8h-1"></path><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"></path><line x1="6" y1="1" x2="6" y2="4"></line><line x1="10" y1="1" x2="10" y2="4"></line><line x1="14" y1="1" x2="14" y2="4"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/columns.svg b/src/apps/qcam/assets/feathericons/columns.svg
new file mode 100644
index 00000000..d264b557
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/columns.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-columns"><path d="M12 3h7a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-7m0-18H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7m0-18v18"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/command.svg b/src/apps/qcam/assets/feathericons/command.svg
new file mode 100644
index 00000000..93f554c3
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/command.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-command"><path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/compass.svg b/src/apps/qcam/assets/feathericons/compass.svg
new file mode 100644
index 00000000..32962608
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/compass.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-compass"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/copy.svg b/src/apps/qcam/assets/feathericons/copy.svg
new file mode 100644
index 00000000..4e0b09f1
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/copy.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/corner-down-left.svg b/src/apps/qcam/assets/feathericons/corner-down-left.svg
new file mode 100644
index 00000000..9fffb3e9
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/corner-down-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-down-left"><polyline points="9 10 4 15 9 20"></polyline><path d="M20 4v7a4 4 0 0 1-4 4H4"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/corner-down-right.svg b/src/apps/qcam/assets/feathericons/corner-down-right.svg
new file mode 100644
index 00000000..b27d408d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/corner-down-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-down-right"><polyline points="15 10 20 15 15 20"></polyline><path d="M4 4v7a4 4 0 0 0 4 4h12"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/corner-left-down.svg b/src/apps/qcam/assets/feathericons/corner-left-down.svg
new file mode 100644
index 00000000..24b8375c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/corner-left-down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-left-down"><polyline points="14 15 9 20 4 15"></polyline><path d="M20 4h-7a4 4 0 0 0-4 4v12"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/corner-left-up.svg b/src/apps/qcam/assets/feathericons/corner-left-up.svg
new file mode 100644
index 00000000..e54527cd
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/corner-left-up.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-left-up"><polyline points="14 9 9 4 4 9"></polyline><path d="M20 20h-7a4 4 0 0 1-4-4V4"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/corner-right-down.svg b/src/apps/qcam/assets/feathericons/corner-right-down.svg
new file mode 100644
index 00000000..a49e6d6c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/corner-right-down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-right-down"><polyline points="10 15 15 20 20 15"></polyline><path d="M4 4h7a4 4 0 0 1 4 4v12"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/corner-right-up.svg b/src/apps/qcam/assets/feathericons/corner-right-up.svg
new file mode 100644
index 00000000..a5c5dce5
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/corner-right-up.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-right-up"><polyline points="10 9 15 4 20 9"></polyline><path d="M4 20h7a4 4 0 0 0 4-4V4"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/corner-up-left.svg b/src/apps/qcam/assets/feathericons/corner-up-left.svg
new file mode 100644
index 00000000..0a1ffd61
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/corner-up-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-up-left"><polyline points="9 14 4 9 9 4"></polyline><path d="M20 20v-7a4 4 0 0 0-4-4H4"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/corner-up-right.svg b/src/apps/qcam/assets/feathericons/corner-up-right.svg
new file mode 100644
index 00000000..0b8f961b
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/corner-up-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-up-right"><polyline points="15 14 20 9 15 4"></polyline><path d="M4 20v-7a4 4 0 0 1 4-4h12"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/cpu.svg b/src/apps/qcam/assets/feathericons/cpu.svg
new file mode 100644
index 00000000..2ed16ef7
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/cpu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/credit-card.svg b/src/apps/qcam/assets/feathericons/credit-card.svg
new file mode 100644
index 00000000..1b7fd029
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/credit-card.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-credit-card"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect><line x1="1" y1="10" x2="23" y2="10"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/crop.svg b/src/apps/qcam/assets/feathericons/crop.svg
new file mode 100644
index 00000000..ffbfd045
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/crop.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crop"><path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"></path><path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/crosshair.svg b/src/apps/qcam/assets/feathericons/crosshair.svg
new file mode 100644
index 00000000..ba394015
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/crosshair.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crosshair"><circle cx="12" cy="12" r="10"></circle><line x1="22" y1="12" x2="18" y2="12"></line><line x1="6" y1="12" x2="2" y2="12"></line><line x1="12" y1="6" x2="12" y2="2"></line><line x1="12" y1="22" x2="12" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/database.svg b/src/apps/qcam/assets/feathericons/database.svg
new file mode 100644
index 00000000..c296fbcf
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/database.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-database"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/delete.svg b/src/apps/qcam/assets/feathericons/delete.svg
new file mode 100644
index 00000000..8c6074b9
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/delete.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-delete"><path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"></path><line x1="18" y1="9" x2="12" y2="15"></line><line x1="12" y1="9" x2="18" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/disc.svg b/src/apps/qcam/assets/feathericons/disc.svg
new file mode 100644
index 00000000..2595b444
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/disc.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-disc"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/dollar-sign.svg b/src/apps/qcam/assets/feathericons/dollar-sign.svg
new file mode 100644
index 00000000..1a124d26
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/dollar-sign.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-dollar-sign"><line x1="12" y1="1" x2="12" y2="23"></line><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/download-cloud.svg b/src/apps/qcam/assets/feathericons/download-cloud.svg
new file mode 100644
index 00000000..f3126fc3
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/download-cloud.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download-cloud"><polyline points="8 17 12 21 16 17"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/download.svg b/src/apps/qcam/assets/feathericons/download.svg
new file mode 100644
index 00000000..76767a92
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/download.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/droplet.svg b/src/apps/qcam/assets/feathericons/droplet.svg
new file mode 100644
index 00000000..ca093014
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/droplet.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-droplet"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/edit-2.svg b/src/apps/qcam/assets/feathericons/edit-2.svg
new file mode 100644
index 00000000..06830c9d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/edit-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit-2"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/edit-3.svg b/src/apps/qcam/assets/feathericons/edit-3.svg
new file mode 100644
index 00000000..d728efcc
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/edit-3.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit-3"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/edit.svg b/src/apps/qcam/assets/feathericons/edit.svg
new file mode 100644
index 00000000..ec7b4ca2
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/edit.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/external-link.svg b/src/apps/qcam/assets/feathericons/external-link.svg
new file mode 100644
index 00000000..6236df3e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/external-link.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/eye-off.svg b/src/apps/qcam/assets/feathericons/eye-off.svg
new file mode 100644
index 00000000..77c54cb4
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/eye-off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye-off"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/eye.svg b/src/apps/qcam/assets/feathericons/eye.svg
new file mode 100644
index 00000000..9cde2437
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/eye.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/facebook.svg b/src/apps/qcam/assets/feathericons/facebook.svg
new file mode 100644
index 00000000..2570f56a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/facebook.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-facebook"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/fast-forward.svg b/src/apps/qcam/assets/feathericons/fast-forward.svg
new file mode 100644
index 00000000..fa39877a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/fast-forward.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-fast-forward"><polygon points="13 19 22 12 13 5 13 19"></polygon><polygon points="2 19 11 12 2 5 2 19"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/feather.svg b/src/apps/qcam/assets/feathericons/feather.svg
new file mode 100644
index 00000000..ac3b868d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/feather.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-feather"><path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"></path><line x1="16" y1="8" x2="2" y2="22"></line><line x1="17.5" y1="15" x2="9" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/feathericons.qrc b/src/apps/qcam/assets/feathericons/feathericons.qrc
new file mode 100644
index 00000000..c5302040
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/feathericons.qrc
@@ -0,0 +1,11 @@
+<!-- SPDX-License-Identifier: GPL-2.0-or-later -->
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file>aperture.svg</file>
+ <file>camera-off.svg</file>
+ <file>play-circle.svg</file>
+ <file>save.svg</file>
+ <file>stop-circle.svg</file>
+ <file>x-circle.svg</file>
+</qresource>
+</RCC>
diff --git a/src/apps/qcam/assets/feathericons/figma.svg b/src/apps/qcam/assets/feathericons/figma.svg
new file mode 100644
index 00000000..66fd2178
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/figma.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-figma"><path d="M5 5.5A3.5 3.5 0 0 1 8.5 2H12v7H8.5A3.5 3.5 0 0 1 5 5.5z"></path><path d="M12 2h3.5a3.5 3.5 0 1 1 0 7H12V2z"></path><path d="M12 12.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 1 1-7 0z"></path><path d="M5 19.5A3.5 3.5 0 0 1 8.5 16H12v3.5a3.5 3.5 0 1 1-7 0z"></path><path d="M5 12.5A3.5 3.5 0 0 1 8.5 9H12v7H8.5A3.5 3.5 0 0 1 5 12.5z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/file-minus.svg b/src/apps/qcam/assets/feathericons/file-minus.svg
new file mode 100644
index 00000000..345756ef
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/file-minus.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-minus"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="9" y1="15" x2="15" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/file-plus.svg b/src/apps/qcam/assets/feathericons/file-plus.svg
new file mode 100644
index 00000000..eed12004
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/file-plus.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-plus"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="12" y1="18" x2="12" y2="12"></line><line x1="9" y1="15" x2="15" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/file-text.svg b/src/apps/qcam/assets/feathericons/file-text.svg
new file mode 100644
index 00000000..4197ddd4
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/file-text.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/file.svg b/src/apps/qcam/assets/feathericons/file.svg
new file mode 100644
index 00000000..378519ab
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/file.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/film.svg b/src/apps/qcam/assets/feathericons/film.svg
new file mode 100644
index 00000000..ac46360d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/film.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-film"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/filter.svg b/src/apps/qcam/assets/feathericons/filter.svg
new file mode 100644
index 00000000..38a47e04
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/filter.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-filter"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/flag.svg b/src/apps/qcam/assets/feathericons/flag.svg
new file mode 100644
index 00000000..037737cb
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/flag.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-flag"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/folder-minus.svg b/src/apps/qcam/assets/feathericons/folder-minus.svg
new file mode 100644
index 00000000..d5b7af65
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/folder-minus.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder-minus"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="9" y1="14" x2="15" y2="14"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/folder-plus.svg b/src/apps/qcam/assets/feathericons/folder-plus.svg
new file mode 100644
index 00000000..898f2fc9
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/folder-plus.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder-plus"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path><line x1="12" y1="11" x2="12" y2="17"></line><line x1="9" y1="14" x2="15" y2="14"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/folder.svg b/src/apps/qcam/assets/feathericons/folder.svg
new file mode 100644
index 00000000..134458b9
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/folder.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/framer.svg b/src/apps/qcam/assets/feathericons/framer.svg
new file mode 100644
index 00000000..3e663478
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/framer.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-framer"><path d="M5 16V9h14V2H5l14 14h-7m-7 0l7 7v-7m-7 0h7"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/frown.svg b/src/apps/qcam/assets/feathericons/frown.svg
new file mode 100644
index 00000000..f3122547
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/frown.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-frown"><circle cx="12" cy="12" r="10"></circle><path d="M16 16s-1.5-2-4-2-4 2-4 2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/gift.svg b/src/apps/qcam/assets/feathericons/gift.svg
new file mode 100644
index 00000000..d2c14bd6
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/gift.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-gift"><polyline points="20 12 20 22 4 22 4 12"></polyline><rect x="2" y="7" width="20" height="5"></rect><line x1="12" y1="22" x2="12" y2="7"></line><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"></path><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/git-branch.svg b/src/apps/qcam/assets/feathericons/git-branch.svg
new file mode 100644
index 00000000..44003726
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/git-branch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-branch"><line x1="6" y1="3" x2="6" y2="15"></line><circle cx="18" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><path d="M18 9a9 9 0 0 1-9 9"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/git-commit.svg b/src/apps/qcam/assets/feathericons/git-commit.svg
new file mode 100644
index 00000000..e959d725
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/git-commit.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-commit"><circle cx="12" cy="12" r="4"></circle><line x1="1.05" y1="12" x2="7" y2="12"></line><line x1="17.01" y1="12" x2="22.96" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/git-merge.svg b/src/apps/qcam/assets/feathericons/git-merge.svg
new file mode 100644
index 00000000..c65fffdd
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/git-merge.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-merge"><circle cx="18" cy="18" r="3"></circle><circle cx="6" cy="6" r="3"></circle><path d="M6 21V9a9 9 0 0 0 9 9"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/git-pull-request.svg b/src/apps/qcam/assets/feathericons/git-pull-request.svg
new file mode 100644
index 00000000..fc80bdfd
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/git-pull-request.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-git-pull-request"><circle cx="18" cy="18" r="3"></circle><circle cx="6" cy="6" r="3"></circle><path d="M13 6h3a2 2 0 0 1 2 2v7"></path><line x1="6" y1="9" x2="6" y2="21"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/github.svg b/src/apps/qcam/assets/feathericons/github.svg
new file mode 100644
index 00000000..ff0af481
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/github.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/gitlab.svg b/src/apps/qcam/assets/feathericons/gitlab.svg
new file mode 100644
index 00000000..85d54a1e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/gitlab.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-gitlab"><path d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/globe.svg b/src/apps/qcam/assets/feathericons/globe.svg
new file mode 100644
index 00000000..0a0586d3
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/globe.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-globe"><circle cx="12" cy="12" r="10"></circle><line x1="2" y1="12" x2="22" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/grid.svg b/src/apps/qcam/assets/feathericons/grid.svg
new file mode 100644
index 00000000..8ef2e9d8
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/grid.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-grid"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/hard-drive.svg b/src/apps/qcam/assets/feathericons/hard-drive.svg
new file mode 100644
index 00000000..8e90fa1b
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/hard-drive.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-hard-drive"><line x1="22" y1="12" x2="2" y2="12"></line><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path><line x1="6" y1="16" x2="6.01" y2="16"></line><line x1="10" y1="16" x2="10.01" y2="16"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/hash.svg b/src/apps/qcam/assets/feathericons/hash.svg
new file mode 100644
index 00000000..c9c8d41f
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/hash.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-hash"><line x1="4" y1="9" x2="20" y2="9"></line><line x1="4" y1="15" x2="20" y2="15"></line><line x1="10" y1="3" x2="8" y2="21"></line><line x1="16" y1="3" x2="14" y2="21"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/headphones.svg b/src/apps/qcam/assets/feathericons/headphones.svg
new file mode 100644
index 00000000..fd8915b4
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/headphones.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-headphones"><path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/heart.svg b/src/apps/qcam/assets/feathericons/heart.svg
new file mode 100644
index 00000000..a083b7e2
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/heart.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-heart"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/help-circle.svg b/src/apps/qcam/assets/feathericons/help-circle.svg
new file mode 100644
index 00000000..51fddd80
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/help-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/hexagon.svg b/src/apps/qcam/assets/feathericons/hexagon.svg
new file mode 100644
index 00000000..eae7f255
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/hexagon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-hexagon"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/home.svg b/src/apps/qcam/assets/feathericons/home.svg
new file mode 100644
index 00000000..7bb31b23
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/home.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/image.svg b/src/apps/qcam/assets/feathericons/image.svg
new file mode 100644
index 00000000..a7d84b98
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/image.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-image"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/inbox.svg b/src/apps/qcam/assets/feathericons/inbox.svg
new file mode 100644
index 00000000..03a13b4e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/inbox.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-inbox"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/info.svg b/src/apps/qcam/assets/feathericons/info.svg
new file mode 100644
index 00000000..a09fa5f1
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/info.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/instagram.svg b/src/apps/qcam/assets/feathericons/instagram.svg
new file mode 100644
index 00000000..9fdb8e35
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/instagram.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-instagram"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/italic.svg b/src/apps/qcam/assets/feathericons/italic.svg
new file mode 100644
index 00000000..a123d371
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/italic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-italic"><line x1="19" y1="4" x2="10" y2="4"></line><line x1="14" y1="20" x2="5" y2="20"></line><line x1="15" y1="4" x2="9" y2="20"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/key.svg b/src/apps/qcam/assets/feathericons/key.svg
new file mode 100644
index 00000000..e778e74e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/key.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-key"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/layers.svg b/src/apps/qcam/assets/feathericons/layers.svg
new file mode 100644
index 00000000..ea788c22
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/layers.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/layout.svg b/src/apps/qcam/assets/feathericons/layout.svg
new file mode 100644
index 00000000..28743d92
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/layout.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layout"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="3" y1="9" x2="21" y2="9"></line><line x1="9" y1="21" x2="9" y2="9"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/life-buoy.svg b/src/apps/qcam/assets/feathericons/life-buoy.svg
new file mode 100644
index 00000000..54c2bd7d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/life-buoy.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-life-buoy"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="4.93" y1="4.93" x2="9.17" y2="9.17"></line><line x1="14.83" y1="14.83" x2="19.07" y2="19.07"></line><line x1="14.83" y1="9.17" x2="19.07" y2="4.93"></line><line x1="14.83" y1="9.17" x2="18.36" y2="5.64"></line><line x1="4.93" y1="19.07" x2="9.17" y2="14.83"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/link-2.svg b/src/apps/qcam/assets/feathericons/link-2.svg
new file mode 100644
index 00000000..8cc7f6dd
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/link-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link-2"><path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path><line x1="8" y1="12" x2="16" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/link.svg b/src/apps/qcam/assets/feathericons/link.svg
new file mode 100644
index 00000000..c89dd41c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/link.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/linkedin.svg b/src/apps/qcam/assets/feathericons/linkedin.svg
new file mode 100644
index 00000000..39531094
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/linkedin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-linkedin"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/list.svg b/src/apps/qcam/assets/feathericons/list.svg
new file mode 100644
index 00000000..5ce38eaa
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/list.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-list"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/loader.svg b/src/apps/qcam/assets/feathericons/loader.svg
new file mode 100644
index 00000000..e1a70c12
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/loader.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-loader"><line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line><line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/lock.svg b/src/apps/qcam/assets/feathericons/lock.svg
new file mode 100644
index 00000000..de09d9db
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/lock.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-lock"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/log-in.svg b/src/apps/qcam/assets/feathericons/log-in.svg
new file mode 100644
index 00000000..ba0da59a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/log-in.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-in"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/log-out.svg b/src/apps/qcam/assets/feathericons/log-out.svg
new file mode 100644
index 00000000..c9002c90
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/log-out.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-log-out"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/mail.svg b/src/apps/qcam/assets/feathericons/mail.svg
new file mode 100644
index 00000000..2af169e8
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/mail.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mail"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/map-pin.svg b/src/apps/qcam/assets/feathericons/map-pin.svg
new file mode 100644
index 00000000..d5548e92
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/map-pin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-map-pin"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/map.svg b/src/apps/qcam/assets/feathericons/map.svg
new file mode 100644
index 00000000..ecebd7bf
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/map.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-map"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon><line x1="8" y1="2" x2="8" y2="18"></line><line x1="16" y1="6" x2="16" y2="22"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/maximize-2.svg b/src/apps/qcam/assets/feathericons/maximize-2.svg
new file mode 100644
index 00000000..e41fc0b7
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/maximize-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" y1="3" x2="14" y2="10"></line><line x1="3" y1="21" x2="10" y2="14"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/maximize.svg b/src/apps/qcam/assets/feathericons/maximize.svg
new file mode 100644
index 00000000..fc305189
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/maximize.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-maximize"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/meh.svg b/src/apps/qcam/assets/feathericons/meh.svg
new file mode 100644
index 00000000..6f57fff2
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/meh.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-meh"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="15" x2="16" y2="15"></line><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/menu.svg b/src/apps/qcam/assets/feathericons/menu.svg
new file mode 100644
index 00000000..e8a84a95
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/menu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/message-circle.svg b/src/apps/qcam/assets/feathericons/message-circle.svg
new file mode 100644
index 00000000..4b21b32b
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/message-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/message-square.svg b/src/apps/qcam/assets/feathericons/message-square.svg
new file mode 100644
index 00000000..6a2e4e59
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/message-square.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-square"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/mic-off.svg b/src/apps/qcam/assets/feathericons/mic-off.svg
new file mode 100644
index 00000000..0786219c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/mic-off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mic-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path><path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/mic.svg b/src/apps/qcam/assets/feathericons/mic.svg
new file mode 100644
index 00000000..dc5f780c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/mic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mic"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/minimize-2.svg b/src/apps/qcam/assets/feathericons/minimize-2.svg
new file mode 100644
index 00000000..a720fa6c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/minimize-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minimize-2"><polyline points="4 14 10 14 10 20"></polyline><polyline points="20 10 14 10 14 4"></polyline><line x1="14" y1="10" x2="21" y2="3"></line><line x1="3" y1="21" x2="10" y2="14"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/minimize.svg b/src/apps/qcam/assets/feathericons/minimize.svg
new file mode 100644
index 00000000..46d61196
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/minimize.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minimize"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/minus-circle.svg b/src/apps/qcam/assets/feathericons/minus-circle.svg
new file mode 100644
index 00000000..80c0de1e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/minus-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="8" y1="12" x2="16" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/minus-square.svg b/src/apps/qcam/assets/feathericons/minus-square.svg
new file mode 100644
index 00000000..4862832a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/minus-square.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="8" y1="12" x2="16" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/minus.svg b/src/apps/qcam/assets/feathericons/minus.svg
new file mode 100644
index 00000000..93cc7340
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/minus.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minus"><line x1="5" y1="12" x2="19" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/monitor.svg b/src/apps/qcam/assets/feathericons/monitor.svg
new file mode 100644
index 00000000..6c3556db
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/monitor.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-monitor"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/moon.svg b/src/apps/qcam/assets/feathericons/moon.svg
new file mode 100644
index 00000000..dbf7c6cf
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/moon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-moon"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/more-horizontal.svg b/src/apps/qcam/assets/feathericons/more-horizontal.svg
new file mode 100644
index 00000000..dc6a8556
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/more-horizontal.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-horizontal"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/more-vertical.svg b/src/apps/qcam/assets/feathericons/more-vertical.svg
new file mode 100644
index 00000000..cba6958f
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/more-vertical.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-more-vertical"><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="5" r="1"></circle><circle cx="12" cy="19" r="1"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/mouse-pointer.svg b/src/apps/qcam/assets/feathericons/mouse-pointer.svg
new file mode 100644
index 00000000..f5af5591
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/mouse-pointer.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-mouse-pointer"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"></path><path d="M13 13l6 6"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/move.svg b/src/apps/qcam/assets/feathericons/move.svg
new file mode 100644
index 00000000..4e251b56
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/move.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-move"><polyline points="5 9 2 12 5 15"></polyline><polyline points="9 5 12 2 15 5"></polyline><polyline points="15 19 12 22 9 19"></polyline><polyline points="19 9 22 12 19 15"></polyline><line x1="2" y1="12" x2="22" y2="12"></line><line x1="12" y1="2" x2="12" y2="22"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/music.svg b/src/apps/qcam/assets/feathericons/music.svg
new file mode 100644
index 00000000..7bee2f7e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/music.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-music"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/navigation-2.svg b/src/apps/qcam/assets/feathericons/navigation-2.svg
new file mode 100644
index 00000000..ae31db96
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/navigation-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-navigation-2"><polygon points="12 2 19 21 12 17 5 21 12 2"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/navigation.svg b/src/apps/qcam/assets/feathericons/navigation.svg
new file mode 100644
index 00000000..f600a414
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/navigation.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-navigation"><polygon points="3 11 22 2 13 21 11 13 3 11"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/octagon.svg b/src/apps/qcam/assets/feathericons/octagon.svg
new file mode 100644
index 00000000..124c5483
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/octagon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-octagon"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/package.svg b/src/apps/qcam/assets/feathericons/package.svg
new file mode 100644
index 00000000..f1e09eec
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/package.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-package"><line x1="16.5" y1="9.4" x2="7.5" y2="4.21"></line><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/paperclip.svg b/src/apps/qcam/assets/feathericons/paperclip.svg
new file mode 100644
index 00000000..b1f69b7a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/paperclip.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-paperclip"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/pause-circle.svg b/src/apps/qcam/assets/feathericons/pause-circle.svg
new file mode 100644
index 00000000..f6b1a8df
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/pause-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pause-circle"><circle cx="12" cy="12" r="10"></circle><line x1="10" y1="15" x2="10" y2="9"></line><line x1="14" y1="15" x2="14" y2="9"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/pause.svg b/src/apps/qcam/assets/feathericons/pause.svg
new file mode 100644
index 00000000..4e78038d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/pause.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pause"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/pen-tool.svg b/src/apps/qcam/assets/feathericons/pen-tool.svg
new file mode 100644
index 00000000..0d26fa1e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/pen-tool.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pen-tool"><path d="M12 19l7-7 3 3-7 7-3-3z"></path><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path><path d="M2 2l7.586 7.586"></path><circle cx="11" cy="11" r="2"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/percent.svg b/src/apps/qcam/assets/feathericons/percent.svg
new file mode 100644
index 00000000..2cb9719d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/percent.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-percent"><line x1="19" y1="5" x2="5" y2="19"></line><circle cx="6.5" cy="6.5" r="2.5"></circle><circle cx="17.5" cy="17.5" r="2.5"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/phone-call.svg b/src/apps/qcam/assets/feathericons/phone-call.svg
new file mode 100644
index 00000000..8b866602
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/phone-call.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-call"><path d="M15.05 5A5 5 0 0 1 19 8.95M15.05 1A9 9 0 0 1 23 8.94m-1 7.98v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/phone-forwarded.svg b/src/apps/qcam/assets/feathericons/phone-forwarded.svg
new file mode 100644
index 00000000..aa21befc
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/phone-forwarded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-forwarded"><polyline points="19 1 23 5 19 9"></polyline><line x1="15" y1="5" x2="23" y2="5"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/phone-incoming.svg b/src/apps/qcam/assets/feathericons/phone-incoming.svg
new file mode 100644
index 00000000..b2d523a8
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/phone-incoming.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-incoming"><polyline points="16 2 16 8 22 8"></polyline><line x1="23" y1="1" x2="16" y2="8"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/phone-missed.svg b/src/apps/qcam/assets/feathericons/phone-missed.svg
new file mode 100644
index 00000000..4950f09f
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/phone-missed.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-missed"><line x1="23" y1="1" x2="17" y2="7"></line><line x1="17" y1="1" x2="23" y2="7"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/phone-off.svg b/src/apps/qcam/assets/feathericons/phone-off.svg
new file mode 100644
index 00000000..4d00fb3d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/phone-off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-off"><path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"></path><line x1="23" y1="1" x2="1" y2="23"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/phone-outgoing.svg b/src/apps/qcam/assets/feathericons/phone-outgoing.svg
new file mode 100644
index 00000000..fea27a37
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/phone-outgoing.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone-outgoing"><polyline points="23 7 23 1 17 1"></polyline><line x1="16" y1="8" x2="23" y2="1"></line><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/phone.svg b/src/apps/qcam/assets/feathericons/phone.svg
new file mode 100644
index 00000000..2a35154a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/phone.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-phone"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/pie-chart.svg b/src/apps/qcam/assets/feathericons/pie-chart.svg
new file mode 100644
index 00000000..b5bbe67c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/pie-chart.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pie-chart"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"></path><path d="M22 12A10 10 0 0 0 12 2v10z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/play-circle.svg b/src/apps/qcam/assets/feathericons/play-circle.svg
new file mode 100644
index 00000000..8766dc7b
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/play-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-play-circle"><circle cx="12" cy="12" r="10"></circle><polygon points="10 8 16 12 10 16 10 8"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/play.svg b/src/apps/qcam/assets/feathericons/play.svg
new file mode 100644
index 00000000..fd76e30d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/play.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-play"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/plus-circle.svg b/src/apps/qcam/assets/feathericons/plus-circle.svg
new file mode 100644
index 00000000..4291ff05
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/plus-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/plus-square.svg b/src/apps/qcam/assets/feathericons/plus-square.svg
new file mode 100644
index 00000000..c380e24b
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/plus-square.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/plus.svg b/src/apps/qcam/assets/feathericons/plus.svg
new file mode 100644
index 00000000..703c5b7b
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/plus.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/pocket.svg b/src/apps/qcam/assets/feathericons/pocket.svg
new file mode 100644
index 00000000..a3b25619
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/pocket.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-pocket"><path d="M4 3h16a2 2 0 0 1 2 2v6a10 10 0 0 1-10 10A10 10 0 0 1 2 11V5a2 2 0 0 1 2-2z"></path><polyline points="8 10 12 14 16 10"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/power.svg b/src/apps/qcam/assets/feathericons/power.svg
new file mode 100644
index 00000000..598308fc
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/power.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-power"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/printer.svg b/src/apps/qcam/assets/feathericons/printer.svg
new file mode 100644
index 00000000..8a9a7ace
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/printer.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-printer"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/radio.svg b/src/apps/qcam/assets/feathericons/radio.svg
new file mode 100644
index 00000000..5abfcd13
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/radio.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-radio"><circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/refresh-ccw.svg b/src/apps/qcam/assets/feathericons/refresh-ccw.svg
new file mode 100644
index 00000000..10cff0ec
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/refresh-ccw.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-ccw"><polyline points="1 4 1 10 7 10"></polyline><polyline points="23 20 23 14 17 14"></polyline><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/refresh-cw.svg b/src/apps/qcam/assets/feathericons/refresh-cw.svg
new file mode 100644
index 00000000..06c358dd
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/refresh-cw.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/repeat.svg b/src/apps/qcam/assets/feathericons/repeat.svg
new file mode 100644
index 00000000..c7657b08
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/repeat.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-repeat"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/rewind.svg b/src/apps/qcam/assets/feathericons/rewind.svg
new file mode 100644
index 00000000..7b0fa3d5
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/rewind.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rewind"><polygon points="11 19 2 12 11 5 11 19"></polygon><polygon points="22 19 13 12 22 5 22 19"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/rotate-ccw.svg b/src/apps/qcam/assets/feathericons/rotate-ccw.svg
new file mode 100644
index 00000000..ade5dc42
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/rotate-ccw.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-ccw"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/rotate-cw.svg b/src/apps/qcam/assets/feathericons/rotate-cw.svg
new file mode 100644
index 00000000..83dca351
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/rotate-cw.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rotate-cw"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/rss.svg b/src/apps/qcam/assets/feathericons/rss.svg
new file mode 100644
index 00000000..c9a13684
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/rss.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/save.svg b/src/apps/qcam/assets/feathericons/save.svg
new file mode 100644
index 00000000..46c72990
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/save.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-save"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/scissors.svg b/src/apps/qcam/assets/feathericons/scissors.svg
new file mode 100644
index 00000000..fd0647ff
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/scissors.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-scissors"><circle cx="6" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><line x1="20" y1="4" x2="8.12" y2="15.88"></line><line x1="14.47" y1="14.48" x2="20" y2="20"></line><line x1="8.12" y1="8.12" x2="12" y2="12"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/search.svg b/src/apps/qcam/assets/feathericons/search.svg
new file mode 100644
index 00000000..8710306d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/search.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/send.svg b/src/apps/qcam/assets/feathericons/send.svg
new file mode 100644
index 00000000..42ef2a24
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/send.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-send"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/server.svg b/src/apps/qcam/assets/feathericons/server.svg
new file mode 100644
index 00000000..54ce094a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/server.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-server"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/settings.svg b/src/apps/qcam/assets/feathericons/settings.svg
new file mode 100644
index 00000000..19c27265
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/settings.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-settings"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/share-2.svg b/src/apps/qcam/assets/feathericons/share-2.svg
new file mode 100644
index 00000000..09b1c7bc
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/share-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-share-2"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/share.svg b/src/apps/qcam/assets/feathericons/share.svg
new file mode 100644
index 00000000..df38c14d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/share.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-share"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/shield-off.svg b/src/apps/qcam/assets/feathericons/shield-off.svg
new file mode 100644
index 00000000..18692ddd
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/shield-off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield-off"><path d="M19.69 14a6.9 6.9 0 0 0 .31-2V5l-8-3-3.16 1.18"></path><path d="M4.73 4.73L4 5v7c0 6 8 10 8 10a20.29 20.29 0 0 0 5.62-4.38"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/shield.svg b/src/apps/qcam/assets/feathericons/shield.svg
new file mode 100644
index 00000000..c7c48413
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/shield.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shield"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/shopping-bag.svg b/src/apps/qcam/assets/feathericons/shopping-bag.svg
new file mode 100644
index 00000000..eaa39e81
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/shopping-bag.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-bag"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/shopping-cart.svg b/src/apps/qcam/assets/feathericons/shopping-cart.svg
new file mode 100644
index 00000000..17a40bf4
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/shopping-cart.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-cart"><circle cx="9" cy="21" r="1"></circle><circle cx="20" cy="21" r="1"></circle><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/shuffle.svg b/src/apps/qcam/assets/feathericons/shuffle.svg
new file mode 100644
index 00000000..8cfb5db5
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/shuffle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shuffle"><polyline points="16 3 21 3 21 8"></polyline><line x1="4" y1="20" x2="21" y2="3"></line><polyline points="21 16 21 21 16 21"></polyline><line x1="15" y1="15" x2="21" y2="21"></line><line x1="4" y1="4" x2="9" y2="9"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/sidebar.svg b/src/apps/qcam/assets/feathericons/sidebar.svg
new file mode 100644
index 00000000..8ba817e6
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/sidebar.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sidebar"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="3" x2="9" y2="21"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/skip-back.svg b/src/apps/qcam/assets/feathericons/skip-back.svg
new file mode 100644
index 00000000..88d024e2
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/skip-back.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-skip-back"><polygon points="19 20 9 12 19 4 19 20"></polygon><line x1="5" y1="19" x2="5" y2="5"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/skip-forward.svg b/src/apps/qcam/assets/feathericons/skip-forward.svg
new file mode 100644
index 00000000..f3fdac3a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/skip-forward.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-skip-forward"><polygon points="5 4 15 12 5 20 5 4"></polygon><line x1="19" y1="5" x2="19" y2="19"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/slack.svg b/src/apps/qcam/assets/feathericons/slack.svg
new file mode 100644
index 00000000..5d973466
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/slack.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-slack"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"></path><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"></path><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"></path><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"></path><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z"></path><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"></path><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z"></path><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/slash.svg b/src/apps/qcam/assets/feathericons/slash.svg
new file mode 100644
index 00000000..f4131b85
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/slash.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-slash"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/sliders.svg b/src/apps/qcam/assets/feathericons/sliders.svg
new file mode 100644
index 00000000..19c93852
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/sliders.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sliders"><line x1="4" y1="21" x2="4" y2="14"></line><line x1="4" y1="10" x2="4" y2="3"></line><line x1="12" y1="21" x2="12" y2="12"></line><line x1="12" y1="8" x2="12" y2="3"></line><line x1="20" y1="21" x2="20" y2="16"></line><line x1="20" y1="12" x2="20" y2="3"></line><line x1="1" y1="14" x2="7" y2="14"></line><line x1="9" y1="8" x2="15" y2="8"></line><line x1="17" y1="16" x2="23" y2="16"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/smartphone.svg b/src/apps/qcam/assets/feathericons/smartphone.svg
new file mode 100644
index 00000000..0171a95a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/smartphone.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-smartphone"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect><line x1="12" y1="18" x2="12.01" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/smile.svg b/src/apps/qcam/assets/feathericons/smile.svg
new file mode 100644
index 00000000..24dc8a26
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/smile.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-smile"><circle cx="12" cy="12" r="10"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/speaker.svg b/src/apps/qcam/assets/feathericons/speaker.svg
new file mode 100644
index 00000000..75d5ff9c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/speaker.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-speaker"><rect x="4" y="2" width="16" height="20" rx="2" ry="2"></rect><circle cx="12" cy="14" r="4"></circle><line x1="12" y1="6" x2="12.01" y2="6"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/square.svg b/src/apps/qcam/assets/feathericons/square.svg
new file mode 100644
index 00000000..6eabc77d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/square.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/star.svg b/src/apps/qcam/assets/feathericons/star.svg
new file mode 100644
index 00000000..bcdc31aa
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/star.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/stop-circle.svg b/src/apps/qcam/assets/feathericons/stop-circle.svg
new file mode 100644
index 00000000..c10d9d47
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/stop-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-stop-circle"><circle cx="12" cy="12" r="10"></circle><rect x="9" y="9" width="6" height="6"></rect></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/sun.svg b/src/apps/qcam/assets/feathericons/sun.svg
new file mode 100644
index 00000000..7f51b94d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/sun.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sun"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/sunrise.svg b/src/apps/qcam/assets/feathericons/sunrise.svg
new file mode 100644
index 00000000..eff4b1e4
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/sunrise.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sunrise"><path d="M17 18a5 5 0 0 0-10 0"></path><line x1="12" y1="2" x2="12" y2="9"></line><line x1="4.22" y1="10.22" x2="5.64" y2="11.64"></line><line x1="1" y1="18" x2="3" y2="18"></line><line x1="21" y1="18" x2="23" y2="18"></line><line x1="18.36" y1="11.64" x2="19.78" y2="10.22"></line><line x1="23" y1="22" x2="1" y2="22"></line><polyline points="8 6 12 2 16 6"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/sunset.svg b/src/apps/qcam/assets/feathericons/sunset.svg
new file mode 100644
index 00000000..a5a22215
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/sunset.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-sunset"><path d="M17 18a5 5 0 0 0-10 0"></path><line x1="12" y1="9" x2="12" y2="2"></line><line x1="4.22" y1="10.22" x2="5.64" y2="11.64"></line><line x1="1" y1="18" x2="3" y2="18"></line><line x1="21" y1="18" x2="23" y2="18"></line><line x1="18.36" y1="11.64" x2="19.78" y2="10.22"></line><line x1="23" y1="22" x2="1" y2="22"></line><polyline points="16 5 12 9 8 5"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/tablet.svg b/src/apps/qcam/assets/feathericons/tablet.svg
new file mode 100644
index 00000000..9c80b40a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/tablet.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tablet"><rect x="4" y="2" width="16" height="20" rx="2" ry="2"></rect><line x1="12" y1="18" x2="12.01" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/tag.svg b/src/apps/qcam/assets/feathericons/tag.svg
new file mode 100644
index 00000000..7219b15f
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/tag.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tag"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/target.svg b/src/apps/qcam/assets/feathericons/target.svg
new file mode 100644
index 00000000..be84b17c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/target.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-target"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="6"></circle><circle cx="12" cy="12" r="2"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/terminal.svg b/src/apps/qcam/assets/feathericons/terminal.svg
new file mode 100644
index 00000000..af459c04
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/terminal.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-terminal"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/thermometer.svg b/src/apps/qcam/assets/feathericons/thermometer.svg
new file mode 100644
index 00000000..33142ccc
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/thermometer.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thermometer"><path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/thumbs-down.svg b/src/apps/qcam/assets/feathericons/thumbs-down.svg
new file mode 100644
index 00000000..3e7bcd6d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/thumbs-down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-down"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/thumbs-up.svg b/src/apps/qcam/assets/feathericons/thumbs-up.svg
new file mode 100644
index 00000000..226c44d8
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/thumbs-up.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-thumbs-up"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/toggle-left.svg b/src/apps/qcam/assets/feathericons/toggle-left.svg
new file mode 100644
index 00000000..240be290
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/toggle-left.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-toggle-left"><rect x="1" y="5" width="22" height="14" rx="7" ry="7"></rect><circle cx="8" cy="12" r="3"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/toggle-right.svg b/src/apps/qcam/assets/feathericons/toggle-right.svg
new file mode 100644
index 00000000..fc6e81c1
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/toggle-right.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-toggle-right"><rect x="1" y="5" width="22" height="14" rx="7" ry="7"></rect><circle cx="16" cy="12" r="3"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/tool.svg b/src/apps/qcam/assets/feathericons/tool.svg
new file mode 100644
index 00000000..f3cbf3d9
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/tool.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tool"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/trash-2.svg b/src/apps/qcam/assets/feathericons/trash-2.svg
new file mode 100644
index 00000000..f24d55bf
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/trash-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash-2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/trash.svg b/src/apps/qcam/assets/feathericons/trash.svg
new file mode 100644
index 00000000..55650bd4
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/trash.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trash"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/trello.svg b/src/apps/qcam/assets/feathericons/trello.svg
new file mode 100644
index 00000000..b2f599b6
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/trello.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trello"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><rect x="7" y="7" width="3" height="9"></rect><rect x="14" y="7" width="3" height="5"></rect></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/trending-down.svg b/src/apps/qcam/assets/feathericons/trending-down.svg
new file mode 100644
index 00000000..a9d4cfa5
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/trending-down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trending-down"><polyline points="23 18 13.5 8.5 8.5 13.5 1 6"></polyline><polyline points="17 18 23 18 23 12"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/trending-up.svg b/src/apps/qcam/assets/feathericons/trending-up.svg
new file mode 100644
index 00000000..52026a4d
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/trending-up.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trending-up"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline><polyline points="17 6 23 6 23 12"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/triangle.svg b/src/apps/qcam/assets/feathericons/triangle.svg
new file mode 100644
index 00000000..274b6528
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/triangle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/truck.svg b/src/apps/qcam/assets/feathericons/truck.svg
new file mode 100644
index 00000000..33898373
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/truck.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-truck"><rect x="1" y="3" width="15" height="13"></rect><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"></polygon><circle cx="5.5" cy="18.5" r="2.5"></circle><circle cx="18.5" cy="18.5" r="2.5"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/tv.svg b/src/apps/qcam/assets/feathericons/tv.svg
new file mode 100644
index 00000000..955bbfff
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/tv.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tv"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"></rect><polyline points="17 2 12 7 7 2"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/twitch.svg b/src/apps/qcam/assets/feathericons/twitch.svg
new file mode 100644
index 00000000..17062495
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/twitch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitch"><path d="M21 2H3v16h5v4l4-4h5l4-4V2zm-10 9V7m5 4V7"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/twitter.svg b/src/apps/qcam/assets/feathericons/twitter.svg
new file mode 100644
index 00000000..f8886eca
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/twitter.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitter"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/type.svg b/src/apps/qcam/assets/feathericons/type.svg
new file mode 100644
index 00000000..c6b2de33
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/type.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-type"><polyline points="4 7 4 4 20 4 20 7"></polyline><line x1="9" y1="20" x2="15" y2="20"></line><line x1="12" y1="4" x2="12" y2="20"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/umbrella.svg b/src/apps/qcam/assets/feathericons/umbrella.svg
new file mode 100644
index 00000000..dc77c0cb
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/umbrella.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-umbrella"><path d="M23 12a11.05 11.05 0 0 0-22 0zm-5 7a3 3 0 0 1-6 0v-7"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/underline.svg b/src/apps/qcam/assets/feathericons/underline.svg
new file mode 100644
index 00000000..044945d4
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/underline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-underline"><path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"></path><line x1="4" y1="21" x2="20" y2="21"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/unlock.svg b/src/apps/qcam/assets/feathericons/unlock.svg
new file mode 100644
index 00000000..01dc3597
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/unlock.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-unlock"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/upload-cloud.svg b/src/apps/qcam/assets/feathericons/upload-cloud.svg
new file mode 100644
index 00000000..a1db297c
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/upload-cloud.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-upload-cloud"><polyline points="16 16 12 12 8 16"></polyline><line x1="12" y1="12" x2="12" y2="21"></line><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"></path><polyline points="16 16 12 12 8 16"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/upload.svg b/src/apps/qcam/assets/feathericons/upload.svg
new file mode 100644
index 00000000..91eaff75
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/upload.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-upload"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/user-check.svg b/src/apps/qcam/assets/feathericons/user-check.svg
new file mode 100644
index 00000000..42f91b29
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/user-check.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user-check"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/user-minus.svg b/src/apps/qcam/assets/feathericons/user-minus.svg
new file mode 100644
index 00000000..44b75f5a
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/user-minus.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user-minus"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="23" y1="11" x2="17" y2="11"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/user-plus.svg b/src/apps/qcam/assets/feathericons/user-plus.svg
new file mode 100644
index 00000000..21460f6e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/user-plus.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user-plus"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/user-x.svg b/src/apps/qcam/assets/feathericons/user-x.svg
new file mode 100644
index 00000000..0c41a481
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/user-x.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user-x"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="18" y1="8" x2="23" y2="13"></line><line x1="23" y1="8" x2="18" y2="13"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/user.svg b/src/apps/qcam/assets/feathericons/user.svg
new file mode 100644
index 00000000..7bb5f291
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/user.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-user"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/users.svg b/src/apps/qcam/assets/feathericons/users.svg
new file mode 100644
index 00000000..aacf6b08
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/users.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/video-off.svg b/src/apps/qcam/assets/feathericons/video-off.svg
new file mode 100644
index 00000000..08ec6973
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/video-off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-video-off"><path d="M16 16v1a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2m5.66 0H14a2 2 0 0 1 2 2v3.34l1 1L23 7v10"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/video.svg b/src/apps/qcam/assets/feathericons/video.svg
new file mode 100644
index 00000000..8ff156aa
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/video.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-video"><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/voicemail.svg b/src/apps/qcam/assets/feathericons/voicemail.svg
new file mode 100644
index 00000000..5d78a8e7
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/voicemail.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-voicemail"><circle cx="5.5" cy="11.5" r="4.5"></circle><circle cx="18.5" cy="11.5" r="4.5"></circle><line x1="5.5" y1="16" x2="18.5" y2="16"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/volume-1.svg b/src/apps/qcam/assets/feathericons/volume-1.svg
new file mode 100644
index 00000000..150e875f
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/volume-1.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-1"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/volume-2.svg b/src/apps/qcam/assets/feathericons/volume-2.svg
new file mode 100644
index 00000000..03d521c7
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/volume-2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/volume-x.svg b/src/apps/qcam/assets/feathericons/volume-x.svg
new file mode 100644
index 00000000..be442406
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/volume-x.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume-x"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><line x1="23" y1="9" x2="17" y2="15"></line><line x1="17" y1="9" x2="23" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/volume.svg b/src/apps/qcam/assets/feathericons/volume.svg
new file mode 100644
index 00000000..53bfe15e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/volume.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-volume"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/watch.svg b/src/apps/qcam/assets/feathericons/watch.svg
new file mode 100644
index 00000000..a1099da3
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/watch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-watch"><circle cx="12" cy="12" r="7"></circle><polyline points="12 9 12 12 13.5 13.5"></polyline><path d="M16.51 17.35l-.35 3.83a2 2 0 0 1-2 1.82H9.83a2 2 0 0 1-2-1.82l-.35-3.83m.01-10.7l.35-3.83A2 2 0 0 1 9.83 1h4.35a2 2 0 0 1 2 1.82l.35 3.83"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/wifi-off.svg b/src/apps/qcam/assets/feathericons/wifi-off.svg
new file mode 100644
index 00000000..35eae43b
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/wifi-off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wifi-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"></path><path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"></path><path d="M10.71 5.05A16 16 0 0 1 22.58 9"></path><path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/wifi.svg b/src/apps/qcam/assets/feathericons/wifi.svg
new file mode 100644
index 00000000..748c285e
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/wifi.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wifi"><path d="M5 12.55a11 11 0 0 1 14.08 0"></path><path d="M1.42 9a16 16 0 0 1 21.16 0"></path><path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path><line x1="12" y1="20" x2="12.01" y2="20"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/wind.svg b/src/apps/qcam/assets/feathericons/wind.svg
new file mode 100644
index 00000000..82b36468
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/wind.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-wind"><path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"></path></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/x-circle.svg b/src/apps/qcam/assets/feathericons/x-circle.svg
new file mode 100644
index 00000000..94aad5e5
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/x-circle.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-circle"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/x-octagon.svg b/src/apps/qcam/assets/feathericons/x-octagon.svg
new file mode 100644
index 00000000..85431985
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/x-octagon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-octagon"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/x-square.svg b/src/apps/qcam/assets/feathericons/x-square.svg
new file mode 100644
index 00000000..7677c387
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/x-square.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-square"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/x.svg b/src/apps/qcam/assets/feathericons/x.svg
new file mode 100644
index 00000000..7d5875ca
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/x.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/youtube.svg b/src/apps/qcam/assets/feathericons/youtube.svg
new file mode 100644
index 00000000..c4824385
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/youtube.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-youtube"><path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z"></path><polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/zap-off.svg b/src/apps/qcam/assets/feathericons/zap-off.svg
new file mode 100644
index 00000000..c636f8bb
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/zap-off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap-off"><polyline points="12.41 6.75 13 2 10.57 4.92"></polyline><polyline points="18.57 12.91 21 10 15.66 10"></polyline><polyline points="8 8 3 14 12 14 11 22 16 16"></polyline><line x1="1" y1="1" x2="23" y2="23"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/zap.svg b/src/apps/qcam/assets/feathericons/zap.svg
new file mode 100644
index 00000000..8fdafa93
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/zap.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/zoom-in.svg b/src/apps/qcam/assets/feathericons/zoom-in.svg
new file mode 100644
index 00000000..da4572d2
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/zoom-in.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zoom-in"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/feathericons/zoom-out.svg b/src/apps/qcam/assets/feathericons/zoom-out.svg
new file mode 100644
index 00000000..fd678d72
--- /dev/null
+++ b/src/apps/qcam/assets/feathericons/zoom-out.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zoom-out"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> \ No newline at end of file
diff --git a/src/apps/qcam/assets/shader/RGB.frag b/src/apps/qcam/assets/shader/RGB.frag
new file mode 100644
index 00000000..4c374ac9
--- /dev/null
+++ b/src/apps/qcam/assets/shader/RGB.frag
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Laurent Pinchart
+ *
+ * RGB.frag - Fragment shader code for RGB formats
+ */
+
+#ifdef GL_ES
+precision mediump float;
+#endif
+
+varying vec2 textureOut;
+uniform sampler2D tex_y;
+
+void main(void)
+{
+ vec3 rgb;
+
+ rgb = texture2D(tex_y, textureOut).RGB_PATTERN;
+
+ gl_FragColor = vec4(rgb, 1.0);
+}
diff --git a/src/apps/qcam/assets/shader/YUV_2_planes.frag b/src/apps/qcam/assets/shader/YUV_2_planes.frag
new file mode 100644
index 00000000..1d5d1206
--- /dev/null
+++ b/src/apps/qcam/assets/shader/YUV_2_planes.frag
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Linaro
+ *
+ * YUV_2_planes.frag - Fragment shader code for NV12, NV16 and NV24 formats
+ */
+
+#ifdef GL_ES
+precision mediump float;
+#endif
+
+varying vec2 textureOut;
+uniform sampler2D tex_y;
+uniform sampler2D tex_u;
+
+const mat3 yuv2rgb_matrix = mat3(
+ YUV2RGB_MATRIX
+);
+
+const vec3 yuv2rgb_offset = vec3(
+ YUV2RGB_Y_OFFSET / 255.0, 128.0 / 255.0, 128.0 / 255.0
+);
+
+void main(void)
+{
+ vec3 yuv;
+
+ yuv.x = texture2D(tex_y, textureOut).r;
+#if defined(YUV_PATTERN_UV)
+ yuv.y = texture2D(tex_u, textureOut).r;
+ yuv.z = texture2D(tex_u, textureOut).a;
+#elif defined(YUV_PATTERN_VU)
+ yuv.y = texture2D(tex_u, textureOut).a;
+ yuv.z = texture2D(tex_u, textureOut).r;
+#else
+#error Invalid pattern
+#endif
+
+ vec3 rgb = yuv2rgb_matrix * (yuv - yuv2rgb_offset);
+
+ gl_FragColor = vec4(rgb, 1.0);
+}
diff --git a/src/apps/qcam/assets/shader/YUV_3_planes.frag b/src/apps/qcam/assets/shader/YUV_3_planes.frag
new file mode 100644
index 00000000..8f788e90
--- /dev/null
+++ b/src/apps/qcam/assets/shader/YUV_3_planes.frag
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Linaro
+ *
+ * YUV_3_planes_UV.frag - Fragment shader code for YUV420 format
+ */
+
+#ifdef GL_ES
+precision mediump float;
+#endif
+
+varying vec2 textureOut;
+uniform sampler2D tex_y;
+uniform sampler2D tex_u;
+uniform sampler2D tex_v;
+
+const mat3 yuv2rgb_matrix = mat3(
+ YUV2RGB_MATRIX
+);
+
+const vec3 yuv2rgb_offset = vec3(
+ YUV2RGB_Y_OFFSET / 255.0, 128.0 / 255.0, 128.0 / 255.0
+);
+
+void main(void)
+{
+ vec3 yuv;
+
+ yuv.x = texture2D(tex_y, textureOut).r;
+ yuv.y = texture2D(tex_u, textureOut).r;
+ yuv.z = texture2D(tex_v, textureOut).r;
+
+ vec3 rgb = yuv2rgb_matrix * (yuv - yuv2rgb_offset);
+
+ gl_FragColor = vec4(rgb, 1.0);
+}
diff --git a/src/apps/qcam/assets/shader/YUV_packed.frag b/src/apps/qcam/assets/shader/YUV_packed.frag
new file mode 100644
index 00000000..b9ef9d41
--- /dev/null
+++ b/src/apps/qcam/assets/shader/YUV_packed.frag
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ *
+ * YUV_packed.frag - Fragment shader code for YUYV packed formats
+ */
+
+#ifdef GL_ES
+precision mediump float;
+#endif
+
+varying vec2 textureOut;
+
+uniform sampler2D tex_y;
+uniform vec2 tex_step;
+
+const mat3 yuv2rgb_matrix = mat3(
+ YUV2RGB_MATRIX
+);
+
+const vec3 yuv2rgb_offset = vec3(
+ YUV2RGB_Y_OFFSET / 255.0, 128.0 / 255.0, 128.0 / 255.0
+);
+
+void main(void)
+{
+ /*
+ * The sampler won't interpolate the texture correctly along the X axis,
+ * as each RGBA pixel effectively stores two pixels. We thus need to
+ * interpolate manually.
+ *
+ * In integer texture coordinates, the Y values are layed out in the
+ * texture memory as follows:
+ *
+ * ...| Y U Y V | Y U Y V | Y U Y V |...
+ * ...| R G B A | R G B A | R G B A |...
+ * ^ ^ ^ ^ ^ ^
+ * | | | | | |
+ * n-1 n-0.5 n n+0.5 n+1 n+1.5
+ *
+ * For a texture location x in the interval [n, n+1[, sample the left
+ * and right pixels at n and n+1, and interpolate them with
+ *
+ * left.r * (1 - a) + left.b * a if fract(x) < 0.5
+ * left.b * (1 - a) + right.r * a if fract(x) >= 0.5
+ *
+ * with a = fract(x * 2) which can also be written
+ *
+ * a = fract(x) * 2 if fract(x) < 0.5
+ * a = fract(x) * 2 - 1 if fract(x) >= 0.5
+ */
+ vec2 pos = textureOut;
+ float f_x = fract(pos.x / tex_step.x);
+
+ vec4 left = texture2D(tex_y, vec2(pos.x - f_x * tex_step.x, pos.y));
+ vec4 right = texture2D(tex_y, vec2(pos.x + (1.0 - f_x) * tex_step.x , pos.y));
+
+#if defined(YUV_PATTERN_UYVY)
+ float y_left = mix(left.g, left.a, f_x * 2.0);
+ float y_right = mix(left.a, right.g, f_x * 2.0 - 1.0);
+ vec2 uv = mix(left.rb, right.rb, f_x);
+#elif defined(YUV_PATTERN_VYUY)
+ float y_left = mix(left.g, left.a, f_x * 2.0);
+ float y_right = mix(left.a, right.g, f_x * 2.0 - 1.0);
+ vec2 uv = mix(left.br, right.br, f_x);
+#elif defined(YUV_PATTERN_YUYV)
+ float y_left = mix(left.r, left.b, f_x * 2.0);
+ float y_right = mix(left.b, right.r, f_x * 2.0 - 1.0);
+ vec2 uv = mix(left.ga, right.ga, f_x);
+#elif defined(YUV_PATTERN_YVYU)
+ float y_left = mix(left.r, left.b, f_x * 2.0);
+ float y_right = mix(left.b, right.r, f_x * 2.0 - 1.0);
+ vec2 uv = mix(left.ag, right.ag, f_x);
+#else
+#error Invalid pattern
+#endif
+
+ float y = mix(y_left, y_right, step(0.5, f_x));
+
+ vec3 rgb = yuv2rgb_matrix * (vec3(y, uv) - yuv2rgb_offset);
+
+ gl_FragColor = vec4(rgb, 1.0);
+}
diff --git a/src/apps/qcam/assets/shader/bayer_1x_packed.frag b/src/apps/qcam/assets/shader/bayer_1x_packed.frag
new file mode 100644
index 00000000..f53f5575
--- /dev/null
+++ b/src/apps/qcam/assets/shader/bayer_1x_packed.frag
@@ -0,0 +1,216 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Based on the code from http://jgt.akpeters.com/papers/McGuire08/
+ *
+ * Efficient, High-Quality Bayer Demosaic Filtering on GPUs
+ *
+ * Morgan McGuire
+ *
+ * This paper appears in issue Volume 13, Number 4.
+ * ---------------------------------------------------------
+ * Copyright (c) 2008, Morgan McGuire. All rights reserved.
+ *
+ *
+ * Modified by Linaro Ltd for 10/12-bit packed vs 8-bit raw Bayer format,
+ * and for simpler demosaic algorithm.
+ * Copyright (C) 2020, Linaro
+ *
+ * bayer_1x_packed.frag - Fragment shader code for raw Bayer 10-bit and 12-bit
+ * packed formats
+ */
+
+#ifdef GL_ES
+precision mediump float;
+#endif
+
+/*
+ * These constants are used to select the bytes containing the HS part of
+ * the pixel value:
+ * BPP - bytes per pixel,
+ * THRESHOLD_L = fract(BPP) * 0.5 + 0.02
+ * THRESHOLD_H = 1.0 - fract(BPP) * 1.5 + 0.02
+ * Let X is the x coordinate in the texture measured in bytes (so that the
+ * range is from 0 to (stride_-1)) aligned on the nearest pixel.
+ * E.g. for RAW10P:
+ * -------------+-------------------+-------------------+--
+ * pixel No | 0 1 2 3 | 4 5 6 7 | ...
+ * -------------+-------------------+-------------------+--
+ * byte offset | 0 1 2 3 4 | 5 6 7 8 9 | ...
+ * -------------+-------------------+-------------------+--
+ * X | 0.0 1.25 2.5 3.75 | 5.0 6.25 7.5 8.75 | ...
+ * -------------+-------------------+-------------------+--
+ * If fract(X) < THRESHOLD_L then the previous byte contains the LS
+ * bits of the pixel values and needs to be skipped.
+ * If fract(X) > THRESHOLD_H then the next byte contains the LS bits
+ * of the pixel values and needs to be skipped.
+ */
+#if defined(RAW10P)
+#define BPP 1.25
+#define THRESHOLD_L 0.14
+#define THRESHOLD_H 0.64
+#elif defined(RAW12P)
+#define BPP 1.5
+#define THRESHOLD_L 0.27
+#define THRESHOLD_H 0.27
+#else
+#error Invalid raw format
+#endif
+
+
+varying vec2 textureOut;
+
+/* the texture size in pixels */
+uniform vec2 tex_size;
+uniform vec2 tex_step;
+uniform vec2 tex_bayer_first_red;
+
+uniform sampler2D tex_y;
+
+void main(void)
+{
+ vec3 rgb;
+
+ /*
+ * center_bytes holds the coordinates of the MS byte of the pixel
+ * being sampled on the [0, stride-1/height-1] range.
+ * center_pixel holds the coordinates of the pixel being sampled
+ * on the [0, width/height-1] range.
+ */
+ vec2 center_bytes;
+ vec2 center_pixel;
+
+ /*
+ * x- and y-positions of the adjacent pixels on the [0, 1] range.
+ */
+ vec2 xcoords;
+ vec2 ycoords;
+
+ /*
+ * The coordinates passed to the shader in textureOut may point
+ * to a place in between the pixels if the texture format doesn't
+ * match the image format. In particular, MIPI packed raw Bayer
+ * formats don't have a matching texture format.
+ * In this case align the coordinates to the left nearest pixel
+ * by hand.
+ */
+ center_pixel = floor(textureOut * tex_size);
+ center_bytes.y = center_pixel.y;
+
+ /*
+ * Add a small number (a few mantissa's LSBs) to avoid float
+ * representation issues. Maybe paranoic.
+ */
+ center_bytes.x = BPP * center_pixel.x + 0.02;
+
+ float fract_x = fract(center_bytes.x);
+
+ /*
+ * The below floor() call ensures that center_bytes.x points
+ * at one of the bytes representing the 8 higher bits of
+ * the pixel value, not at the byte containing the LS bits
+ * of the group of the pixels.
+ */
+ center_bytes.x = floor(center_bytes.x);
+ center_bytes *= tex_step;
+
+ xcoords = center_bytes.x + vec2(-tex_step.x, tex_step.x);
+ ycoords = center_bytes.y + vec2(-tex_step.y, tex_step.y);
+
+ /*
+ * If xcoords[0] points at the byte containing the LS bits
+ * of the previous group of the pixels, move xcoords[0] one
+ * byte back.
+ */
+ xcoords[0] += (fract_x < THRESHOLD_L) ? -tex_step.x : 0.0;
+
+ /*
+ * If xcoords[1] points at the byte containing the LS bits
+ * of the current group of the pixels, move xcoords[1] one
+ * byte forward.
+ */
+ xcoords[1] += (fract_x > THRESHOLD_H) ? tex_step.x : 0.0;
+
+ vec2 alternate = mod(center_pixel.xy + tex_bayer_first_red, 2.0);
+ bool even_col = alternate.x < 1.0;
+ bool even_row = alternate.y < 1.0;
+
+ /*
+ * We need to sample the central pixel and the ones with offset
+ * of -1 to +1 pixel in both X and Y directions. Let's name these
+ * pixels as below, where C is the central pixel:
+ *
+ * +----+----+----+----+
+ * | \ x| | | |
+ * |y \ | -1 | 0 | +1 |
+ * +----+----+----+----+
+ * | +1 | D2 | A1 | D3 |
+ * +----+----+----+----+
+ * | 0 | B0 | C | B1 |
+ * +----+----+----+----+
+ * | -1 | D0 | A0 | D1 |
+ * +----+----+----+----+
+ *
+ * In the below equations (0,-1).r means "r component of the texel
+ * shifted by -tex_step.y from the center_bytes one" etc.
+ *
+ * In the even row / even column (EE) case the colour values are:
+ * R = C = (0,0).r,
+ * G = (A0 + A1 + B0 + B1) / 4.0 =
+ * ( (0,-1).r + (0,1).r + (-1,0).r + (1,0).r ) / 4.0,
+ * B = (D0 + D1 + D2 + D3) / 4.0 =
+ * ( (-1,-1).r + (1,-1).r + (-1,1).r + (1,1).r ) / 4.0
+ *
+ * For even row / odd column (EO):
+ * R = (B0 + B1) / 2.0 = ( (-1,0).r + (1,0).r ) / 2.0,
+ * G = C = (0,0).r,
+ * B = (A0 + A1) / 2.0 = ( (0,-1).r + (0,1).r ) / 2.0
+ *
+ * For odd row / even column (OE):
+ * R = (A0 + A1) / 2.0 = ( (0,-1).r + (0,1).r ) / 2.0,
+ * G = C = (0,0).r,
+ * B = (B0 + B1) / 2.0 = ( (-1,0).r + (1,0).r ) / 2.0
+ *
+ * For odd row / odd column (OO):
+ * R = (D0 + D1 + D2 + D3) / 4.0 =
+ * ( (-1,-1).r + (1,-1).r + (-1,1).r + (1,1).r ) / 4.0,
+ * G = (A0 + A1 + B0 + B1) / 4.0 =
+ * ( (0,-1).r + (0,1).r + (-1,0).r + (1,0).r ) / 4.0,
+ * B = C = (0,0).r
+ */
+
+ /*
+ * Fetch the values and precalculate the terms:
+ * patterns.x = (A0 + A1) / 2.0
+ * patterns.y = (B0 + B1) / 2.0
+ * patterns.z = (A0 + A1 + B0 + B1) / 4.0
+ * patterns.w = (D0 + D1 + D2 + D3) / 4.0
+ */
+ #define fetch(x, y) texture2D(tex_y, vec2(x, y)).r
+
+ float C = texture2D(tex_y, center_bytes).r;
+ vec4 patterns = vec4(
+ fetch(center_bytes.x, ycoords[0]), /* A0: (0,-1) */
+ fetch(xcoords[0], center_bytes.y), /* B0: (-1,0) */
+ fetch(xcoords[0], ycoords[0]), /* D0: (-1,-1) */
+ fetch(xcoords[1], ycoords[0])); /* D1: (1,-1) */
+ vec4 temp = vec4(
+ fetch(center_bytes.x, ycoords[1]), /* A1: (0,1) */
+ fetch(xcoords[1], center_bytes.y), /* B1: (1,0) */
+ fetch(xcoords[1], ycoords[1]), /* D3: (1,1) */
+ fetch(xcoords[0], ycoords[1])); /* D2: (-1,1) */
+ patterns = (patterns + temp) * 0.5;
+ /* .x = (A0 + A1) / 2.0, .y = (B0 + B1) / 2.0 */
+ /* .z = (D0 + D3) / 2.0, .w = (D1 + D2) / 2.0 */
+ patterns.w = (patterns.z + patterns.w) * 0.5;
+ patterns.z = (patterns.x + patterns.y) * 0.5;
+
+ rgb = even_col ?
+ (even_row ?
+ vec3(C, patterns.zw) :
+ vec3(patterns.x, C, patterns.y)) :
+ (even_row ?
+ vec3(patterns.y, C, patterns.x) :
+ vec3(patterns.wz, C));
+
+ gl_FragColor = vec4(rgb, 1.0);
+}
diff --git a/src/apps/qcam/assets/shader/bayer_8.frag b/src/apps/qcam/assets/shader/bayer_8.frag
new file mode 100644
index 00000000..7e35ca88
--- /dev/null
+++ b/src/apps/qcam/assets/shader/bayer_8.frag
@@ -0,0 +1,107 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+From http://jgt.akpeters.com/papers/McGuire08/
+
+Efficient, High-Quality Bayer Demosaic Filtering on GPUs
+
+Morgan McGuire
+
+This paper appears in issue Volume 13, Number 4.
+---------------------------------------------------------
+Copyright (c) 2008, Morgan McGuire. All rights reserved.
+
+Modified by Linaro Ltd to integrate it into libcamera.
+Copyright (C) 2021, Linaro
+*/
+
+//Pixel Shader
+#ifdef GL_ES
+precision mediump float;
+#endif
+
+/** Monochrome RGBA or GL_LUMINANCE Bayer encoded texture.*/
+uniform sampler2D tex_y;
+varying vec4 center;
+varying vec4 yCoord;
+varying vec4 xCoord;
+
+void main(void) {
+ #define fetch(x, y) texture2D(tex_y, vec2(x, y)).r
+
+ float C = texture2D(tex_y, center.xy).r; // ( 0, 0)
+ const vec4 kC = vec4( 4.0, 6.0, 5.0, 5.0) / 8.0;
+
+ // Determine which of four types of pixels we are on.
+ vec2 alternate = mod(floor(center.zw), 2.0);
+
+ vec4 Dvec = vec4(
+ fetch(xCoord[1], yCoord[1]), // (-1,-1)
+ fetch(xCoord[1], yCoord[2]), // (-1, 1)
+ fetch(xCoord[2], yCoord[1]), // ( 1,-1)
+ fetch(xCoord[2], yCoord[2])); // ( 1, 1)
+
+ vec4 PATTERN = (kC.xyz * C).xyzz;
+
+ // Can also be a dot product with (1,1,1,1) on hardware where that is
+ // specially optimized.
+ // Equivalent to: D = Dvec[0] + Dvec[1] + Dvec[2] + Dvec[3];
+ Dvec.xy += Dvec.zw;
+ Dvec.x += Dvec.y;
+
+ vec4 value = vec4(
+ fetch(center.x, yCoord[0]), // ( 0,-2)
+ fetch(center.x, yCoord[1]), // ( 0,-1)
+ fetch(xCoord[0], center.y), // (-2, 0)
+ fetch(xCoord[1], center.y)); // (-1, 0)
+
+ vec4 temp = vec4(
+ fetch(center.x, yCoord[3]), // ( 0, 2)
+ fetch(center.x, yCoord[2]), // ( 0, 1)
+ fetch(xCoord[3], center.y), // ( 2, 0)
+ fetch(xCoord[2], center.y)); // ( 1, 0)
+
+ // Even the simplest compilers should be able to constant-fold these to
+ // avoid the division.
+ // Note that on scalar processors these constants force computation of some
+ // identical products twice.
+ const vec4 kA = vec4(-1.0, -1.5, 0.5, -1.0) / 8.0;
+ const vec4 kB = vec4( 2.0, 0.0, 0.0, 4.0) / 8.0;
+ const vec4 kD = vec4( 0.0, 2.0, -1.0, -1.0) / 8.0;
+
+ // Conserve constant registers and take advantage of free swizzle on load
+ #define kE (kA.xywz)
+ #define kF (kB.xywz)
+
+ value += temp;
+
+ // There are five filter patterns (identity, cross, checker,
+ // theta, phi). Precompute the terms from all of them and then
+ // use swizzles to assign to color channels.
+ //
+ // Channel Matches
+ // x cross (e.g., EE G)
+ // y checker (e.g., EE B)
+ // z theta (e.g., EO R)
+ // w phi (e.g., EO R)
+ #define A (value[0])
+ #define B (value[1])
+ #define D (Dvec.x)
+ #define E (value[2])
+ #define F (value[3])
+
+ // Avoid zero elements. On a scalar processor this saves two MADDs
+ // and it has no effect on a vector processor.
+ PATTERN.yzw += (kD.yz * D).xyy;
+
+ PATTERN += (kA.xyz * A).xyzx + (kE.xyw * E).xyxz;
+ PATTERN.xw += kB.xw * B;
+ PATTERN.xz += kF.xz * F;
+
+ gl_FragColor.rgb = (alternate.y == 0.0) ?
+ ((alternate.x == 0.0) ?
+ vec3(C, PATTERN.xy) :
+ vec3(PATTERN.z, C, PATTERN.w)) :
+ ((alternate.x == 0.0) ?
+ vec3(PATTERN.w, C, PATTERN.z) :
+ vec3(PATTERN.yx, C));
+}
diff --git a/src/apps/qcam/assets/shader/bayer_8.vert b/src/apps/qcam/assets/shader/bayer_8.vert
new file mode 100644
index 00000000..3695a5e9
--- /dev/null
+++ b/src/apps/qcam/assets/shader/bayer_8.vert
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+From http://jgt.akpeters.com/papers/McGuire08/
+
+Efficient, High-Quality Bayer Demosaic Filtering on GPUs
+
+Morgan McGuire
+
+This paper appears in issue Volume 13, Number 4.
+---------------------------------------------------------
+Copyright (c) 2008, Morgan McGuire. All rights reserved.
+
+Modified by Linaro Ltd to integrate it into libcamera.
+Copyright (C) 2021, Linaro
+*/
+
+//Vertex Shader
+
+attribute vec4 vertexIn;
+attribute vec2 textureIn;
+
+uniform vec2 tex_size; /* The texture size in pixels */
+uniform vec2 tex_step;
+
+/** Pixel position of the first red pixel in the */
+/** Bayer pattern. [{0,1}, {0, 1}]*/
+uniform vec2 tex_bayer_first_red;
+
+/** .xy = Pixel being sampled in the fragment shader on the range [0, 1]
+ .zw = ...on the range [0, sourceSize], offset by firstRed */
+varying vec4 center;
+
+/** center.x + (-2/w, -1/w, 1/w, 2/w); These are the x-positions */
+/** of the adjacent pixels.*/
+varying vec4 xCoord;
+
+/** center.y + (-2/h, -1/h, 1/h, 2/h); These are the y-positions */
+/** of the adjacent pixels.*/
+varying vec4 yCoord;
+
+void main(void) {
+ center.xy = textureIn;
+ center.zw = textureIn * tex_size + tex_bayer_first_red;
+
+ xCoord = center.x + vec4(-2.0 * tex_step.x,
+ -tex_step.x, tex_step.x, 2.0 * tex_step.x);
+ yCoord = center.y + vec4(-2.0 * tex_step.y,
+ -tex_step.y, tex_step.y, 2.0 * tex_step.y);
+
+ gl_Position = vertexIn;
+}
diff --git a/src/apps/qcam/assets/shader/identity.vert b/src/apps/qcam/assets/shader/identity.vert
new file mode 100644
index 00000000..12c41377
--- /dev/null
+++ b/src/apps/qcam/assets/shader/identity.vert
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Linaro
+ *
+ * identity.vert - Identity vertex shader for pixel format conversion
+ */
+
+attribute vec4 vertexIn;
+attribute vec2 textureIn;
+varying vec2 textureOut;
+
+uniform float stride_factor;
+
+void main(void)
+{
+ gl_Position = vertexIn;
+ textureOut = vec2(textureIn.x * stride_factor, textureIn.y);
+}
diff --git a/src/apps/qcam/assets/shader/shaders.qrc b/src/apps/qcam/assets/shader/shaders.qrc
new file mode 100644
index 00000000..96c709f9
--- /dev/null
+++ b/src/apps/qcam/assets/shader/shaders.qrc
@@ -0,0 +1,13 @@
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file>RGB.frag</file>
+ <file>YUV_2_planes.frag</file>
+ <file>YUV_3_planes.frag</file>
+ <file>YUV_packed.frag</file>
+ <file>bayer_1x_packed.frag</file>
+ <file>bayer_8.frag</file>
+ <file>bayer_8.vert</file>
+ <file>identity.vert</file>
+</qresource>
+</RCC>
diff --git a/src/apps/qcam/cam_select_dialog.cpp b/src/apps/qcam/cam_select_dialog.cpp
new file mode 100644
index 00000000..3c8b12a9
--- /dev/null
+++ b/src/apps/qcam/cam_select_dialog.cpp
@@ -0,0 +1,111 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Utkarsh Tiwari <utkarsh02t@gmail.com>
+ *
+ * cam_select_dialog.cpp - qcam - Camera Selection dialog
+ */
+
+#include "cam_select_dialog.h"
+
+#include <memory>
+
+#include <libcamera/camera.h>
+#include <libcamera/camera_manager.h>
+
+#include <QComboBox>
+#include <QDialogButtonBox>
+#include <QFormLayout>
+#include <QLabel>
+#include <QString>
+
+CameraSelectorDialog::CameraSelectorDialog(libcamera::CameraManager *cameraManager,
+ QWidget *parent)
+ : QDialog(parent), cm_(cameraManager)
+{
+ /* Use a QFormLayout for the dialog. */
+ QFormLayout *layout = new QFormLayout(this);
+
+ /* Setup the camera id combo-box. */
+ cameraIdComboBox_ = new QComboBox;
+ for (const auto &cam : cm_->cameras())
+ cameraIdComboBox_->addItem(QString::fromStdString(cam->id()));
+
+ /* Set camera information labels. */
+ cameraLocation_ = new QLabel;
+ cameraModel_ = new QLabel;
+
+ updateCameraInfo(cameraIdComboBox_->currentText());
+ connect(cameraIdComboBox_, &QComboBox::currentTextChanged,
+ this, &CameraSelectorDialog::updateCameraInfo);
+
+ /* Setup the QDialogButton Box */
+ QDialogButtonBox *buttonBox =
+ new QDialogButtonBox(QDialogButtonBox::Ok |
+ QDialogButtonBox::Cancel);
+
+ connect(buttonBox, &QDialogButtonBox::accepted,
+ this, &QDialog::accept);
+ connect(buttonBox, &QDialogButtonBox::rejected,
+ this, &QDialog::reject);
+
+ /* Set the layout. */
+ layout->addRow("Camera:", cameraIdComboBox_);
+ layout->addRow("Location:", cameraLocation_);
+ layout->addRow("Model:", cameraModel_);
+ layout->addWidget(buttonBox);
+}
+
+CameraSelectorDialog::~CameraSelectorDialog() = default;
+
+std::string CameraSelectorDialog::getCameraId()
+{
+ return cameraIdComboBox_->currentText().toStdString();
+}
+
+/* Hotplug / Unplug Support. */
+void CameraSelectorDialog::addCamera(QString cameraId)
+{
+ cameraIdComboBox_->addItem(cameraId);
+}
+
+void CameraSelectorDialog::removeCamera(QString cameraId)
+{
+ int cameraIndex = cameraIdComboBox_->findText(cameraId);
+ cameraIdComboBox_->removeItem(cameraIndex);
+}
+
+/* Camera Information */
+void CameraSelectorDialog::updateCameraInfo(QString cameraId)
+{
+ const std::shared_ptr<libcamera::Camera> &camera =
+ cm_->get(cameraId.toStdString());
+
+ if (!camera)
+ return;
+
+ const libcamera::ControlList &properties = camera->properties();
+
+ const auto &location = properties.get(libcamera::properties::Location);
+ if (location) {
+ switch (*location) {
+ case libcamera::properties::CameraLocationFront:
+ cameraLocation_->setText("Internal front camera");
+ break;
+ case libcamera::properties::CameraLocationBack:
+ cameraLocation_->setText("Internal back camera");
+ break;
+ case libcamera::properties::CameraLocationExternal:
+ cameraLocation_->setText("External camera");
+ break;
+ default:
+ cameraLocation_->setText("Unknown");
+ }
+ } else {
+ cameraLocation_->setText("Unknown");
+ }
+
+ const auto &model = properties.get(libcamera::properties::Model)
+ .value_or("Unknown");
+
+ cameraModel_->setText(QString::fromStdString(model));
+}
diff --git a/src/apps/qcam/cam_select_dialog.h b/src/apps/qcam/cam_select_dialog.h
new file mode 100644
index 00000000..0b7709ed
--- /dev/null
+++ b/src/apps/qcam/cam_select_dialog.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2022, Utkarsh Tiwari <utkarsh02t@gmail.com>
+ *
+ * cam_select_dialog.h - qcam - Camera Selection dialog
+ */
+
+#pragma once
+
+#include <string>
+
+#include <libcamera/camera.h>
+#include <libcamera/camera_manager.h>
+#include <libcamera/controls.h>
+#include <libcamera/property_ids.h>
+
+#include <QDialog>
+#include <QString>
+
+class QComboBox;
+class QLabel;
+
+class CameraSelectorDialog : public QDialog
+{
+ Q_OBJECT
+public:
+ CameraSelectorDialog(libcamera::CameraManager *cameraManager,
+ QWidget *parent);
+ ~CameraSelectorDialog();
+
+ std::string getCameraId();
+
+ /* Hotplug / Unplug Support. */
+ void addCamera(QString cameraId);
+ void removeCamera(QString cameraId);
+
+ /* Camera Information */
+ void updateCameraInfo(QString cameraId);
+
+private:
+ libcamera::CameraManager *cm_;
+
+ /* UI elements. */
+ QComboBox *cameraIdComboBox_;
+ QLabel *cameraLocation_;
+ QLabel *cameraModel_;
+};
diff --git a/src/apps/qcam/format_converter.cpp b/src/apps/qcam/format_converter.cpp
new file mode 100644
index 00000000..9331da0c
--- /dev/null
+++ b/src/apps/qcam/format_converter.cpp
@@ -0,0 +1,359 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * format_convert.cpp - qcam - Convert buffer to RGB
+ */
+
+#include "format_converter.h"
+
+#include <errno.h>
+#include <utility>
+
+#include <QImage>
+
+#include <libcamera/formats.h>
+
+#include "../cam/image.h"
+
+#define RGBSHIFT 8
+#ifndef MAX
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#endif
+#ifndef MIN
+#define MIN(a,b) ((a)<(b)?(a):(b))
+#endif
+#ifndef CLAMP
+#define CLAMP(a,low,high) MAX((low),MIN((high),(a)))
+#endif
+#ifndef CLIP
+#define CLIP(x) CLAMP(x,0,255)
+#endif
+
+int FormatConverter::configure(const libcamera::PixelFormat &format,
+ const QSize &size, unsigned int stride)
+{
+ switch (format) {
+ case libcamera::formats::NV12:
+ formatFamily_ = YUVSemiPlanar;
+ horzSubSample_ = 2;
+ vertSubSample_ = 2;
+ nvSwap_ = false;
+ break;
+ case libcamera::formats::NV21:
+ formatFamily_ = YUVSemiPlanar;
+ horzSubSample_ = 2;
+ vertSubSample_ = 2;
+ nvSwap_ = true;
+ break;
+ case libcamera::formats::NV16:
+ formatFamily_ = YUVSemiPlanar;
+ horzSubSample_ = 2;
+ vertSubSample_ = 1;
+ nvSwap_ = false;
+ break;
+ case libcamera::formats::NV61:
+ formatFamily_ = YUVSemiPlanar;
+ horzSubSample_ = 2;
+ vertSubSample_ = 1;
+ nvSwap_ = true;
+ break;
+ case libcamera::formats::NV24:
+ formatFamily_ = YUVSemiPlanar;
+ horzSubSample_ = 1;
+ vertSubSample_ = 1;
+ nvSwap_ = false;
+ break;
+ case libcamera::formats::NV42:
+ formatFamily_ = YUVSemiPlanar;
+ horzSubSample_ = 1;
+ vertSubSample_ = 1;
+ nvSwap_ = true;
+ break;
+
+ case libcamera::formats::R8:
+ formatFamily_ = RGB;
+ r_pos_ = 0;
+ g_pos_ = 0;
+ b_pos_ = 0;
+ bpp_ = 1;
+ break;
+ case libcamera::formats::RGB888:
+ formatFamily_ = RGB;
+ r_pos_ = 2;
+ g_pos_ = 1;
+ b_pos_ = 0;
+ bpp_ = 3;
+ break;
+ case libcamera::formats::BGR888:
+ formatFamily_ = RGB;
+ r_pos_ = 0;
+ g_pos_ = 1;
+ b_pos_ = 2;
+ bpp_ = 3;
+ break;
+ case libcamera::formats::ARGB8888:
+ case libcamera::formats::XRGB8888:
+ formatFamily_ = RGB;
+ r_pos_ = 2;
+ g_pos_ = 1;
+ b_pos_ = 0;
+ bpp_ = 4;
+ break;
+ case libcamera::formats::RGBA8888:
+ case libcamera::formats::RGBX8888:
+ formatFamily_ = RGB;
+ r_pos_ = 3;
+ g_pos_ = 2;
+ b_pos_ = 1;
+ bpp_ = 4;
+ break;
+ case libcamera::formats::ABGR8888:
+ case libcamera::formats::XBGR8888:
+ formatFamily_ = RGB;
+ r_pos_ = 0;
+ g_pos_ = 1;
+ b_pos_ = 2;
+ bpp_ = 4;
+ break;
+ case libcamera::formats::BGRA8888:
+ case libcamera::formats::BGRX8888:
+ formatFamily_ = RGB;
+ r_pos_ = 1;
+ g_pos_ = 2;
+ b_pos_ = 3;
+ bpp_ = 4;
+ break;
+
+ case libcamera::formats::VYUY:
+ formatFamily_ = YUVPacked;
+ y_pos_ = 1;
+ cb_pos_ = 2;
+ break;
+ case libcamera::formats::YVYU:
+ formatFamily_ = YUVPacked;
+ y_pos_ = 0;
+ cb_pos_ = 3;
+ break;
+ case libcamera::formats::UYVY:
+ formatFamily_ = YUVPacked;
+ y_pos_ = 1;
+ cb_pos_ = 0;
+ break;
+ case libcamera::formats::YUYV:
+ formatFamily_ = YUVPacked;
+ y_pos_ = 0;
+ cb_pos_ = 1;
+ break;
+
+ case libcamera::formats::YUV420:
+ formatFamily_ = YUVPlanar;
+ horzSubSample_ = 2;
+ vertSubSample_ = 2;
+ nvSwap_ = false;
+ break;
+ case libcamera::formats::YVU420:
+ formatFamily_ = YUVPlanar;
+ horzSubSample_ = 2;
+ vertSubSample_ = 2;
+ nvSwap_ = true;
+ break;
+ case libcamera::formats::YUV422:
+ formatFamily_ = YUVPlanar;
+ horzSubSample_ = 2;
+ vertSubSample_ = 1;
+ nvSwap_ = false;
+ break;
+
+ case libcamera::formats::MJPEG:
+ formatFamily_ = MJPEG;
+ break;
+
+ default:
+ return -EINVAL;
+ };
+
+ format_ = format;
+ width_ = size.width();
+ height_ = size.height();
+ stride_ = stride;
+
+ return 0;
+}
+
+void FormatConverter::convert(const Image *src, size_t size, QImage *dst)
+{
+ switch (formatFamily_) {
+ case MJPEG:
+ dst->loadFromData(src->data(0).data(), size, "JPEG");
+ break;
+ case RGB:
+ convertRGB(src, dst->bits());
+ break;
+ case YUVPacked:
+ convertYUVPacked(src, dst->bits());
+ break;
+ case YUVSemiPlanar:
+ convertYUVSemiPlanar(src, dst->bits());
+ break;
+ case YUVPlanar:
+ convertYUVPlanar(src, dst->bits());
+ break;
+ };
+}
+
+static void yuv_to_rgb(int y, int u, int v, int *r, int *g, int *b)
+{
+ int c = y - 16;
+ int d = u - 128;
+ int e = v - 128;
+ *r = CLIP(( 298 * c + 409 * e + 128) >> RGBSHIFT);
+ *g = CLIP(( 298 * c - 100 * d - 208 * e + 128) >> RGBSHIFT);
+ *b = CLIP(( 298 * c + 516 * d + 128) >> RGBSHIFT);
+}
+
+void FormatConverter::convertRGB(const Image *srcImage, unsigned char *dst)
+{
+ const unsigned char *src = srcImage->data(0).data();
+ unsigned int x, y;
+ int r, g, b;
+
+ for (y = 0; y < height_; y++) {
+ for (x = 0; x < width_; x++) {
+ r = src[bpp_ * x + r_pos_];
+ g = src[bpp_ * x + g_pos_];
+ b = src[bpp_ * x + b_pos_];
+
+ dst[4 * x + 0] = b;
+ dst[4 * x + 1] = g;
+ dst[4 * x + 2] = r;
+ dst[4 * x + 3] = 0xff;
+ }
+
+ src += stride_;
+ dst += width_ * 4;
+ }
+}
+
+void FormatConverter::convertYUVPacked(const Image *srcImage, unsigned char *dst)
+{
+ const unsigned char *src = srcImage->data(0).data();
+ unsigned int src_x, src_y, dst_x, dst_y;
+ unsigned int src_stride;
+ unsigned int dst_stride;
+ unsigned int cr_pos;
+ int r, g, b, y, cr, cb;
+
+ cr_pos = (cb_pos_ + 2) % 4;
+ src_stride = stride_;
+ dst_stride = width_ * 4;
+
+ for (src_y = 0, dst_y = 0; dst_y < height_; src_y++, dst_y++) {
+ for (src_x = 0, dst_x = 0; dst_x < width_; ) {
+ cb = src[src_y * src_stride + src_x * 4 + cb_pos_];
+ cr = src[src_y * src_stride + src_x * 4 + cr_pos];
+
+ y = src[src_y * src_stride + src_x * 4 + y_pos_];
+ yuv_to_rgb(y, cb, cr, &r, &g, &b);
+ dst[dst_y * dst_stride + 4 * dst_x + 0] = b;
+ dst[dst_y * dst_stride + 4 * dst_x + 1] = g;
+ dst[dst_y * dst_stride + 4 * dst_x + 2] = r;
+ dst[dst_y * dst_stride + 4 * dst_x + 3] = 0xff;
+ dst_x++;
+
+ y = src[src_y * src_stride + src_x * 4 + y_pos_ + 2];
+ yuv_to_rgb(y, cb, cr, &r, &g, &b);
+ dst[dst_y * dst_stride + 4 * dst_x + 0] = b;
+ dst[dst_y * dst_stride + 4 * dst_x + 1] = g;
+ dst[dst_y * dst_stride + 4 * dst_x + 2] = r;
+ dst[dst_y * dst_stride + 4 * dst_x + 3] = 0xff;
+ dst_x++;
+
+ src_x++;
+ }
+ }
+}
+
+void FormatConverter::convertYUVPlanar(const Image *srcImage, unsigned char *dst)
+{
+ unsigned int c_stride = stride_ / horzSubSample_;
+ unsigned int c_inc = horzSubSample_ == 1 ? 1 : 0;
+ const unsigned char *src_y = srcImage->data(0).data();
+ const unsigned char *src_cb = srcImage->data(1).data();
+ const unsigned char *src_cr = srcImage->data(2).data();
+ int r, g, b;
+
+ if (nvSwap_)
+ std::swap(src_cb, src_cr);
+
+ for (unsigned int y = 0; y < height_; y++) {
+ const unsigned char *line_y = src_y + y * stride_;
+ const unsigned char *line_cb = src_cb + (y / vertSubSample_) *
+ c_stride;
+ const unsigned char *line_cr = src_cr + (y / vertSubSample_) *
+ c_stride;
+
+ for (unsigned int x = 0; x < width_; x += 2) {
+ yuv_to_rgb(*line_y, *line_cb, *line_cr, &r, &g, &b);
+ dst[0] = b;
+ dst[1] = g;
+ dst[2] = r;
+ dst[3] = 0xff;
+ line_y++;
+ line_cb += c_inc;
+ line_cr += c_inc;
+ dst += 4;
+
+ yuv_to_rgb(*line_y, *line_cb, *line_cr, &r, &g, &b);
+ dst[0] = b;
+ dst[1] = g;
+ dst[2] = r;
+ dst[3] = 0xff;
+ line_y++;
+ line_cb += 1;
+ line_cr += 1;
+ dst += 4;
+ }
+ }
+}
+
+void FormatConverter::convertYUVSemiPlanar(const Image *srcImage, unsigned char *dst)
+{
+ unsigned int c_stride = stride_ * (2 / horzSubSample_);
+ unsigned int c_inc = horzSubSample_ == 1 ? 2 : 0;
+ unsigned int cb_pos = nvSwap_ ? 1 : 0;
+ unsigned int cr_pos = nvSwap_ ? 0 : 1;
+ const unsigned char *src = srcImage->data(0).data();
+ const unsigned char *src_c = srcImage->data(1).data();
+ int r, g, b;
+
+ for (unsigned int y = 0; y < height_; y++) {
+ const unsigned char *src_y = src + y * stride_;
+ const unsigned char *src_cb = src_c + (y / vertSubSample_) *
+ c_stride + cb_pos;
+ const unsigned char *src_cr = src_c + (y / vertSubSample_) *
+ c_stride + cr_pos;
+
+ for (unsigned int x = 0; x < width_; x += 2) {
+ yuv_to_rgb(*src_y, *src_cb, *src_cr, &r, &g, &b);
+ dst[0] = b;
+ dst[1] = g;
+ dst[2] = r;
+ dst[3] = 0xff;
+ src_y++;
+ src_cb += c_inc;
+ src_cr += c_inc;
+ dst += 4;
+
+ yuv_to_rgb(*src_y, *src_cb, *src_cr, &r, &g, &b);
+ dst[0] = b;
+ dst[1] = g;
+ dst[2] = r;
+ dst[3] = 0xff;
+ src_y++;
+ src_cb += 2;
+ src_cr += 2;
+ dst += 4;
+ }
+ }
+}
diff --git a/src/apps/qcam/format_converter.h b/src/apps/qcam/format_converter.h
new file mode 100644
index 00000000..37dbfae2
--- /dev/null
+++ b/src/apps/qcam/format_converter.h
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * format_convert.h - qcam - Convert buffer to RGB
+ */
+
+#pragma once
+
+#include <stddef.h>
+
+#include <QSize>
+
+#include <libcamera/pixel_format.h>
+
+class Image;
+class QImage;
+
+class FormatConverter
+{
+public:
+ int configure(const libcamera::PixelFormat &format, const QSize &size,
+ unsigned int stride);
+
+ void convert(const Image *src, size_t size, QImage *dst);
+
+private:
+ enum FormatFamily {
+ MJPEG,
+ RGB,
+ YUVPacked,
+ YUVPlanar,
+ YUVSemiPlanar,
+ };
+
+ void convertRGB(const Image *src, unsigned char *dst);
+ void convertYUVPacked(const Image *src, unsigned char *dst);
+ void convertYUVPlanar(const Image *src, unsigned char *dst);
+ void convertYUVSemiPlanar(const Image *src, unsigned char *dst);
+
+ libcamera::PixelFormat format_;
+ unsigned int width_;
+ unsigned int height_;
+ unsigned int stride_;
+
+ enum FormatFamily formatFamily_;
+
+ /* NV parameters */
+ unsigned int horzSubSample_;
+ unsigned int vertSubSample_;
+ bool nvSwap_;
+
+ /* RGB parameters */
+ unsigned int bpp_;
+ unsigned int r_pos_;
+ unsigned int g_pos_;
+ unsigned int b_pos_;
+
+ /* YUV parameters */
+ unsigned int y_pos_;
+ unsigned int cb_pos_;
+};
diff --git a/src/apps/qcam/main.cpp b/src/apps/qcam/main.cpp
new file mode 100644
index 00000000..d3f01a85
--- /dev/null
+++ b/src/apps/qcam/main.cpp
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * main.cpp - qcam - The libcamera GUI test application
+ */
+
+#include <signal.h>
+#include <string.h>
+
+#include <QApplication>
+#include <QtDebug>
+
+#include <libcamera/camera_manager.h>
+
+#include "../cam/options.h"
+#include "../cam/stream_options.h"
+#include "main_window.h"
+#include "message_handler.h"
+
+using namespace libcamera;
+
+void signalHandler([[maybe_unused]] int signal)
+{
+ qInfo() << "Exiting";
+ qApp->quit();
+}
+
+OptionsParser::Options parseOptions(int argc, char *argv[])
+{
+ StreamKeyValueParser streamKeyValue;
+
+ OptionsParser parser;
+ parser.addOption(OptCamera, OptionString,
+ "Specify which camera to operate on", "camera",
+ ArgumentRequired, "camera");
+ parser.addOption(OptHelp, OptionNone, "Display this help message",
+ "help");
+ parser.addOption(OptRenderer, OptionString,
+ "Choose the renderer type {qt,gles} (default: qt)",
+ "renderer", ArgumentRequired, "renderer");
+ parser.addOption(OptStream, &streamKeyValue,
+ "Set configuration of a camera stream", "stream", true);
+ parser.addOption(OptVerbose, OptionNone,
+ "Print verbose log messages", "verbose");
+
+ OptionsParser::Options options = parser.parse(argc, argv);
+ if (options.isSet(OptHelp))
+ parser.usage();
+
+ return options;
+}
+
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+ int ret;
+
+ OptionsParser::Options options = parseOptions(argc, argv);
+ if (!options.valid())
+ return EXIT_FAILURE;
+ if (options.isSet(OptHelp))
+ return 0;
+
+ MessageHandler msgHandler(options.isSet(OptVerbose));
+
+ struct sigaction sa = {};
+ sa.sa_handler = &signalHandler;
+ sigaction(SIGINT, &sa, nullptr);
+
+ CameraManager *cm = new libcamera::CameraManager();
+
+ ret = cm->start();
+ if (ret) {
+ qInfo() << "Failed to start camera manager:"
+ << strerror(-ret);
+ return EXIT_FAILURE;
+ }
+
+ MainWindow *mainWindow = new MainWindow(cm, options);
+ mainWindow->show();
+ ret = app.exec();
+ delete mainWindow;
+
+ cm->stop();
+ delete cm;
+
+ return ret;
+}
diff --git a/src/apps/qcam/main_window.cpp b/src/apps/qcam/main_window.cpp
new file mode 100644
index 00000000..f553ccb0
--- /dev/null
+++ b/src/apps/qcam/main_window.cpp
@@ -0,0 +1,790 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * main_window.cpp - qcam - Main application window
+ */
+
+#include "main_window.h"
+
+#include <assert.h>
+#include <iomanip>
+#include <string>
+
+#include <libcamera/camera_manager.h>
+#include <libcamera/version.h>
+
+#include <QCoreApplication>
+#include <QFileDialog>
+#include <QImage>
+#include <QImageWriter>
+#include <QMutexLocker>
+#include <QStandardPaths>
+#include <QStringList>
+#include <QTimer>
+#include <QToolBar>
+#include <QToolButton>
+#include <QtDebug>
+
+#include "../cam/dng_writer.h"
+#include "../cam/image.h"
+
+#include "cam_select_dialog.h"
+#ifndef QT_NO_OPENGL
+#include "viewfinder_gl.h"
+#endif
+#include "viewfinder_qt.h"
+
+using namespace libcamera;
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
+/*
+ * Qt::fixed was introduced in v5.14, and ::fixed deprecated in v5.15. Allow
+ * usage of Qt::fixed unconditionally.
+ */
+namespace Qt {
+constexpr auto fixed = ::fixed;
+} /* namespace Qt */
+#endif
+
+/**
+ * \brief Custom QEvent to signal capture completion
+ */
+class CaptureEvent : public QEvent
+{
+public:
+ CaptureEvent()
+ : QEvent(type())
+ {
+ }
+
+ static Type type()
+ {
+ static int type = QEvent::registerEventType();
+ return static_cast<Type>(type);
+ }
+};
+
+/**
+ * \brief Custom QEvent to signal hotplug or unplug
+ */
+class HotplugEvent : public QEvent
+{
+public:
+ enum PlugEvent {
+ HotPlug,
+ HotUnplug
+ };
+
+ HotplugEvent(std::shared_ptr<Camera> camera, PlugEvent event)
+ : QEvent(type()), camera_(std::move(camera)), plugEvent_(event)
+ {
+ }
+
+ static Type type()
+ {
+ static int type = QEvent::registerEventType();
+ return static_cast<Type>(type);
+ }
+
+ PlugEvent hotplugEvent() const { return plugEvent_; }
+ Camera *camera() const { return camera_.get(); }
+
+private:
+ std::shared_ptr<Camera> camera_;
+ PlugEvent plugEvent_;
+};
+
+MainWindow::MainWindow(CameraManager *cm, const OptionsParser::Options &options)
+ : saveRaw_(nullptr), options_(options), cm_(cm), allocator_(nullptr),
+ isCapturing_(false), captureRaw_(false)
+{
+ int ret;
+
+ /*
+ * Initialize the UI: Create the toolbar, set the window title and
+ * create the viewfinder widget.
+ */
+ createToolbars();
+
+ title_ = "QCam " + QString::fromStdString(CameraManager::version());
+ setWindowTitle(title_);
+ connect(&titleTimer_, SIGNAL(timeout()), this, SLOT(updateTitle()));
+
+ /* Renderer type Qt or GLES, select Qt by default. */
+ std::string renderType = "qt";
+ if (options_.isSet(OptRenderer))
+ renderType = options_[OptRenderer].toString();
+
+ if (renderType == "qt") {
+ ViewFinderQt *viewfinder = new ViewFinderQt(this);
+ connect(viewfinder, &ViewFinderQt::renderComplete,
+ this, &MainWindow::renderComplete);
+ viewfinder_ = viewfinder;
+ setCentralWidget(viewfinder);
+#ifndef QT_NO_OPENGL
+ } else if (renderType == "gles") {
+ ViewFinderGL *viewfinder = new ViewFinderGL(this);
+ connect(viewfinder, &ViewFinderGL::renderComplete,
+ this, &MainWindow::renderComplete);
+ viewfinder_ = viewfinder;
+ setCentralWidget(viewfinder);
+#endif
+ } else {
+ qWarning() << "Invalid render type"
+ << QString::fromStdString(renderType);
+ quit();
+ return;
+ }
+
+ adjustSize();
+
+ /* Hotplug/unplug support */
+ cm_->cameraAdded.connect(this, &MainWindow::addCamera);
+ cm_->cameraRemoved.connect(this, &MainWindow::removeCamera);
+
+ cameraSelectorDialog_ = new CameraSelectorDialog(cm_, this);
+
+ /* Open the camera and start capture. */
+ ret = openCamera();
+ if (ret < 0) {
+ quit();
+ return;
+ }
+
+ startStopAction_->setChecked(true);
+}
+
+MainWindow::~MainWindow()
+{
+ if (camera_) {
+ stopCapture();
+ camera_->release();
+ camera_.reset();
+ }
+}
+
+bool MainWindow::event(QEvent *e)
+{
+ if (e->type() == CaptureEvent::type()) {
+ processCapture();
+ return true;
+ } else if (e->type() == HotplugEvent::type()) {
+ processHotplug(static_cast<HotplugEvent *>(e));
+ return true;
+ }
+
+ return QMainWindow::event(e);
+}
+
+int MainWindow::createToolbars()
+{
+ QAction *action;
+
+ toolbar_ = addToolBar("Main");
+
+ /* Disable right click context menu. */
+ toolbar_->setContextMenuPolicy(Qt::PreventContextMenu);
+
+ /* Quit action. */
+ action = toolbar_->addAction(QIcon::fromTheme("application-exit",
+ QIcon(":x-circle.svg")),
+ "Quit");
+ action->setShortcut(Qt::CTRL | Qt::Key_Q);
+ connect(action, &QAction::triggered, this, &MainWindow::quit);
+
+ /* Camera selector. */
+ cameraSelectButton_ = new QPushButton;
+ connect(cameraSelectButton_, &QPushButton::clicked,
+ this, &MainWindow::switchCamera);
+
+ toolbar_->addWidget(cameraSelectButton_);
+
+ toolbar_->addSeparator();
+
+ /* Start/Stop action. */
+ iconPlay_ = QIcon::fromTheme("media-playback-start",
+ QIcon(":play-circle.svg"));
+ iconStop_ = QIcon::fromTheme("media-playback-stop",
+ QIcon(":stop-circle.svg"));
+
+ action = toolbar_->addAction(iconPlay_, "Start Capture");
+ action->setCheckable(true);
+ action->setShortcut(Qt::Key_Space);
+ connect(action, &QAction::toggled, this, &MainWindow::toggleCapture);
+ startStopAction_ = action;
+
+ /* Save As... action. */
+ action = toolbar_->addAction(QIcon::fromTheme("document-save-as",
+ QIcon(":save.svg")),
+ "Save As...");
+ action->setShortcut(QKeySequence::SaveAs);
+ connect(action, &QAction::triggered, this, &MainWindow::saveImageAs);
+
+#ifdef HAVE_DNG
+ /* Save Raw action. */
+ action = toolbar_->addAction(QIcon::fromTheme("camera-photo",
+ QIcon(":aperture.svg")),
+ "Save Raw");
+ action->setEnabled(false);
+ connect(action, &QAction::triggered, this, &MainWindow::captureRaw);
+ saveRaw_ = action;
+#endif
+
+ return 0;
+}
+
+void MainWindow::quit()
+{
+ QTimer::singleShot(0, QCoreApplication::instance(),
+ &QCoreApplication::quit);
+}
+
+void MainWindow::updateTitle()
+{
+ /* Calculate the average frame rate over the last period. */
+ unsigned int duration = frameRateInterval_.elapsed();
+ unsigned int frames = framesCaptured_ - previousFrames_;
+ double fps = frames * 1000.0 / duration;
+
+ /* Restart counters. */
+ frameRateInterval_.start();
+ previousFrames_ = framesCaptured_;
+
+ setWindowTitle(title_ + " : " + QString::number(fps, 'f', 2) + " fps");
+}
+
+/* -----------------------------------------------------------------------------
+ * Camera Selection
+ */
+
+void MainWindow::switchCamera()
+{
+ /* Get and acquire the new camera. */
+ std::string newCameraId = chooseCamera();
+
+ if (newCameraId.empty())
+ return;
+
+ if (camera_ && newCameraId == camera_->id())
+ return;
+
+ const std::shared_ptr<Camera> &cam = cm_->get(newCameraId);
+
+ if (cam->acquire()) {
+ qInfo() << "Failed to acquire camera" << cam->id().c_str();
+ return;
+ }
+
+ qInfo() << "Switching to camera" << cam->id().c_str();
+
+ /*
+ * Stop the capture session, release the current camera, replace it with
+ * the new camera and start a new capture session.
+ */
+ startStopAction_->setChecked(false);
+
+ if (camera_)
+ camera_->release();
+
+ camera_ = cam;
+
+ startStopAction_->setChecked(true);
+
+ /* Display the current cameraId in the toolbar .*/
+ cameraSelectButton_->setText(QString::fromStdString(newCameraId));
+}
+
+std::string MainWindow::chooseCamera()
+{
+ if (cameraSelectorDialog_->exec() != QDialog::Accepted)
+ return std::string();
+
+ return cameraSelectorDialog_->getCameraId();
+}
+
+int MainWindow::openCamera()
+{
+ std::string cameraName;
+
+ /*
+ * Use the camera specified on the command line, if any, or display the
+ * camera selection dialog box otherwise.
+ */
+ if (options_.isSet(OptCamera))
+ cameraName = static_cast<std::string>(options_[OptCamera]);
+ else
+ cameraName = chooseCamera();
+
+ if (cameraName == "")
+ return -EINVAL;
+
+ /* Get and acquire the camera. */
+ camera_ = cm_->get(cameraName);
+ if (!camera_) {
+ qInfo() << "Camera" << cameraName.c_str() << "not found";
+ return -ENODEV;
+ }
+
+ if (camera_->acquire()) {
+ qInfo() << "Failed to acquire camera";
+ camera_.reset();
+ return -EBUSY;
+ }
+
+ /* Set the camera switch button with the currently selected Camera id. */
+ cameraSelectButton_->setText(QString::fromStdString(cameraName));
+
+ return 0;
+}
+
+/* -----------------------------------------------------------------------------
+ * Capture Start & Stop
+ */
+
+void MainWindow::toggleCapture(bool start)
+{
+ if (start) {
+ startCapture();
+ startStopAction_->setIcon(iconStop_);
+ startStopAction_->setText("Stop Capture");
+ } else {
+ stopCapture();
+ startStopAction_->setIcon(iconPlay_);
+ startStopAction_->setText("Start Capture");
+ }
+}
+
+/**
+ * \brief Start capture with the current camera
+ *
+ * This function shall not be called directly, use toggleCapture() instead.
+ */
+int MainWindow::startCapture()
+{
+ StreamRoles roles = StreamKeyValueParser::roles(options_[OptStream]);
+ int ret;
+
+ /* Verify roles are supported. */
+ switch (roles.size()) {
+ case 1:
+ if (roles[0] != StreamRole::Viewfinder) {
+ qWarning() << "Only viewfinder supported for single stream";
+ return -EINVAL;
+ }
+ break;
+ case 2:
+ if (roles[0] != StreamRole::Viewfinder ||
+ roles[1] != StreamRole::Raw) {
+ qWarning() << "Only viewfinder + raw supported for dual streams";
+ return -EINVAL;
+ }
+ break;
+ default:
+ if (roles.size() != 1) {
+ qWarning() << "Unsupported stream configuration";
+ return -EINVAL;
+ }
+ break;
+ }
+
+ /* Configure the camera. */
+ config_ = camera_->generateConfiguration(roles);
+ if (!config_) {
+ qWarning() << "Failed to generate configuration from roles";
+ return -EINVAL;
+ }
+
+ StreamConfiguration &vfConfig = config_->at(0);
+
+ /* Use a format supported by the viewfinder if available. */
+ std::vector<PixelFormat> formats = vfConfig.formats().pixelformats();
+ for (const PixelFormat &format : viewfinder_->nativeFormats()) {
+ auto match = std::find_if(formats.begin(), formats.end(),
+ [&](const PixelFormat &f) {
+ return f == format;
+ });
+ if (match != formats.end()) {
+ vfConfig.pixelFormat = format;
+ break;
+ }
+ }
+
+ /* Allow user to override configuration. */
+ if (StreamKeyValueParser::updateConfiguration(config_.get(),
+ options_[OptStream])) {
+ qWarning() << "Failed to update configuration";
+ return -EINVAL;
+ }
+
+ CameraConfiguration::Status validation = config_->validate();
+ if (validation == CameraConfiguration::Invalid) {
+ qWarning() << "Failed to create valid camera configuration";
+ return -EINVAL;
+ }
+
+ if (validation == CameraConfiguration::Adjusted)
+ qInfo() << "Stream configuration adjusted to "
+ << vfConfig.toString().c_str();
+
+ ret = camera_->configure(config_.get());
+ if (ret < 0) {
+ qInfo() << "Failed to configure camera";
+ return ret;
+ }
+
+ /* Store stream allocation. */
+ vfStream_ = config_->at(0).stream();
+ if (config_->size() == 2)
+ rawStream_ = config_->at(1).stream();
+ else
+ rawStream_ = nullptr;
+
+ /*
+ * Configure the viewfinder. If no color space is reported, default to
+ * sYCC.
+ */
+ ret = viewfinder_->setFormat(vfConfig.pixelFormat,
+ QSize(vfConfig.size.width, vfConfig.size.height),
+ vfConfig.colorSpace.value_or(ColorSpace::Sycc),
+ vfConfig.stride);
+ if (ret < 0) {
+ qInfo() << "Failed to set viewfinder format";
+ return ret;
+ }
+
+ adjustSize();
+
+ /* Configure the raw capture button. */
+ if (saveRaw_)
+ saveRaw_->setEnabled(config_->size() == 2);
+
+ /* Allocate and map buffers. */
+ allocator_ = new FrameBufferAllocator(camera_);
+ for (StreamConfiguration &config : *config_) {
+ Stream *stream = config.stream();
+
+ ret = allocator_->allocate(stream);
+ if (ret < 0) {
+ qWarning() << "Failed to allocate capture buffers";
+ goto error;
+ }
+
+ for (const std::unique_ptr<FrameBuffer> &buffer : allocator_->buffers(stream)) {
+ /* Map memory buffers and cache the mappings. */
+ std::unique_ptr<Image> image =
+ Image::fromFrameBuffer(buffer.get(), Image::MapMode::ReadOnly);
+ assert(image != nullptr);
+ mappedBuffers_[buffer.get()] = std::move(image);
+
+ /* Store buffers on the free list. */
+ freeBuffers_[stream].enqueue(buffer.get());
+ }
+ }
+
+ /* Create requests and fill them with buffers from the viewfinder. */
+ while (!freeBuffers_[vfStream_].isEmpty()) {
+ FrameBuffer *buffer = freeBuffers_[vfStream_].dequeue();
+
+ std::unique_ptr<Request> request = camera_->createRequest();
+ if (!request) {
+ qWarning() << "Can't create request";
+ ret = -ENOMEM;
+ goto error;
+ }
+
+ ret = request->addBuffer(vfStream_, buffer);
+ if (ret < 0) {
+ qWarning() << "Can't set buffer for request";
+ goto error;
+ }
+
+ requests_.push_back(std::move(request));
+ }
+
+ /* Start the title timer and the camera. */
+ titleTimer_.start(2000);
+ frameRateInterval_.start();
+ previousFrames_ = 0;
+ framesCaptured_ = 0;
+ lastBufferTime_ = 0;
+
+ ret = camera_->start();
+ if (ret) {
+ qInfo() << "Failed to start capture";
+ goto error;
+ }
+
+ camera_->requestCompleted.connect(this, &MainWindow::requestComplete);
+
+ /* Queue all requests. */
+ for (std::unique_ptr<Request> &request : requests_) {
+ ret = queueRequest(request.get());
+ if (ret < 0) {
+ qWarning() << "Can't queue request";
+ goto error_disconnect;
+ }
+ }
+
+ isCapturing_ = true;
+
+ return 0;
+
+error_disconnect:
+ camera_->requestCompleted.disconnect(this);
+ camera_->stop();
+
+error:
+ requests_.clear();
+
+ mappedBuffers_.clear();
+
+ freeBuffers_.clear();
+
+ delete allocator_;
+ allocator_ = nullptr;
+
+ return ret;
+}
+
+/**
+ * \brief Stop ongoing capture
+ *
+ * This function may be called directly when tearing down the MainWindow. Use
+ * toggleCapture() instead in all other cases.
+ */
+void MainWindow::stopCapture()
+{
+ if (!isCapturing_)
+ return;
+
+ viewfinder_->stop();
+ if (saveRaw_)
+ saveRaw_->setEnabled(false);
+ captureRaw_ = false;
+
+ int ret = camera_->stop();
+ if (ret)
+ qInfo() << "Failed to stop capture";
+
+ camera_->requestCompleted.disconnect(this);
+
+ mappedBuffers_.clear();
+
+ requests_.clear();
+ freeQueue_.clear();
+
+ delete allocator_;
+
+ isCapturing_ = false;
+
+ config_.reset();
+
+ /*
+ * A CaptureEvent may have been posted before we stopped the camera,
+ * but not processed yet. Clear the queue of done buffers to avoid
+ * racing with the event handler.
+ */
+ freeBuffers_.clear();
+ doneQueue_.clear();
+
+ titleTimer_.stop();
+ setWindowTitle(title_);
+}
+
+/* -----------------------------------------------------------------------------
+ * Camera hotplugging support
+ */
+
+void MainWindow::processHotplug(HotplugEvent *e)
+{
+ Camera *camera = e->camera();
+ QString cameraId = QString::fromStdString(camera->id());
+ HotplugEvent::PlugEvent event = e->hotplugEvent();
+
+ if (event == HotplugEvent::HotPlug) {
+ cameraSelectorDialog_->addCamera(cameraId);
+ } else if (event == HotplugEvent::HotUnplug) {
+ /* Check if the currently-streaming camera is removed. */
+ if (camera == camera_.get()) {
+ toggleCapture(false);
+ camera_->release();
+ camera_.reset();
+ }
+
+ cameraSelectorDialog_->removeCamera(cameraId);
+ }
+}
+
+void MainWindow::addCamera(std::shared_ptr<Camera> camera)
+{
+ qInfo() << "Adding new camera:" << camera->id().c_str();
+ QCoreApplication::postEvent(this,
+ new HotplugEvent(std::move(camera),
+ HotplugEvent::HotPlug));
+}
+
+void MainWindow::removeCamera(std::shared_ptr<Camera> camera)
+{
+ qInfo() << "Removing camera:" << camera->id().c_str();
+ QCoreApplication::postEvent(this,
+ new HotplugEvent(std::move(camera),
+ HotplugEvent::HotUnplug));
+}
+
+/* -----------------------------------------------------------------------------
+ * Image Save
+ */
+
+void MainWindow::saveImageAs()
+{
+ QImage image = viewfinder_->getCurrentImage();
+ QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
+
+ QString filename = QFileDialog::getSaveFileName(this, "Save Image", defaultPath,
+ "Image Files (*.png *.jpg *.jpeg)");
+ if (filename.isEmpty())
+ return;
+
+ QImageWriter writer(filename);
+ writer.setQuality(95);
+ writer.write(image);
+}
+
+void MainWindow::captureRaw()
+{
+ captureRaw_ = true;
+}
+
+void MainWindow::processRaw(FrameBuffer *buffer,
+ [[maybe_unused]] const ControlList &metadata)
+{
+#ifdef HAVE_DNG
+ QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
+ QString filename = QFileDialog::getSaveFileName(this, "Save DNG", defaultPath,
+ "DNG Files (*.dng)");
+
+ if (!filename.isEmpty()) {
+ uint8_t *memory = mappedBuffers_[buffer]->data(0).data();
+ DNGWriter::write(filename.toStdString().c_str(), camera_.get(),
+ rawStream_->configuration(), metadata, buffer,
+ memory);
+ }
+#endif
+
+ {
+ QMutexLocker locker(&mutex_);
+ freeBuffers_[rawStream_].enqueue(buffer);
+ }
+}
+
+/* -----------------------------------------------------------------------------
+ * Request Completion Handling
+ */
+
+void MainWindow::requestComplete(Request *request)
+{
+ if (request->status() == Request::RequestCancelled)
+ return;
+
+ /*
+ * We're running in the libcamera thread context, expensive operations
+ * are not allowed. Add the buffer to the done queue and post a
+ * CaptureEvent for the application thread to handle.
+ */
+ {
+ QMutexLocker locker(&mutex_);
+ doneQueue_.enqueue(request);
+ }
+
+ QCoreApplication::postEvent(this, new CaptureEvent);
+}
+
+void MainWindow::processCapture()
+{
+ /*
+ * Retrieve the next buffer from the done queue. The queue may be empty
+ * if stopCapture() has been called while a CaptureEvent was posted but
+ * not processed yet. Return immediately in that case.
+ */
+ Request *request;
+ {
+ QMutexLocker locker(&mutex_);
+ if (doneQueue_.isEmpty())
+ return;
+
+ request = doneQueue_.dequeue();
+ }
+
+ /* Process buffers. */
+ if (request->buffers().count(vfStream_))
+ processViewfinder(request->buffers().at(vfStream_));
+
+ if (request->buffers().count(rawStream_))
+ processRaw(request->buffers().at(rawStream_), request->metadata());
+
+ request->reuse();
+ QMutexLocker locker(&mutex_);
+ freeQueue_.enqueue(request);
+}
+
+void MainWindow::processViewfinder(FrameBuffer *buffer)
+{
+ framesCaptured_++;
+
+ const FrameMetadata &metadata = buffer->metadata();
+
+ double fps = metadata.timestamp - lastBufferTime_;
+ fps = lastBufferTime_ && fps ? 1000000000.0 / fps : 0.0;
+ lastBufferTime_ = metadata.timestamp;
+
+ QStringList bytesused;
+ for (const FrameMetadata::Plane &plane : metadata.planes())
+ bytesused << QString::number(plane.bytesused);
+
+ qDebug().noquote()
+ << QString("seq: %1").arg(metadata.sequence, 6, 10, QLatin1Char('0'))
+ << "bytesused: {" << bytesused.join(", ")
+ << "} timestamp:" << metadata.timestamp
+ << "fps:" << Qt::fixed << qSetRealNumberPrecision(2) << fps;
+
+ /* Render the frame on the viewfinder. */
+ viewfinder_->render(buffer, mappedBuffers_[buffer].get());
+}
+
+void MainWindow::renderComplete(FrameBuffer *buffer)
+{
+ Request *request;
+ {
+ QMutexLocker locker(&mutex_);
+ if (freeQueue_.isEmpty())
+ return;
+
+ request = freeQueue_.dequeue();
+ }
+
+ request->addBuffer(vfStream_, buffer);
+
+ if (captureRaw_) {
+ FrameBuffer *rawBuffer = nullptr;
+
+ {
+ QMutexLocker locker(&mutex_);
+ if (!freeBuffers_[rawStream_].isEmpty())
+ rawBuffer = freeBuffers_[rawStream_].dequeue();
+ }
+
+ if (rawBuffer) {
+ request->addBuffer(rawStream_, rawBuffer);
+ captureRaw_ = false;
+ } else {
+ qWarning() << "No free buffer available for RAW capture";
+ }
+ }
+ queueRequest(request);
+}
+
+int MainWindow::queueRequest(Request *request)
+{
+ return camera_->queueRequest(request);
+}
diff --git a/src/apps/qcam/main_window.h b/src/apps/qcam/main_window.h
new file mode 100644
index 00000000..95b64124
--- /dev/null
+++ b/src/apps/qcam/main_window.h
@@ -0,0 +1,133 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * main_window.h - qcam - Main application window
+ */
+
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include <libcamera/camera.h>
+#include <libcamera/camera_manager.h>
+#include <libcamera/controls.h>
+#include <libcamera/framebuffer.h>
+#include <libcamera/framebuffer_allocator.h>
+#include <libcamera/request.h>
+#include <libcamera/stream.h>
+
+#include <QElapsedTimer>
+#include <QIcon>
+#include <QMainWindow>
+#include <QMutex>
+#include <QObject>
+#include <QPushButton>
+#include <QQueue>
+#include <QTimer>
+
+#include "../cam/stream_options.h"
+
+#include "viewfinder.h"
+
+class QAction;
+
+class CameraSelectorDialog;
+class Image;
+class HotplugEvent;
+
+enum {
+ OptCamera = 'c',
+ OptHelp = 'h',
+ OptRenderer = 'r',
+ OptStream = 's',
+ OptVerbose = 'v',
+};
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ MainWindow(libcamera::CameraManager *cm,
+ const OptionsParser::Options &options);
+ ~MainWindow();
+
+ bool event(QEvent *e) override;
+
+private Q_SLOTS:
+ void quit();
+ void updateTitle();
+
+ void switchCamera();
+ void toggleCapture(bool start);
+
+ void saveImageAs();
+ void captureRaw();
+ void processRaw(libcamera::FrameBuffer *buffer,
+ const libcamera::ControlList &metadata);
+
+ void renderComplete(libcamera::FrameBuffer *buffer);
+
+private:
+ int createToolbars();
+
+ std::string chooseCamera();
+ int openCamera();
+
+ int startCapture();
+ void stopCapture();
+
+ void addCamera(std::shared_ptr<libcamera::Camera> camera);
+ void removeCamera(std::shared_ptr<libcamera::Camera> camera);
+
+ int queueRequest(libcamera::Request *request);
+ void requestComplete(libcamera::Request *request);
+ void processCapture();
+ void processHotplug(HotplugEvent *e);
+ void processViewfinder(libcamera::FrameBuffer *buffer);
+
+ /* UI elements */
+ QToolBar *toolbar_;
+ QAction *startStopAction_;
+ QPushButton *cameraSelectButton_;
+ QAction *saveRaw_;
+ ViewFinder *viewfinder_;
+
+ QIcon iconPlay_;
+ QIcon iconStop_;
+
+ QString title_;
+ QTimer titleTimer_;
+
+ CameraSelectorDialog *cameraSelectorDialog_;
+
+ /* Options */
+ const OptionsParser::Options &options_;
+
+ /* Camera manager, camera, configuration and buffers */
+ libcamera::CameraManager *cm_;
+ std::shared_ptr<libcamera::Camera> camera_;
+ libcamera::FrameBufferAllocator *allocator_;
+
+ std::unique_ptr<libcamera::CameraConfiguration> config_;
+ std::map<libcamera::FrameBuffer *, std::unique_ptr<Image>> mappedBuffers_;
+
+ /* Capture state, buffers queue and statistics */
+ bool isCapturing_;
+ bool captureRaw_;
+ libcamera::Stream *vfStream_;
+ libcamera::Stream *rawStream_;
+ std::map<const libcamera::Stream *, QQueue<libcamera::FrameBuffer *>> freeBuffers_;
+ QQueue<libcamera::Request *> doneQueue_;
+ QQueue<libcamera::Request *> freeQueue_;
+ QMutex mutex_; /* Protects freeBuffers_, doneQueue_, and freeQueue_ */
+
+ uint64_t lastBufferTime_;
+ QElapsedTimer frameRateInterval_;
+ uint32_t previousFrames_;
+ uint32_t framesCaptured_;
+
+ std::vector<std::unique_ptr<libcamera::Request>> requests_;
+};
diff --git a/src/apps/qcam/meson.build b/src/apps/qcam/meson.build
new file mode 100644
index 00000000..d5916d0d
--- /dev/null
+++ b/src/apps/qcam/meson.build
@@ -0,0 +1,83 @@
+# SPDX-License-Identifier: CC0-1.0
+
+qt5 = import('qt5')
+qt5_dep = dependency('qt5',
+ method : 'pkg-config',
+ modules : ['Core', 'Gui', 'Widgets'],
+ required : get_option('qcam'),
+ version : '>=5.4')
+
+if not qt5_dep.found()
+ qcam_enabled = false
+ subdir_done()
+endif
+
+qcam_enabled = true
+
+qcam_sources = files([
+ '../cam/image.cpp',
+ '../cam/options.cpp',
+ '../cam/stream_options.cpp',
+ 'cam_select_dialog.cpp',
+ 'format_converter.cpp',
+ 'main.cpp',
+ 'main_window.cpp',
+ 'message_handler.cpp',
+ 'viewfinder_qt.cpp',
+])
+
+qcam_moc_headers = files([
+ 'cam_select_dialog.h',
+ 'main_window.h',
+ 'viewfinder_qt.h',
+])
+
+qcam_resources = files([
+ 'assets/feathericons/feathericons.qrc',
+])
+
+qt5_cpp_args = ['-DQT_NO_KEYWORDS']
+
+tiff_dep = dependency('libtiff-4', required : false)
+if tiff_dep.found()
+ qt5_cpp_args += ['-DHAVE_TIFF']
+ qcam_sources += files([
+ '../cam/dng_writer.cpp',
+ ])
+endif
+
+if cxx.has_header_symbol('QOpenGLWidget', 'QOpenGLWidget',
+ dependencies : qt5_dep, args : '-fPIC')
+ qcam_sources += files([
+ 'viewfinder_gl.cpp',
+ ])
+ qcam_moc_headers += files([
+ 'viewfinder_gl.h',
+ ])
+ qcam_resources += files([
+ 'assets/shader/shaders.qrc'
+ ])
+endif
+
+# gcc 9 introduced a deprecated-copy warning that is triggered by Qt until
+# Qt 5.13. clang 10 introduced the same warning, but detects more issues
+# that are not fixed in Qt yet. Disable the warning manually in both cases.
+if ((cc.get_id() == 'gcc' and cc.version().version_compare('>=9.0') and
+ qt5_dep.version().version_compare('<5.13')) or
+ (cc.get_id() == 'clang' and cc.version().version_compare('>=10.0')))
+ qt5_cpp_args += ['-Wno-deprecated-copy']
+endif
+
+resources = qt5.preprocess(moc_headers: qcam_moc_headers,
+ qresources : qcam_resources,
+ dependencies: qt5_dep)
+
+qcam = executable('qcam', qcam_sources, resources,
+ install : true,
+ dependencies : [
+ libatomic,
+ libcamera_public,
+ qt5_dep,
+ tiff_dep,
+ ],
+ cpp_args : qt5_cpp_args)
diff --git a/src/apps/qcam/message_handler.cpp b/src/apps/qcam/message_handler.cpp
new file mode 100644
index 00000000..261623e1
--- /dev/null
+++ b/src/apps/qcam/message_handler.cpp
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ *
+ * message_handler.cpp - qcam - Log message handling
+ */
+
+#include "message_handler.h"
+
+QtMessageHandler MessageHandler::handler_ = nullptr;
+bool MessageHandler::verbose_ = false;
+
+MessageHandler::MessageHandler(bool verbose)
+{
+ verbose_ = verbose;
+ handler_ = qInstallMessageHandler(&MessageHandler::handleMessage);
+}
+
+void MessageHandler::handleMessage(QtMsgType type,
+ const QMessageLogContext &context,
+ const QString &msg)
+{
+ if (type == QtDebugMsg && !verbose_)
+ return;
+
+ handler_(type, context, msg);
+}
diff --git a/src/apps/qcam/message_handler.h b/src/apps/qcam/message_handler.h
new file mode 100644
index 00000000..56294d37
--- /dev/null
+++ b/src/apps/qcam/message_handler.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+ *
+ * message_handler.cpp - qcam - Log message handling
+ */
+
+#pragma once
+
+#include <QtGlobal>
+
+class MessageHandler
+{
+public:
+ MessageHandler(bool verbose);
+
+private:
+ static void handleMessage(QtMsgType type,
+ const QMessageLogContext &context,
+ const QString &msg);
+
+ static QtMessageHandler handler_;
+ static bool verbose_;
+};
diff --git a/src/apps/qcam/viewfinder.h b/src/apps/qcam/viewfinder.h
new file mode 100644
index 00000000..a57446e8
--- /dev/null
+++ b/src/apps/qcam/viewfinder.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * viewfinder.h - qcam - Viewfinder base class
+ */
+
+#pragma once
+
+#include <QImage>
+#include <QList>
+#include <QSize>
+
+#include <libcamera/color_space.h>
+#include <libcamera/formats.h>
+#include <libcamera/framebuffer.h>
+
+class Image;
+
+class ViewFinder
+{
+public:
+ virtual ~ViewFinder() = default;
+
+ virtual const QList<libcamera::PixelFormat> &nativeFormats() const = 0;
+
+ virtual int setFormat(const libcamera::PixelFormat &format, const QSize &size,
+ const libcamera::ColorSpace &colorSpace,
+ unsigned int stride) = 0;
+ virtual void render(libcamera::FrameBuffer *buffer, Image *image) = 0;
+ virtual void stop() = 0;
+
+ virtual QImage getCurrentImage() = 0;
+};
diff --git a/src/apps/qcam/viewfinder_gl.cpp b/src/apps/qcam/viewfinder_gl.cpp
new file mode 100644
index 00000000..38ddad58
--- /dev/null
+++ b/src/apps/qcam/viewfinder_gl.cpp
@@ -0,0 +1,835 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Linaro
+ *
+ * viewfinderGL.cpp - OpenGL Viewfinder for rendering by OpenGL shader
+ */
+
+#include "viewfinder_gl.h"
+
+#include <array>
+
+#include <QByteArray>
+#include <QFile>
+#include <QImage>
+#include <QStringList>
+
+#include <libcamera/formats.h>
+
+#include "../cam/image.h"
+
+static const QList<libcamera::PixelFormat> supportedFormats{
+ /* YUV - packed (single plane) */
+ libcamera::formats::UYVY,
+ libcamera::formats::VYUY,
+ libcamera::formats::YUYV,
+ libcamera::formats::YVYU,
+ /* YUV - semi planar (two planes) */
+ libcamera::formats::NV12,
+ libcamera::formats::NV21,
+ libcamera::formats::NV16,
+ libcamera::formats::NV61,
+ libcamera::formats::NV24,
+ libcamera::formats::NV42,
+ /* YUV - fully planar (three planes) */
+ libcamera::formats::YUV420,
+ libcamera::formats::YVU420,
+ /* RGB */
+ libcamera::formats::ABGR8888,
+ libcamera::formats::ARGB8888,
+ libcamera::formats::BGRA8888,
+ libcamera::formats::RGBA8888,
+ libcamera::formats::BGR888,
+ libcamera::formats::RGB888,
+ /* Raw Bayer 8-bit */
+ libcamera::formats::SBGGR8,
+ libcamera::formats::SGBRG8,
+ libcamera::formats::SGRBG8,
+ libcamera::formats::SRGGB8,
+ /* Raw Bayer 10-bit packed */
+ libcamera::formats::SBGGR10_CSI2P,
+ libcamera::formats::SGBRG10_CSI2P,
+ libcamera::formats::SGRBG10_CSI2P,
+ libcamera::formats::SRGGB10_CSI2P,
+ /* Raw Bayer 12-bit packed */
+ libcamera::formats::SBGGR12_CSI2P,
+ libcamera::formats::SGBRG12_CSI2P,
+ libcamera::formats::SGRBG12_CSI2P,
+ libcamera::formats::SRGGB12_CSI2P,
+};
+
+ViewFinderGL::ViewFinderGL(QWidget *parent)
+ : QOpenGLWidget(parent), buffer_(nullptr),
+ colorSpace_(libcamera::ColorSpace::Raw), image_(nullptr),
+ vertexBuffer_(QOpenGLBuffer::VertexBuffer)
+{
+}
+
+ViewFinderGL::~ViewFinderGL()
+{
+ removeShader();
+}
+
+const QList<libcamera::PixelFormat> &ViewFinderGL::nativeFormats() const
+{
+ return supportedFormats;
+}
+
+int ViewFinderGL::setFormat(const libcamera::PixelFormat &format, const QSize &size,
+ const libcamera::ColorSpace &colorSpace,
+ unsigned int stride)
+{
+ if (format != format_ || colorSpace != colorSpace_) {
+ /*
+ * If the fragment already exists, remove it and create a new
+ * one for the new format.
+ */
+ if (shaderProgram_.isLinked()) {
+ shaderProgram_.release();
+ shaderProgram_.removeShader(fragmentShader_.get());
+ fragmentShader_.reset();
+ }
+
+ if (!selectFormat(format))
+ return -1;
+
+ selectColorSpace(colorSpace);
+
+ format_ = format;
+ colorSpace_ = colorSpace;
+ }
+
+ size_ = size;
+ stride_ = stride;
+
+ updateGeometry();
+ return 0;
+}
+
+void ViewFinderGL::stop()
+{
+ if (buffer_) {
+ renderComplete(buffer_);
+ buffer_ = nullptr;
+ image_ = nullptr;
+ }
+}
+
+QImage ViewFinderGL::getCurrentImage()
+{
+ QMutexLocker locker(&mutex_);
+
+ return grabFramebuffer();
+}
+
+void ViewFinderGL::render(libcamera::FrameBuffer *buffer, Image *image)
+{
+ if (buffer_)
+ renderComplete(buffer_);
+
+ image_ = image;
+ update();
+ buffer_ = buffer;
+}
+
+bool ViewFinderGL::selectFormat(const libcamera::PixelFormat &format)
+{
+ bool ret = true;
+
+ /* Set min/mag filters to GL_LINEAR by default. */
+ textureMinMagFilters_ = GL_LINEAR;
+
+ /* Use identity.vert as the default vertex shader. */
+ vertexShaderFile_ = ":identity.vert";
+
+ fragmentShaderDefines_.clear();
+
+ switch (format) {
+ case libcamera::formats::NV12:
+ horzSubSample_ = 2;
+ vertSubSample_ = 2;
+ fragmentShaderDefines_.append("#define YUV_PATTERN_UV");
+ fragmentShaderFile_ = ":YUV_2_planes.frag";
+ break;
+ case libcamera::formats::NV21:
+ horzSubSample_ = 2;
+ vertSubSample_ = 2;
+ fragmentShaderDefines_.append("#define YUV_PATTERN_VU");
+ fragmentShaderFile_ = ":YUV_2_planes.frag";
+ break;
+ case libcamera::formats::NV16:
+ horzSubSample_ = 2;
+ vertSubSample_ = 1;
+ fragmentShaderDefines_.append("#define YUV_PATTERN_UV");
+ fragmentShaderFile_ = ":YUV_2_planes.frag";
+ break;
+ case libcamera::formats::NV61:
+ horzSubSample_ = 2;
+ vertSubSample_ = 1;
+ fragmentShaderDefines_.append("#define YUV_PATTERN_VU");
+ fragmentShaderFile_ = ":YUV_2_planes.frag";
+ break;
+ case libcamera::formats::NV24:
+ horzSubSample_ = 1;
+ vertSubSample_ = 1;
+ fragmentShaderDefines_.append("#define YUV_PATTERN_UV");
+ fragmentShaderFile_ = ":YUV_2_planes.frag";
+ break;
+ case libcamera::formats::NV42:
+ horzSubSample_ = 1;
+ vertSubSample_ = 1;
+ fragmentShaderDefines_.append("#define YUV_PATTERN_VU");
+ fragmentShaderFile_ = ":YUV_2_planes.frag";
+ break;
+ case libcamera::formats::YUV420:
+ horzSubSample_ = 2;
+ vertSubSample_ = 2;
+ fragmentShaderFile_ = ":YUV_3_planes.frag";
+ break;
+ case libcamera::formats::YVU420:
+ horzSubSample_ = 2;
+ vertSubSample_ = 2;
+ fragmentShaderFile_ = ":YUV_3_planes.frag";
+ break;
+ case libcamera::formats::UYVY:
+ fragmentShaderDefines_.append("#define YUV_PATTERN_UYVY");
+ fragmentShaderFile_ = ":YUV_packed.frag";
+ break;
+ case libcamera::formats::VYUY:
+ fragmentShaderDefines_.append("#define YUV_PATTERN_VYUY");
+ fragmentShaderFile_ = ":YUV_packed.frag";
+ break;
+ case libcamera::formats::YUYV:
+ fragmentShaderDefines_.append("#define YUV_PATTERN_YUYV");
+ fragmentShaderFile_ = ":YUV_packed.frag";
+ break;
+ case libcamera::formats::YVYU:
+ fragmentShaderDefines_.append("#define YUV_PATTERN_YVYU");
+ fragmentShaderFile_ = ":YUV_packed.frag";
+ break;
+ case libcamera::formats::ABGR8888:
+ fragmentShaderDefines_.append("#define RGB_PATTERN rgb");
+ fragmentShaderFile_ = ":RGB.frag";
+ break;
+ case libcamera::formats::ARGB8888:
+ fragmentShaderDefines_.append("#define RGB_PATTERN bgr");
+ fragmentShaderFile_ = ":RGB.frag";
+ break;
+ case libcamera::formats::BGRA8888:
+ fragmentShaderDefines_.append("#define RGB_PATTERN gba");
+ fragmentShaderFile_ = ":RGB.frag";
+ break;
+ case libcamera::formats::RGBA8888:
+ fragmentShaderDefines_.append("#define RGB_PATTERN abg");
+ fragmentShaderFile_ = ":RGB.frag";
+ break;
+ case libcamera::formats::BGR888:
+ fragmentShaderDefines_.append("#define RGB_PATTERN rgb");
+ fragmentShaderFile_ = ":RGB.frag";
+ break;
+ case libcamera::formats::RGB888:
+ fragmentShaderDefines_.append("#define RGB_PATTERN bgr");
+ fragmentShaderFile_ = ":RGB.frag";
+ break;
+ case libcamera::formats::SBGGR8:
+ firstRed_.setX(1.0);
+ firstRed_.setY(1.0);
+ vertexShaderFile_ = ":bayer_8.vert";
+ fragmentShaderFile_ = ":bayer_8.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SGBRG8:
+ firstRed_.setX(0.0);
+ firstRed_.setY(1.0);
+ vertexShaderFile_ = ":bayer_8.vert";
+ fragmentShaderFile_ = ":bayer_8.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SGRBG8:
+ firstRed_.setX(1.0);
+ firstRed_.setY(0.0);
+ vertexShaderFile_ = ":bayer_8.vert";
+ fragmentShaderFile_ = ":bayer_8.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SRGGB8:
+ firstRed_.setX(0.0);
+ firstRed_.setY(0.0);
+ vertexShaderFile_ = ":bayer_8.vert";
+ fragmentShaderFile_ = ":bayer_8.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SBGGR10_CSI2P:
+ firstRed_.setX(1.0);
+ firstRed_.setY(1.0);
+ fragmentShaderDefines_.append("#define RAW10P");
+ fragmentShaderFile_ = ":bayer_1x_packed.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SGBRG10_CSI2P:
+ firstRed_.setX(0.0);
+ firstRed_.setY(1.0);
+ fragmentShaderDefines_.append("#define RAW10P");
+ fragmentShaderFile_ = ":bayer_1x_packed.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SGRBG10_CSI2P:
+ firstRed_.setX(1.0);
+ firstRed_.setY(0.0);
+ fragmentShaderDefines_.append("#define RAW10P");
+ fragmentShaderFile_ = ":bayer_1x_packed.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SRGGB10_CSI2P:
+ firstRed_.setX(0.0);
+ firstRed_.setY(0.0);
+ fragmentShaderDefines_.append("#define RAW10P");
+ fragmentShaderFile_ = ":bayer_1x_packed.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SBGGR12_CSI2P:
+ firstRed_.setX(1.0);
+ firstRed_.setY(1.0);
+ fragmentShaderDefines_.append("#define RAW12P");
+ fragmentShaderFile_ = ":bayer_1x_packed.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SGBRG12_CSI2P:
+ firstRed_.setX(0.0);
+ firstRed_.setY(1.0);
+ fragmentShaderDefines_.append("#define RAW12P");
+ fragmentShaderFile_ = ":bayer_1x_packed.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SGRBG12_CSI2P:
+ firstRed_.setX(1.0);
+ firstRed_.setY(0.0);
+ fragmentShaderDefines_.append("#define RAW12P");
+ fragmentShaderFile_ = ":bayer_1x_packed.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ case libcamera::formats::SRGGB12_CSI2P:
+ firstRed_.setX(0.0);
+ firstRed_.setY(0.0);
+ fragmentShaderDefines_.append("#define RAW12P");
+ fragmentShaderFile_ = ":bayer_1x_packed.frag";
+ textureMinMagFilters_ = GL_NEAREST;
+ break;
+ default:
+ ret = false;
+ qWarning() << "[ViewFinderGL]:"
+ << "format not supported.";
+ break;
+ };
+
+ return ret;
+}
+
+void ViewFinderGL::selectColorSpace(const libcamera::ColorSpace &colorSpace)
+{
+ std::array<double, 9> yuv2rgb;
+
+ /* OpenGL stores arrays in column-major order. */
+ switch (colorSpace.ycbcrEncoding) {
+ case libcamera::ColorSpace::YcbcrEncoding::None:
+ default:
+ yuv2rgb = {
+ 1.0000, 0.0000, 0.0000,
+ 0.0000, 1.0000, 0.0000,
+ 0.0000, 0.0000, 1.0000,
+ };
+ break;
+
+ case libcamera::ColorSpace::YcbcrEncoding::Rec601:
+ yuv2rgb = {
+ 1.0000, 1.0000, 1.0000,
+ 0.0000, -0.3441, 1.7720,
+ 1.4020, -0.7141, 0.0000,
+ };
+ break;
+
+ case libcamera::ColorSpace::YcbcrEncoding::Rec709:
+ yuv2rgb = {
+ 1.0000, 1.0000, 1.0000,
+ 0.0000, -0.1873, 1.8856,
+ 1.5748, -0.4681, 0.0000,
+ };
+ break;
+
+ case libcamera::ColorSpace::YcbcrEncoding::Rec2020:
+ yuv2rgb = {
+ 1.0000, 1.0000, 1.0000,
+ 0.0000, -0.1646, 1.8814,
+ 1.4746, -0.5714, 0.0000,
+ };
+ break;
+ }
+
+ double offset;
+
+ switch (colorSpace.range) {
+ case libcamera::ColorSpace::Range::Full:
+ default:
+ offset = 0.0;
+ break;
+
+ case libcamera::ColorSpace::Range::Limited:
+ offset = 16.0;
+
+ for (unsigned int i = 0; i < 3; ++i)
+ yuv2rgb[i] *= 255.0 / 219.0;
+ for (unsigned int i = 4; i < 9; ++i)
+ yuv2rgb[i] *= 255.0 / 224.0;
+ break;
+ }
+
+ QStringList matrix;
+
+ for (double coeff : yuv2rgb)
+ matrix.append(QString::number(coeff, 'f'));
+
+ fragmentShaderDefines_.append("#define YUV2RGB_MATRIX " + matrix.join(", "));
+ fragmentShaderDefines_.append(QString("#define YUV2RGB_Y_OFFSET %1")
+ .arg(offset, 0, 'f', 1));
+}
+
+bool ViewFinderGL::createVertexShader()
+{
+ /* Create Vertex Shader */
+ vertexShader_ = std::make_unique<QOpenGLShader>(QOpenGLShader::Vertex, this);
+
+ /* Compile the vertex shader */
+ if (!vertexShader_->compileSourceFile(vertexShaderFile_)) {
+ qWarning() << "[ViewFinderGL]:" << vertexShader_->log();
+ return false;
+ }
+
+ shaderProgram_.addShader(vertexShader_.get());
+ return true;
+}
+
+bool ViewFinderGL::createFragmentShader()
+{
+ int attributeVertex;
+ int attributeTexture;
+
+ /*
+ * Create the fragment shader, compile it, and add it to the shader
+ * program. The #define macros stored in fragmentShaderDefines_, if
+ * any, are prepended to the source code.
+ */
+ fragmentShader_ = std::make_unique<QOpenGLShader>(QOpenGLShader::Fragment, this);
+
+ QFile file(fragmentShaderFile_);
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ qWarning() << "Shader" << fragmentShaderFile_ << "not found";
+ return false;
+ }
+
+ QString defines = fragmentShaderDefines_.join('\n') + "\n";
+ QByteArray src = file.readAll();
+ src.prepend(defines.toUtf8());
+
+ if (!fragmentShader_->compileSourceCode(src)) {
+ qWarning() << "[ViewFinderGL]:" << fragmentShader_->log();
+ return false;
+ }
+
+ shaderProgram_.addShader(fragmentShader_.get());
+
+ /* Link shader pipeline */
+ if (!shaderProgram_.link()) {
+ qWarning() << "[ViewFinderGL]:" << shaderProgram_.log();
+ close();
+ }
+
+ /* Bind shader pipeline for use */
+ if (!shaderProgram_.bind()) {
+ qWarning() << "[ViewFinderGL]:" << shaderProgram_.log();
+ close();
+ }
+
+ attributeVertex = shaderProgram_.attributeLocation("vertexIn");
+ attributeTexture = shaderProgram_.attributeLocation("textureIn");
+
+ shaderProgram_.enableAttributeArray(attributeVertex);
+ shaderProgram_.setAttributeBuffer(attributeVertex,
+ GL_FLOAT,
+ 0,
+ 2,
+ 2 * sizeof(GLfloat));
+
+ shaderProgram_.enableAttributeArray(attributeTexture);
+ shaderProgram_.setAttributeBuffer(attributeTexture,
+ GL_FLOAT,
+ 8 * sizeof(GLfloat),
+ 2,
+ 2 * sizeof(GLfloat));
+
+ textureUniformY_ = shaderProgram_.uniformLocation("tex_y");
+ textureUniformU_ = shaderProgram_.uniformLocation("tex_u");
+ textureUniformV_ = shaderProgram_.uniformLocation("tex_v");
+ textureUniformStep_ = shaderProgram_.uniformLocation("tex_step");
+ textureUniformSize_ = shaderProgram_.uniformLocation("tex_size");
+ textureUniformStrideFactor_ = shaderProgram_.uniformLocation("stride_factor");
+ textureUniformBayerFirstRed_ = shaderProgram_.uniformLocation("tex_bayer_first_red");
+
+ /* Create the textures. */
+ for (std::unique_ptr<QOpenGLTexture> &texture : textures_) {
+ if (texture)
+ continue;
+
+ texture = std::make_unique<QOpenGLTexture>(QOpenGLTexture::Target2D);
+ texture->create();
+ }
+
+ return true;
+}
+
+void ViewFinderGL::configureTexture(QOpenGLTexture &texture)
+{
+ glBindTexture(GL_TEXTURE_2D, texture.textureId());
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
+ textureMinMagFilters_);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
+ textureMinMagFilters_);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+}
+
+void ViewFinderGL::removeShader()
+{
+ if (shaderProgram_.isLinked()) {
+ shaderProgram_.release();
+ shaderProgram_.removeAllShaders();
+ }
+}
+
+void ViewFinderGL::initializeGL()
+{
+ initializeOpenGLFunctions();
+ glEnable(GL_TEXTURE_2D);
+ glDisable(GL_DEPTH_TEST);
+
+ static const GLfloat coordinates[2][4][2]{
+ {
+ /* Vertex coordinates */
+ { -1.0f, -1.0f },
+ { -1.0f, +1.0f },
+ { +1.0f, +1.0f },
+ { +1.0f, -1.0f },
+ },
+ {
+ /* Texture coordinates */
+ { 0.0f, 1.0f },
+ { 0.0f, 0.0f },
+ { 1.0f, 0.0f },
+ { 1.0f, 1.0f },
+ },
+ };
+
+ vertexBuffer_.create();
+ vertexBuffer_.bind();
+ vertexBuffer_.allocate(coordinates, sizeof(coordinates));
+
+ /* Create Vertex Shader */
+ if (!createVertexShader())
+ qWarning() << "[ViewFinderGL]: create vertex shader failed.";
+
+ glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
+}
+
+void ViewFinderGL::doRender()
+{
+ /* Stride of the first plane, in pixels. */
+ unsigned int stridePixels;
+
+ switch (format_) {
+ case libcamera::formats::NV12:
+ case libcamera::formats::NV21:
+ case libcamera::formats::NV16:
+ case libcamera::formats::NV61:
+ case libcamera::formats::NV24:
+ case libcamera::formats::NV42:
+ /* Activate texture Y */
+ glActiveTexture(GL_TEXTURE0);
+ configureTexture(*textures_[0]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_LUMINANCE,
+ stride_,
+ size_.height(),
+ 0,
+ GL_LUMINANCE,
+ GL_UNSIGNED_BYTE,
+ image_->data(0).data());
+ shaderProgram_.setUniformValue(textureUniformY_, 0);
+
+ /* Activate texture UV/VU */
+ glActiveTexture(GL_TEXTURE1);
+ configureTexture(*textures_[1]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_LUMINANCE_ALPHA,
+ stride_ / horzSubSample_,
+ size_.height() / vertSubSample_,
+ 0,
+ GL_LUMINANCE_ALPHA,
+ GL_UNSIGNED_BYTE,
+ image_->data(1).data());
+ shaderProgram_.setUniformValue(textureUniformU_, 1);
+
+ stridePixels = stride_;
+ break;
+
+ case libcamera::formats::YUV420:
+ /* Activate texture Y */
+ glActiveTexture(GL_TEXTURE0);
+ configureTexture(*textures_[0]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_LUMINANCE,
+ stride_,
+ size_.height(),
+ 0,
+ GL_LUMINANCE,
+ GL_UNSIGNED_BYTE,
+ image_->data(0).data());
+ shaderProgram_.setUniformValue(textureUniformY_, 0);
+
+ /* Activate texture U */
+ glActiveTexture(GL_TEXTURE1);
+ configureTexture(*textures_[1]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_LUMINANCE,
+ stride_ / horzSubSample_,
+ size_.height() / vertSubSample_,
+ 0,
+ GL_LUMINANCE,
+ GL_UNSIGNED_BYTE,
+ image_->data(1).data());
+ shaderProgram_.setUniformValue(textureUniformU_, 1);
+
+ /* Activate texture V */
+ glActiveTexture(GL_TEXTURE2);
+ configureTexture(*textures_[2]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_LUMINANCE,
+ stride_ / horzSubSample_,
+ size_.height() / vertSubSample_,
+ 0,
+ GL_LUMINANCE,
+ GL_UNSIGNED_BYTE,
+ image_->data(2).data());
+ shaderProgram_.setUniformValue(textureUniformV_, 2);
+
+ stridePixels = stride_;
+ break;
+
+ case libcamera::formats::YVU420:
+ /* Activate texture Y */
+ glActiveTexture(GL_TEXTURE0);
+ configureTexture(*textures_[0]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_LUMINANCE,
+ stride_,
+ size_.height(),
+ 0,
+ GL_LUMINANCE,
+ GL_UNSIGNED_BYTE,
+ image_->data(0).data());
+ shaderProgram_.setUniformValue(textureUniformY_, 0);
+
+ /* Activate texture V */
+ glActiveTexture(GL_TEXTURE2);
+ configureTexture(*textures_[2]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_LUMINANCE,
+ stride_ / horzSubSample_,
+ size_.height() / vertSubSample_,
+ 0,
+ GL_LUMINANCE,
+ GL_UNSIGNED_BYTE,
+ image_->data(1).data());
+ shaderProgram_.setUniformValue(textureUniformV_, 2);
+
+ /* Activate texture U */
+ glActiveTexture(GL_TEXTURE1);
+ configureTexture(*textures_[1]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_LUMINANCE,
+ stride_ / horzSubSample_,
+ size_.height() / vertSubSample_,
+ 0,
+ GL_LUMINANCE,
+ GL_UNSIGNED_BYTE,
+ image_->data(2).data());
+ shaderProgram_.setUniformValue(textureUniformU_, 1);
+
+ stridePixels = stride_;
+ break;
+
+ case libcamera::formats::UYVY:
+ case libcamera::formats::VYUY:
+ case libcamera::formats::YUYV:
+ case libcamera::formats::YVYU:
+ /*
+ * Packed YUV formats are stored in a RGBA texture to match the
+ * OpenGL texel size with the 4 bytes repeating pattern in YUV.
+ * The texture width is thus half of the image_ with.
+ */
+ glActiveTexture(GL_TEXTURE0);
+ configureTexture(*textures_[0]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_RGBA,
+ stride_ / 4,
+ size_.height(),
+ 0,
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ image_->data(0).data());
+ shaderProgram_.setUniformValue(textureUniformY_, 0);
+
+ /*
+ * The shader needs the step between two texture pixels in the
+ * horizontal direction, expressed in texture coordinate units
+ * ([0, 1]). There are exactly width - 1 steps between the
+ * leftmost and rightmost texels.
+ */
+ shaderProgram_.setUniformValue(textureUniformStep_,
+ 1.0f / (size_.width() / 2 - 1),
+ 1.0f /* not used */);
+
+ stridePixels = stride_ / 2;
+ break;
+
+ case libcamera::formats::ABGR8888:
+ case libcamera::formats::ARGB8888:
+ case libcamera::formats::BGRA8888:
+ case libcamera::formats::RGBA8888:
+ glActiveTexture(GL_TEXTURE0);
+ configureTexture(*textures_[0]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_RGBA,
+ stride_ / 4,
+ size_.height(),
+ 0,
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ image_->data(0).data());
+ shaderProgram_.setUniformValue(textureUniformY_, 0);
+
+ stridePixels = stride_ / 4;
+ break;
+
+ case libcamera::formats::BGR888:
+ case libcamera::formats::RGB888:
+ glActiveTexture(GL_TEXTURE0);
+ configureTexture(*textures_[0]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_RGB,
+ stride_ / 3,
+ size_.height(),
+ 0,
+ GL_RGB,
+ GL_UNSIGNED_BYTE,
+ image_->data(0).data());
+ shaderProgram_.setUniformValue(textureUniformY_, 0);
+
+ stridePixels = stride_ / 3;
+ break;
+
+ case libcamera::formats::SBGGR8:
+ case libcamera::formats::SGBRG8:
+ case libcamera::formats::SGRBG8:
+ case libcamera::formats::SRGGB8:
+ case libcamera::formats::SBGGR10_CSI2P:
+ case libcamera::formats::SGBRG10_CSI2P:
+ case libcamera::formats::SGRBG10_CSI2P:
+ case libcamera::formats::SRGGB10_CSI2P:
+ case libcamera::formats::SBGGR12_CSI2P:
+ case libcamera::formats::SGBRG12_CSI2P:
+ case libcamera::formats::SGRBG12_CSI2P:
+ case libcamera::formats::SRGGB12_CSI2P:
+ /*
+ * Raw Bayer 8-bit, and packed raw Bayer 10-bit/12-bit formats
+ * are stored in a GL_LUMINANCE texture. The texture width is
+ * equal to the stride.
+ */
+ glActiveTexture(GL_TEXTURE0);
+ configureTexture(*textures_[0]);
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_LUMINANCE,
+ stride_,
+ size_.height(),
+ 0,
+ GL_LUMINANCE,
+ GL_UNSIGNED_BYTE,
+ image_->data(0).data());
+ shaderProgram_.setUniformValue(textureUniformY_, 0);
+ shaderProgram_.setUniformValue(textureUniformBayerFirstRed_,
+ firstRed_);
+ shaderProgram_.setUniformValue(textureUniformSize_,
+ size_.width(), /* in pixels */
+ size_.height());
+ shaderProgram_.setUniformValue(textureUniformStep_,
+ 1.0f / (stride_ - 1),
+ 1.0f / (size_.height() - 1));
+
+ /*
+ * The stride is already taken into account in the shaders, set
+ * the generic stride factor to 1.0.
+ */
+ stridePixels = size_.width();
+ break;
+
+ default:
+ stridePixels = size_.width();
+ break;
+ };
+
+ /*
+ * Compute the stride factor for the vertex shader, to map the
+ * horizontal texture coordinate range [0.0, 1.0] to the active portion
+ * of the image.
+ */
+ shaderProgram_.setUniformValue(textureUniformStrideFactor_,
+ static_cast<float>(size_.width() - 1) /
+ (stridePixels - 1));
+}
+
+void ViewFinderGL::paintGL()
+{
+ if (!fragmentShader_)
+ if (!createFragmentShader()) {
+ qWarning() << "[ViewFinderGL]:"
+ << "create fragment shader failed.";
+ }
+
+ if (image_) {
+ glClearColor(0.0, 0.0, 0.0, 1.0);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ doRender();
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ }
+}
+
+void ViewFinderGL::resizeGL(int w, int h)
+{
+ glViewport(0, 0, w, h);
+}
+
+QSize ViewFinderGL::sizeHint() const
+{
+ return size_.isValid() ? size_ : QSize(640, 480);
+}
diff --git a/src/apps/qcam/viewfinder_gl.h b/src/apps/qcam/viewfinder_gl.h
new file mode 100644
index 00000000..68c2912d
--- /dev/null
+++ b/src/apps/qcam/viewfinder_gl.h
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Linaro
+ *
+ * viewfinder_GL.h - OpenGL Viewfinder for rendering by OpenGL shader
+ *
+ */
+
+#pragma once
+
+#include <array>
+#include <memory>
+
+#include <QImage>
+#include <QMutex>
+#include <QOpenGLBuffer>
+#include <QOpenGLFunctions>
+#include <QOpenGLShader>
+#include <QOpenGLShaderProgram>
+#include <QOpenGLTexture>
+#include <QOpenGLWidget>
+#include <QSize>
+
+#include <libcamera/formats.h>
+#include <libcamera/framebuffer.h>
+
+#include "viewfinder.h"
+
+class ViewFinderGL : public QOpenGLWidget,
+ public ViewFinder,
+ protected QOpenGLFunctions
+{
+ Q_OBJECT
+
+public:
+ ViewFinderGL(QWidget *parent = nullptr);
+ ~ViewFinderGL();
+
+ const QList<libcamera::PixelFormat> &nativeFormats() const override;
+
+ int setFormat(const libcamera::PixelFormat &format, const QSize &size,
+ const libcamera::ColorSpace &colorSpace,
+ unsigned int stride) override;
+ void render(libcamera::FrameBuffer *buffer, Image *image) override;
+ void stop() override;
+
+ QImage getCurrentImage() override;
+
+Q_SIGNALS:
+ void renderComplete(libcamera::FrameBuffer *buffer);
+
+protected:
+ void initializeGL() override;
+ void paintGL() override;
+ void resizeGL(int w, int h) override;
+ QSize sizeHint() const override;
+
+private:
+ bool selectFormat(const libcamera::PixelFormat &format);
+ void selectColorSpace(const libcamera::ColorSpace &colorSpace);
+
+ void configureTexture(QOpenGLTexture &texture);
+ bool createFragmentShader();
+ bool createVertexShader();
+ void removeShader();
+ void doRender();
+
+ /* Captured image size, format and buffer */
+ libcamera::FrameBuffer *buffer_;
+ libcamera::PixelFormat format_;
+ libcamera::ColorSpace colorSpace_;
+ QSize size_;
+ unsigned int stride_;
+ Image *image_;
+
+ /* Shaders */
+ QOpenGLShaderProgram shaderProgram_;
+ std::unique_ptr<QOpenGLShader> vertexShader_;
+ std::unique_ptr<QOpenGLShader> fragmentShader_;
+ QString vertexShaderFile_;
+ QString fragmentShaderFile_;
+ QStringList fragmentShaderDefines_;
+
+ /* Vertex buffer */
+ QOpenGLBuffer vertexBuffer_;
+
+ /* Textures */
+ std::array<std::unique_ptr<QOpenGLTexture>, 3> textures_;
+
+ /* Common texture parameters */
+ GLuint textureMinMagFilters_;
+
+ /* YUV texture parameters */
+ GLuint textureUniformU_;
+ GLuint textureUniformV_;
+ GLuint textureUniformY_;
+ GLuint textureUniformStep_;
+ unsigned int horzSubSample_;
+ unsigned int vertSubSample_;
+
+ /* Raw Bayer texture parameters */
+ GLuint textureUniformSize_;
+ GLuint textureUniformStrideFactor_;
+ GLuint textureUniformBayerFirstRed_;
+ QPointF firstRed_;
+
+ QMutex mutex_; /* Prevent concurrent access to image_ */
+};
diff --git a/src/apps/qcam/viewfinder_qt.cpp b/src/apps/qcam/viewfinder_qt.cpp
new file mode 100644
index 00000000..c20fd6bc
--- /dev/null
+++ b/src/apps/qcam/viewfinder_qt.cpp
@@ -0,0 +1,181 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * viewfinder_qt.cpp - qcam - QPainter-based ViewFinder
+ */
+
+#include "viewfinder_qt.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <utility>
+
+#include <libcamera/formats.h>
+
+#include <QImage>
+#include <QImageWriter>
+#include <QMap>
+#include <QMutexLocker>
+#include <QPainter>
+#include <QtDebug>
+
+#include "../cam/image.h"
+
+#include "format_converter.h"
+
+static const QMap<libcamera::PixelFormat, QImage::Format> nativeFormats
+{
+#if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0)
+ { libcamera::formats::ABGR8888, QImage::Format_RGBX8888 },
+ { libcamera::formats::XBGR8888, QImage::Format_RGBX8888 },
+#endif
+ { libcamera::formats::ARGB8888, QImage::Format_RGB32 },
+ { libcamera::formats::XRGB8888, QImage::Format_RGB32 },
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ { libcamera::formats::RGB888, QImage::Format_BGR888 },
+#endif
+ { libcamera::formats::BGR888, QImage::Format_RGB888 },
+};
+
+ViewFinderQt::ViewFinderQt(QWidget *parent)
+ : QWidget(parent), buffer_(nullptr)
+{
+ icon_ = QIcon(":camera-off.svg");
+}
+
+ViewFinderQt::~ViewFinderQt()
+{
+}
+
+const QList<libcamera::PixelFormat> &ViewFinderQt::nativeFormats() const
+{
+ static const QList<libcamera::PixelFormat> formats = ::nativeFormats.keys();
+ return formats;
+}
+
+int ViewFinderQt::setFormat(const libcamera::PixelFormat &format, const QSize &size,
+ [[maybe_unused]] const libcamera::ColorSpace &colorSpace,
+ unsigned int stride)
+{
+ image_ = QImage();
+
+ /*
+ * If format conversion is needed, configure the converter and allocate
+ * the destination image.
+ */
+ if (!::nativeFormats.contains(format)) {
+ int ret = converter_.configure(format, size, stride);
+ if (ret < 0)
+ return ret;
+
+ image_ = QImage(size, QImage::Format_RGB32);
+
+ qInfo() << "Using software format conversion from" << format;
+ } else {
+ qInfo() << "Zero-copy enabled";
+ }
+
+ format_ = format;
+ size_ = size;
+
+ updateGeometry();
+ return 0;
+}
+
+void ViewFinderQt::render(libcamera::FrameBuffer *buffer, Image *image)
+{
+ size_t size = buffer->metadata().planes()[0].bytesused;
+
+ {
+ QMutexLocker locker(&mutex_);
+
+ if (::nativeFormats.contains(format_)) {
+ /*
+ * If the frame format is identical to the display
+ * format, create a QImage that references the frame
+ * and store a reference to the frame buffer. The
+ * previously stored frame buffer, if any, will be
+ * released.
+ *
+ * \todo Get the stride from the buffer instead of
+ * computing it naively
+ */
+ assert(buffer->planes().size() == 1);
+ image_ = QImage(image->data(0).data(), size_.width(),
+ size_.height(), size / size_.height(),
+ ::nativeFormats[format_]);
+ std::swap(buffer, buffer_);
+ } else {
+ /*
+ * Otherwise, convert the format and release the frame
+ * buffer immediately.
+ */
+ converter_.convert(image, size, &image_);
+ }
+ }
+
+ update();
+
+ if (buffer)
+ renderComplete(buffer);
+}
+
+void ViewFinderQt::stop()
+{
+ image_ = QImage();
+
+ if (buffer_) {
+ renderComplete(buffer_);
+ buffer_ = nullptr;
+ }
+
+ update();
+}
+
+QImage ViewFinderQt::getCurrentImage()
+{
+ QMutexLocker locker(&mutex_);
+
+ return image_.copy();
+}
+
+void ViewFinderQt::paintEvent(QPaintEvent *)
+{
+ QPainter painter(this);
+
+ /* If we have an image, draw it. */
+ if (!image_.isNull()) {
+ painter.drawImage(rect(), image_, image_.rect());
+ return;
+ }
+
+ /*
+ * Otherwise, draw the camera stopped icon. Render it to the pixmap if
+ * the size has changed.
+ */
+ constexpr int margin = 20;
+
+ if (vfSize_ != size() || pixmap_.isNull()) {
+ QSize vfSize = size() - QSize{ 2 * margin, 2 * margin };
+ QSize pixmapSize{ 1, 1 };
+ pixmapSize.scale(vfSize, Qt::KeepAspectRatio);
+ pixmap_ = icon_.pixmap(pixmapSize);
+
+ vfSize_ = size();
+ }
+
+ QPoint point{ margin, margin };
+ if (pixmap_.width() < width() - 2 * margin)
+ point.setX((width() - pixmap_.width()) / 2);
+ else
+ point.setY((height() - pixmap_.height()) / 2);
+
+ painter.setBackgroundMode(Qt::OpaqueMode);
+ painter.drawPixmap(point, pixmap_);
+}
+
+QSize ViewFinderQt::sizeHint() const
+{
+ return size_.isValid() ? size_ : QSize(640, 480);
+}
diff --git a/src/apps/qcam/viewfinder_qt.h b/src/apps/qcam/viewfinder_qt.h
new file mode 100644
index 00000000..eb3a9988
--- /dev/null
+++ b/src/apps/qcam/viewfinder_qt.h
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * viewfinder_qt.h - qcam - QPainter-based ViewFinder
+ */
+
+#pragma once
+
+#include <QIcon>
+#include <QImage>
+#include <QList>
+#include <QMutex>
+#include <QSize>
+#include <QWidget>
+
+#include <libcamera/formats.h>
+#include <libcamera/framebuffer.h>
+#include <libcamera/pixel_format.h>
+
+#include "format_converter.h"
+#include "viewfinder.h"
+
+class ViewFinderQt : public QWidget, public ViewFinder
+{
+ Q_OBJECT
+
+public:
+ ViewFinderQt(QWidget *parent);
+ ~ViewFinderQt();
+
+ const QList<libcamera::PixelFormat> &nativeFormats() const override;
+
+ int setFormat(const libcamera::PixelFormat &format, const QSize &size,
+ const libcamera::ColorSpace &colorSpace,
+ unsigned int stride) override;
+ void render(libcamera::FrameBuffer *buffer, Image *image) override;
+ void stop() override;
+
+ QImage getCurrentImage() override;
+
+Q_SIGNALS:
+ void renderComplete(libcamera::FrameBuffer *buffer);
+
+protected:
+ void paintEvent(QPaintEvent *) override;
+ QSize sizeHint() const override;
+
+private:
+ FormatConverter converter_;
+
+ libcamera::PixelFormat format_;
+ QSize size_;
+
+ /* Camera stopped icon */
+ QSize vfSize_;
+ QIcon icon_;
+ QPixmap pixmap_;
+
+ /* Buffer and render image */
+ libcamera::FrameBuffer *buffer_;
+ QImage image_;
+ QMutex mutex_; /* Prevent concurrent access to image_ */
+};