diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7fce33b39c73d1728e4a97f72ebfd0584c331199..bef49119c32dbdca2e4b99ea0381ed001c9c1963 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,4 +4,5 @@ add_subdirectory(gallery_controls2)
 add_subdirectory(rasterwindow)
 add_subdirectory(principledmaterial)
 add_subdirectory(hellogl)
+add_subdirectory(camera)
 # (slate is built separately)
\ No newline at end of file
diff --git a/camera/CMakeLists.txt b/camera/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f04c6bf7c612affe3890109cbbed46611e6b0926
--- /dev/null
+++ b/camera/CMakeLists.txt
@@ -0,0 +1,24 @@
+cmake_minimum_required(VERSION 3.16)
+project(camera VERSION 1.0 LANGUAGES CXX)
+
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTOUIC ON)
+
+find_package(QT NAMES Qt5 Qt6 REQUIRED COMPONENTS Core)
+find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Gui Multimedia MultimediaWidgets)
+find_package(Qt${QT_VERSION_MAJOR} OPTIONAL_COMPONENTS Widgets)
+
+qt_add_executable(camera
+    main.cpp
+    mainwindow.cpp mainwindow.h mainwindow.ui
+)
+target_link_libraries(camera PUBLIC
+    Qt::Core
+    Qt::Gui
+    Qt::Multimedia
+    Qt::MultimediaWidgets
+    Qt::CorePrivate
+    Qt::Widgets
+)
diff --git a/camera/main.cpp b/camera/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8370ffb87dfebd13fb4751fc1d1db061e74a9501
--- /dev/null
+++ b/camera/main.cpp
@@ -0,0 +1,17 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "mainwindow.h"
+
+#include <QApplication>
+#include <QLoggingCategory>
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);
+    QLoggingCategory::setFilterRules("*.debug=false\n"
+                                     "qt.multimedia.wasm.*=true");
+    MainWindow w;
+    w.show();
+    return a.exec();
+}
diff --git a/camera/mainwindow.cpp b/camera/mainwindow.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..78c6a75848878bb6e297085520775842d08f1688
--- /dev/null
+++ b/camera/mainwindow.cpp
@@ -0,0 +1,261 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+
+#include <QAudioInput>
+#include <QCamera>
+#include <QCameraDevice>
+#include <QGraphicsScene>
+#include <QGraphicsVideoItem>
+#include <QImageCapture>
+#include <QMediaCaptureSession>
+#include <QMediaDevices>
+#include <QMediaFormat>
+#include <QApplication>
+#include <QTimer>
+#include <QVideoWidget>
+#include <QLabel>
+#include <QFileDialog>
+#include <QScreen>
+#include <QMediaPlayer>
+
+#if QT_CONFIG(permissions)
+  #include <QPermission>
+#endif
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent),
+      ui(new Ui::MainWindow),
+      m_recorder(nullptr)
+{
+    ui->setupUi(this);
+    init();
+}
+
+MainWindow::~MainWindow()
+{
+    if (m_recorder)
+        delete m_recorder;
+    delete ui;
+}
+
+void MainWindow::init()
+{
+#if QT_CONFIG(permissions)
+    // camera
+    QCameraPermission cameraPermission;
+    switch (qApp->checkPermission(cameraPermission)) {
+    case Qt::PermissionStatus::Undetermined:
+        qApp->requestPermission(cameraPermission, this, &MainWindow::init);
+        return;
+    case Qt::PermissionStatus::Denied:
+        qWarning("Camera permission is not granted!");
+        return;
+    case Qt::PermissionStatus::Granted:
+        break;
+    }
+    // microphone
+    QMicrophonePermission microphonePermission;
+    switch (qApp->checkPermission(microphonePermission)) {
+    case Qt::PermissionStatus::Undetermined:
+        qApp->requestPermission(microphonePermission, this, &MainWindow::init);
+        return;
+    case Qt::PermissionStatus::Denied:
+        qWarning("Microphone permission is not granted!");
+        return;
+    case Qt::PermissionStatus::Granted:
+        break;
+    }
+#endif
+    m_captureSession = new QMediaCaptureSession(this);
+
+    m_mediaDevices = new QMediaDevices(this);
+    // wait until devices list is ready
+    connect(m_mediaDevices, &QMediaDevices::videoInputsChanged,
+            [this]() { doCamera(); });
+}
+
+void MainWindow::doCamera()
+{
+    m_audioInput.reset(new QAudioInput);
+    m_captureSession->setAudioInput(m_audioInput.get());
+    const QList<QCameraDevice> cameras = QMediaDevices::videoInputs();
+
+    ui->camerasComboBox->clear();
+    ui->camerasComboBox->setPlaceholderText("select camera");
+
+    for (const QCameraDevice &cameraDevice : cameras) {
+        if (ui->camerasComboBox->findText(cameraDevice.description()) == -1)
+            ui->camerasComboBox->addItem(cameraDevice.description(), cameraDevice.id());
+    }
+
+    if (cameras.count() == 0) {
+        qWarning() << "No camera found";
+    } else {
+
+        QGraphicsVideoItem *videoItem = new QGraphicsVideoItem();
+        QGraphicsScene *scene = new QGraphicsScene(this);
+        m_captureSession->setVideoOutput(videoItem); // sets videoSink
+
+        ui->graphicsView->setScene(scene);
+        ui->graphicsView->scene()->addItem(videoItem);
+        ui->graphicsView->show();
+    }
+}
+
+void MainWindow::on_startButton_clicked()
+{
+    m_camera.data()->start();
+}
+
+void MainWindow::on_stopButton_clicked()
+{
+    m_camera.data()->stop();
+}
+
+void MainWindow::on_captureButton_clicked()
+{
+    connect(m_camera.data(), &QCamera::errorOccurred,
+            [](QCamera::Error error, const QString &errorString) {
+                qWarning() << "Error occurred" << error << errorString;
+    });
+
+    QImageCapture *m_imageCapture = new QImageCapture(this);
+    connect(m_imageCapture, &QImageCapture::readyForCaptureChanged, [] (bool ready) {
+
+        qWarning() << "MainWindow::readyForCaptureChanged" << ready;
+    });
+
+    connect(m_imageCapture,
+            &QImageCapture::imageCaptured,
+            [] (int id, const QImage &preview) {
+        qWarning() << "MainWindow::imageCaptured" << id << preview;
+
+    });
+
+    connect(m_imageCapture, &QImageCapture::imageSaved,
+            [this] (int id, const QString &fileName) {
+                Q_UNUSED(id)
+        QFileInfo fi(fileName);
+        if (!fi.exists()) {
+            qWarning() << fileName << "Does not exist";
+        } else {
+            QDialog *dlg = new QDialog(this);
+            dlg->setWindowTitle(fi.fileName());
+            QHBoxLayout *l = new QHBoxLayout(dlg);
+            QLabel *label = new QLabel(this);
+            l->addWidget(label);
+            label->setPixmap(QPixmap(fileName));
+            dlg->show();
+        }
+
+    });
+    connect(m_imageCapture,
+            &QImageCapture::errorOccurred, []
+            (int id, QImageCapture::Error error, const QString &errorString) {
+        qWarning() << "MainWindow::errorOccurred" << id << error << errorString;
+    });
+
+    m_captureSession->setImageCapture(m_imageCapture);
+
+    // take photo
+    if (m_imageCapture->isReadyForCapture())
+        m_imageCapture->captureToFile(QStringLiteral("/home/web_user/image.png"));
+}
+
+void MainWindow::on_openButton_clicked()
+{
+    // open
+    QFileDialog *dialog = new QFileDialog(this);
+    dialog->setNameFilter(tr("All Files (*.*)"));
+    connect(dialog, &QFileDialog::fileSelected,
+            [this](const QString &file) {
+                qWarning() << "open this file" << file;
+                showFile(file);
+            });
+
+    dialog->show();
+}
+
+void MainWindow::on_camerasComboBox_textActivated(const QString &arg1)
+{
+    const QList<QCameraDevice> cameras = QMediaDevices::videoInputs();
+    for (const QCameraDevice &cameraDevice :cameras) {
+        if (arg1 == cameraDevice.description()) {
+            m_camera.reset(new QCamera(cameraDevice));
+            connect(m_captureSession, &QMediaCaptureSession::cameraChanged,
+                    [this]() {
+                        enableButtons(true);
+                    });
+            m_captureSession->setCamera(m_camera.data());
+            break;
+        }
+    }
+}
+
+void MainWindow::on_recordButton_clicked()
+{
+    if (!isRecording) {
+        if (m_recorder)
+            delete m_recorder;
+        m_recorder = new QMediaRecorder();
+        connect(m_recorder, &QMediaRecorder::durationChanged,
+                [](qint64 duration) {
+            qWarning() << "MainWindow::durationChanged"
+                       << duration;
+        });
+        m_captureSession->setRecorder(m_recorder);
+
+        m_recorder->setQuality(QMediaRecorder::HighQuality);
+        m_recorder->setOutputLocation(QUrl::fromLocalFile("test.mp4"));
+        m_recorder->record();
+        isRecording = true;
+        ui->recordButton->setText(QStringLiteral("Stop"));
+    } else {
+        m_recorder->stop();
+        isRecording = false;
+        m_recorder->deleteLater();
+        ui->recordButton->setText(QStringLiteral("Record"));
+    }
+}
+
+void MainWindow::enableButtons(bool ok)
+{
+    ui->captureButton->setEnabled(ok);
+    ui->startButton->setEnabled(ok);
+    ui->stopButton->setEnabled(ok);
+    ui->openButton->setEnabled(ok);
+    ui->recordButton->setEnabled(ok);
+}
+
+void MainWindow::showFile(const QString &fileName)
+{
+
+    const QSize screenGeometry = screen()->availableSize();
+    QFileInfo fi(fileName);
+    QDialog *dlg = new QDialog(this);
+    dlg->setWindowTitle(fi.fileName());
+    QHBoxLayout *layout = new QHBoxLayout(dlg);
+    QMediaPlayer *player = new QMediaPlayer(dlg);
+    connect(player, &QMediaPlayer::errorOccurred, [=] (QMediaPlayer::Error error, const QString &errorString) {
+            qWarning() << "MediaPlayer erro!:" << error << errorString;
+    });
+
+    QGraphicsVideoItem *m_videoItem = new QGraphicsVideoItem();
+    QSizeF dialogSize(screenGeometry.width() / 2, screenGeometry.height() / 2);
+    m_videoItem->setSize(dialogSize);
+    player->setVideoOutput(m_videoItem);
+    dlg->setGeometry(20, 20, dialogSize.width() + 40, dialogSize.height() + 40);
+
+    QGraphicsScene *scene = new QGraphicsScene(dlg);
+    QGraphicsView *graphicsView = new QGraphicsView(scene);
+    scene->addItem(m_videoItem);
+    layout->addWidget(graphicsView);
+
+    player->setSource(QUrl(fileName));
+
+    dlg->show();
+    player->play();
+}
diff --git a/camera/mainwindow.h b/camera/mainwindow.h
new file mode 100644
index 0000000000000000000000000000000000000000..854c00935ecb6cfe4b0e68d92739bb4e7d84dd7d
--- /dev/null
+++ b/camera/mainwindow.h
@@ -0,0 +1,53 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QImageCapture>
+#include <QMediaCaptureSession>
+#include <QGraphicsVideoItem>
+#include <QCamera>
+#include <QMediaDevices>
+#include <QMediaRecorder>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class MainWindow; }
+
+class QMediaCaptureSession;
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+    void init();
+
+private slots:
+    void doCamera();
+    void on_startButton_clicked();
+    void on_stopButton_clicked();
+    void on_captureButton_clicked();
+    void on_openButton_clicked();
+    void on_recordButton_clicked();
+    void on_camerasComboBox_textActivated(const QString &arg1);
+
+private:
+    Ui::MainWindow *ui;
+
+    void enableButtons(bool ok);
+    void showFile(const QString &filename);
+
+    QScopedPointer<QCamera> m_camera;
+    QMediaCaptureSession *m_captureSession;
+    QScopedPointer<QAudioInput> m_audioInput;
+    QMediaDevices *m_mediaDevices;
+    QMediaRecorder *m_recorder;
+    bool isRecording = false;
+};
+
+QT_END_NAMESPACE
+#endif // MAINWINDOW_H
diff --git a/camera/mainwindow.ui b/camera/mainwindow.ui
new file mode 100644
index 0000000000000000000000000000000000000000..b5de383480f07ec94b1483b8232de2a8b6613ef2
--- /dev/null
+++ b/camera/mainwindow.ui
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>MainWindow</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="0" column="0">
+     <widget class="QComboBox" name="camerasComboBox">
+      <property name="currentIndex">
+       <number>-1</number>
+      </property>
+      <item>
+       <property name="text">
+        <string/>
+       </property>
+      </item>
+     </widget>
+    </item>
+    <item row="1" column="0">
+     <widget class="QGraphicsView" name="graphicsView"/>
+    </item>
+    <item row="2" column="0">
+     <layout class="QHBoxLayout" name="horizontalLayout">
+      <item>
+       <widget class="QPushButton" name="startButton">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>start camera</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="stopButton">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>stop camera</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+    <item row="3" column="0">
+     <layout class="QHBoxLayout" name="horizontalLayout_2">
+      <item>
+       <widget class="QPushButton" name="captureButton">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>capture photo</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="recordButton">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>record</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="openButton">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>open</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>800</width>
+     <height>24</height>
+    </rect>
+   </property>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/deploy.sh b/deploy.sh
index a02531a107259f65d2e14c5d8a0571451a4b6e83..c480c518d3243cac8e17c19123ba81e260cc0eac 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -34,6 +34,7 @@ deploy "multicanvas"
 deploy "rasterwindow"
 deploy "principledmaterial"
 deploy "hellogl"
+deploy "camera"
 
 # slate
 rm -rf slate