diff options
Diffstat (limited to 'examples/multimedia/camera/camera.py')
-rw-r--r-- | examples/multimedia/camera/camera.py | 533 |
1 files changed, 346 insertions, 187 deletions
diff --git a/examples/multimedia/camera/camera.py b/examples/multimedia/camera/camera.py index c3cfe5384..fa379c807 100644 --- a/examples/multimedia/camera/camera.py +++ b/examples/multimedia/camera/camera.py @@ -1,210 +1,369 @@ - -############################################################################# -## -## Copyright (C) 2021 The Qt Company Ltd. -## Contact: http://www.qt.io/licensing/ -## -## This file is part of the Qt for Python examples of the Qt Toolkit. -## -## $QT_BEGIN_LICENSE:BSD$ -## You may use this file under the terms of the BSD license as follows: -## -## "Redistribution and use in source and binary forms, with or without -## modification, are permitted provided that the following conditions are -## met: -## * Redistributions of source code must retain the above copyright -## notice, this list of conditions and the following disclaimer. -## * Redistributions in binary form must reproduce the above copyright -## notice, this list of conditions and the following disclaimer in -## the documentation and/or other materials provided with the -## distribution. -## * Neither the name of The Qt Company Ltd nor the names of its -## contributors may be used to endorse or promote products derived -## from this software without specific prior written permission. -## -## -## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." -## -## $QT_END_LICENSE$ -## -############################################################################# - -"""PySide6 Multimedia Camera Example""" +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import os import sys -from PySide6.QtCore import QDate, QDir, QStandardPaths, Qt, QUrl, Slot -from PySide6.QtGui import QAction, QGuiApplication, QDesktopServices, QIcon -from PySide6.QtGui import QImage, QPixmap -from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, - QMainWindow, QPushButton, QTabWidget, QToolBar, QVBoxLayout, QWidget) -from PySide6.QtMultimedia import (QCamera, QImageCapture, - QCameraDevice, QMediaCaptureSession, - QMediaDevices) -from PySide6.QtMultimediaWidgets import QVideoWidget - - -class ImageView(QWidget): - def __init__(self, previewImage, fileName): +from pathlib import Path + +from PySide6.QtMultimedia import (QAudioInput, QCamera, QCameraDevice, + QImageCapture, QMediaCaptureSession, + QMediaDevices, QMediaMetaData, + QMediaRecorder) +from PySide6.QtWidgets import QDialog, QMainWindow, QMessageBox +from PySide6.QtGui import QAction, QActionGroup, QIcon, QImage, QPixmap +from PySide6.QtCore import QDateTime, QDir, QTimer, Qt, Slot, qWarning + +from metadatadialog import MetaDataDialog +from imagesettings import ImageSettings +from videosettings import VideoSettings, is_android + +if is_android or sys.platform == "darwin": + from PySide6.QtCore import QMicrophonePermission, QCameraPermission + +if is_android: + from ui_camera_mobile import Ui_Camera +else: + from ui_camera import Ui_Camera + + +class Camera(QMainWindow): + def __init__(self): super().__init__() - self._file_name = fileName + self._video_devices_group = None + self.m_devices = QMediaDevices() + self.m_imageCapture = None + self.m_captureSession = QMediaCaptureSession() + self.m_camera = None + self.m_mediaRecorder = None + + self.m_isCapturingImage = False + self.m_applicationExiting = False + self.m_doImageCapture = True + + self.m_metaDataDialog = None + + self._ui = Ui_Camera() + self._ui.setupUi(self) + image = Path(__file__).parent / "shutter.svg" + self._ui.takeImageButton.setIcon(QIcon(os.fspath(image))) + if not is_android: + self._ui.actionAbout_Qt.triggered.connect(qApp.aboutQt) # noqa: F821 + + # disable all buttons by default + self.updateCameraActive(False) + self.readyForCapture(False) + self._ui.recordButton.setEnabled(False) + self._ui.pauseButton.setEnabled(False) + self._ui.stopButton.setEnabled(False) + self._ui.metaDataButton.setEnabled(False) + + # try to actually initialize camera & mic + self.initialize() + + @Slot() + def initialize(self): + if is_android or sys.platform == "darwin": + is_nuitka = "__compiled__" in globals() + if not is_nuitka and sys.platform == "darwin": + print("This example does not work on macOS when Python is run in interpreted mode." + "For this example to work on macOS, package the example using pyside6-deploy" + "For more information, read `Notes for Developer` in the documentation") + sys.exit(0) + + # camera + cam_permission = QCameraPermission() + cam_permission_status = qApp.checkPermission(cam_permission) # noqa: F821 + if cam_permission_status == Qt.PermissionStatus.Undetermined: + qApp.requestPermission(cam_permission, self, self.initialize) # noqa: F821 + return + if cam_permission_status == Qt.PermissionStatus.Denied: + qWarning("Camera permission is not granted!") + return + elif cam_permission_status == Qt.PermissionStatus.Granted: + print("[AudioSource] Camera permission granted") + + # microphone + microphone_permission = QMicrophonePermission() + microphone_permission_status = qApp.checkPermission(microphone_permission) # noqa: F821 + if microphone_permission_status == Qt.PermissionStatus.Undetermined: + qApp.requestPermission(microphone_permission, self, self.initialize) # noqa: F821 + return + if microphone_permission_status == Qt.PermissionStatus.Denied: + qWarning("Microphone permission is not granted!") + self.initializeErrorWindow() + return + elif microphone_permission_status == Qt.PermissionStatus.Granted: + print("[AudioSource] Microphone permission granted") + + self.m_audioInput = QAudioInput() + self.m_captureSession.setAudioInput(self.m_audioInput) + + # Camera devices + + self._video_devices_group = QActionGroup(self) + self._video_devices_group.setExclusive(True) + self.updateCameras() + self.m_devices.videoInputsChanged.connect(self.updateCameras) + + self._video_devices_group.triggered.connect(self.updateCameraDevice) + self._ui.captureWidget.currentChanged.connect(self.updateCaptureMode) + + self._ui.metaDataButton.clicked.connect(self.showMetaDataDialog) + self._ui.exposureCompensation.valueChanged.connect(self.setExposureCompensation) + + self.setCamera(QMediaDevices.defaultVideoInput()) + + @Slot(QCameraDevice) + def setCamera(self, cameraDevice): + self.m_camera = QCamera(cameraDevice) + self.m_captureSession.setCamera(self.m_camera) + + self.m_camera.activeChanged.connect(self.updateCameraActive) + self.m_camera.errorOccurred.connect(self.displayCameraError) + + if not self.m_mediaRecorder: + self.m_mediaRecorder = QMediaRecorder() + self.m_captureSession.setRecorder(self.m_mediaRecorder) + self.m_mediaRecorder.recorderStateChanged.connect(self.updateRecorderState) + self.m_mediaRecorder.durationChanged.connect(self.updateRecordTime) + self.m_mediaRecorder.errorChanged.connect(self.displayRecorderError) + + if not self.m_imageCapture: + self.m_imageCapture = QImageCapture() + self.m_captureSession.setImageCapture(self.m_imageCapture) + self.m_imageCapture.readyForCaptureChanged.connect(self.readyForCapture) + self.m_imageCapture.imageCaptured.connect(self.processCapturedImage) + self.m_imageCapture.imageSaved.connect(self.imageSaved) + self.m_imageCapture.errorOccurred.connect(self.displayCaptureError) + + self.m_captureSession.setVideoOutput(self._ui.viewfinder) + + self.updateCameraActive(self.m_camera.isActive()) + self.updateRecorderState(self.m_mediaRecorder.recorderState()) + self.readyForCapture(self.m_imageCapture.isReadyForCapture()) + + self.updateCaptureMode() + + self.m_camera.start() + + def keyPressEvent(self, event): + if event.isAutoRepeat(): + return + + key = event.key() + if key == Qt.Key_CameraFocus: + self.displayViewfinder() + event.accept() + elif key == Qt.Key_Camera: + if self.m_doImageCapture: + self.takeImage() + else: + if self.m_mediaRecorder.recorderState() == QMediaRecorder.RecordingState: + self.stop() + else: + self.record() + + event.accept() + else: + super().keyPressEvent(event) + + @Slot() + def updateRecordTime(self): + d = self.m_mediaRecorder.duration() / 1000 + self._ui.statusbar.showMessage(f"Recorded {d} sec") + + @Slot(int, QImage) + def processCapturedImage(self, requestId, img): + scaled_image = img.scaled(self._ui.viewfinder.size(), Qt.KeepAspectRatio, + Qt.SmoothTransformation) - main_layout = QVBoxLayout(self) - self._image_label = QLabel() - self._image_label.setPixmap(QPixmap.fromImage(previewImage)) - main_layout.addWidget(self._image_label) + self._ui.lastImagePreviewLabel.setPixmap(QPixmap.fromImage(scaled_image)) - top_layout = QHBoxLayout() - self._file_name_label = QLabel(QDir.toNativeSeparators(fileName)) - self._file_name_label.setTextInteractionFlags(Qt.TextBrowserInteraction) + # Display captured image for 4 seconds. + self.displayCapturedImage() + QTimer.singleShot(4000, self.displayViewfinder) - top_layout.addWidget(self._file_name_label) - top_layout.addStretch() - copy_button = QPushButton("Copy") - copy_button.setToolTip("Copy file name to clipboard") - top_layout.addWidget(copy_button) - copy_button.clicked.connect(self.copy) - launch_button = QPushButton("Launch") - launch_button.setToolTip("Launch image viewer") - top_layout.addWidget(launch_button) - launch_button.clicked.connect(self.launch) - main_layout.addLayout(top_layout) + @Slot() + def configureCaptureSettings(self): + if self.m_doImageCapture: + self.configureImageSettings() + else: + self.configureVideoSettings() @Slot() - def copy(self): - QGuiApplication.clipboard().setText(self._file_name_label.text()) + def configureVideoSettings(self): + settings_dialog = VideoSettings(self.m_mediaRecorder) + + if settings_dialog.exec(): + settings_dialog.apply_settings() @Slot() - def launch(self): - QDesktopServices.openUrl(QUrl.fromLocalFile(self._file_name)) + def configureImageSettings(self): + settings_dialog = ImageSettings(self.m_imageCapture) + if settings_dialog.exec(): + settings_dialog.apply_image_settings() -class MainWindow(QMainWindow): - def __init__(self): - super().__init__() + @Slot() + def record(self): + self.m_mediaRecorder.record() + self.updateRecordTime() - self._capture_session = None - self._camera = None - self._camera_info = None - self._image_capture = None + @Slot() + def pause(self): + self.m_mediaRecorder.pause() - available_cameras = QMediaDevices.videoInputs() - if available_cameras: - self._camera_info = available_cameras[0] - self._camera = QCamera(self._camera_info) - self._camera.errorOccurred.connect(self._camera_error) - self._image_capture = QImageCapture(self._camera) - self._image_capture.imageCaptured.connect(self.image_captured) - self._image_capture.imageSaved.connect(self.image_saved) - self._image_capture.errorOccurred.connect(self._capture_error) - self._capture_session = QMediaCaptureSession() - self._capture_session.setCamera(self._camera) - self._capture_session.setImageCapture(self._image_capture) - - self._current_preview = QImage() - - tool_bar = QToolBar() - self.addToolBar(tool_bar) - - file_menu = self.menuBar().addMenu("&File") - shutter_icon = QIcon(os.path.join(os.path.dirname(__file__), - "shutter.svg")) - self._take_picture_action = QAction(shutter_icon, "&Take Picture", self, - shortcut="Ctrl+T", - triggered=self.take_picture) - self._take_picture_action.setToolTip("Take Picture") - file_menu.addAction(self._take_picture_action) - tool_bar.addAction(self._take_picture_action) - - exit_action = QAction(QIcon.fromTheme("application-exit"), "E&xit", - self, shortcut="Ctrl+Q", triggered=self.close) - file_menu.addAction(exit_action) - - about_menu = self.menuBar().addMenu("&About") - about_qt_action = QAction("About &Qt", self, triggered=qApp.aboutQt) - about_menu.addAction(about_qt_action) - - self._tab_widget = QTabWidget() - self.setCentralWidget(self._tab_widget) - - self._camera_viewfinder = QVideoWidget() - self._tab_widget.addTab(self._camera_viewfinder, "Viewfinder") - - if self._camera and self._camera.error() == QCamera.NoError: - name = self._camera_info.description() - self.setWindowTitle(f"PySide6 Camera Example ({name})") - self.show_status_message(f"Starting: '{name}'") - self._capture_session.setVideoOutput(self._camera_viewfinder) - self._take_picture_action.setEnabled(self._image_capture.isReadyForCapture()) - self._image_capture.readyForCaptureChanged.connect(self._take_picture_action.setEnabled) - self._camera.start() + @Slot() + def stop(self): + self.m_mediaRecorder.stop() + + @Slot(bool) + def setMuted(self, muted): + self.m_captureSession.audioInput().setMuted(muted) + + @Slot() + def takeImage(self): + self.m_isCapturingImage = True + self.m_imageCapture.captureToFile() + + @Slot(int, QImageCapture.Error, str) + def displayCaptureError(self, id, error, errorString): + QMessageBox.warning(self, "Image Capture Error", errorString) + self.m_isCapturingImage = False + + @Slot() + def startCamera(self): + self.m_camera.start() + + @Slot() + def stopCamera(self): + self.m_camera.stop() + + @Slot() + def updateCaptureMode(self): + tab_index = self._ui.captureWidget.currentIndex() + self.m_doImageCapture = (tab_index == 0) + + @Slot(bool) + def updateCameraActive(self, active): + if active: + self._ui.actionStartCamera.setEnabled(False) + self._ui.actionStopCamera.setEnabled(True) + self._ui.captureWidget.setEnabled(True) + self._ui.actionSettings.setEnabled(True) else: - self.setWindowTitle("PySide6 Camera Example") - self._take_picture_action.setEnabled(False) - self.show_status_message("Camera unavailable") + self._ui.actionStartCamera.setEnabled(True) + self._ui.actionStopCamera.setEnabled(False) + self._ui.captureWidget.setEnabled(False) + self._ui.actionSettings.setEnabled(False) + + @Slot(QMediaRecorder.RecorderState) + def updateRecorderState(self, state): + if state == QMediaRecorder.StoppedState: + self._ui.recordButton.setEnabled(True) + self._ui.pauseButton.setEnabled(True) + self._ui.stopButton.setEnabled(False) + self._ui.metaDataButton.setEnabled(True) + elif state == QMediaRecorder.PausedState: + self._ui.recordButton.setEnabled(True) + self._ui.pauseButton.setEnabled(False) + self._ui.stopButton.setEnabled(True) + self._ui.metaDataButton.setEnabled(False) + elif state == QMediaRecorder.RecordingState: + self._ui.recordButton.setEnabled(False) + self._ui.pauseButton.setEnabled(True) + self._ui.stopButton.setEnabled(True) + self._ui.metaDataButton.setEnabled(False) + + @Slot(int) + def setExposureCompensation(self, index): + self.m_camera.setExposureCompensation(index * 0.5) - def show_status_message(self, message): - self.statusBar().showMessage(message, 5000) + @Slot() + def displayRecorderError(self): + if self.m_mediaRecorder.error() != QMediaRecorder.NoError: + QMessageBox.warning(self, "Capture Error", + self.m_mediaRecorder.errorString()) - def closeEvent(self, event): - if self._camera and self._camera.isActive(): - self._camera.stop() - event.accept() - - def next_image_file_name(self): - pictures_location = QStandardPaths.writableLocation(QStandardPaths.PicturesLocation) - date_string = QDate.currentDate().toString("yyyyMMdd") - pattern = f"{pictures_location}/pyside6_camera_{date_string}_{{:03d}}.jpg" - n = 1 - while True: - result = pattern.format(n) - if not os.path.exists(result): - return result - n = n + 1 - return None - - @Slot() - def take_picture(self): - self._current_preview = QImage() - self._image_capture.captureToFile(self.next_image_file_name()) + @Slot() + def displayCameraError(self): + if self.m_camera.error() != QCamera.NoError: + QMessageBox.warning(self, "Camera Error", + self.m_camera.errorString()) - @Slot(int, QImage) - def image_captured(self, id, previewImage): - self._current_preview = previewImage + @Slot(QAction) + def updateCameraDevice(self, action): + self.setCamera(QCameraDevice(action)) + + @Slot() + def displayViewfinder(self): + self._ui.stackedWidget.setCurrentIndex(0) + + @Slot() + def displayCapturedImage(self): + self._ui.stackedWidget.setCurrentIndex(1) + + @Slot(bool) + def readyForCapture(self, ready): + self._ui.takeImageButton.setEnabled(ready) @Slot(int, str) - def image_saved(self, id, fileName): - index = self._tab_widget.count() - image_view = ImageView(self._current_preview, fileName) - self._tab_widget.addTab(image_view, f"Capture #{index}") - self._tab_widget.setCurrentIndex(index) + def imageSaved(self, id, fileName): + f = QDir.toNativeSeparators(fileName) + self._ui.statusbar.showMessage(f"Captured \"{f}\"") - @Slot(int, QImageCapture.Error, str) - def _capture_error(self, id, error, error_string): - print(error_string, file=sys.stderr) - self.show_status_message(error_string) - - @Slot(QCamera.Error, str) - def _camera_error(self, error, error_string): - print(error_string, file=sys.stderr) - self.show_status_message(error_string) - - -if __name__ == '__main__': - app = QApplication(sys.argv) - main_win = MainWindow() - available_geometry = main_win.screen().availableGeometry() - main_win.resize(available_geometry.width() / 3, available_geometry.height() / 2) - main_win.show() - sys.exit(app.exec()) + self.m_isCapturingImage = False + if self.m_applicationExiting: + self.close() + + def closeEvent(self, event): + if self.m_isCapturingImage: + self.setEnabled(False) + self.m_applicationExiting = True + event.ignore() + else: + event.accept() + + @Slot() + def updateCameras(self): + self._ui.menuDevices.clear() + available_cameras = QMediaDevices.videoInputs() + for cameraDevice in available_cameras: + video_device_action = QAction(cameraDevice.description(), + self._video_devices_group) + video_device_action.setCheckable(True) + video_device_action.setData(cameraDevice) + if cameraDevice == QMediaDevices.defaultVideoInput(): + video_device_action.setChecked(True) + + self._ui.menuDevices.addAction(video_device_action) + + @Slot() + def showMetaDataDialog(self): + if not self.m_metaDataDialog: + self.m_metaDataDialog = MetaDataDialog(self) + self.m_metaDataDialog.setAttribute(Qt.WA_DeleteOnClose, False) + if self.m_metaDataDialog.exec() == QDialog.Accepted: + self.saveMetaData() + + @Slot() + def saveMetaData(self): + data = QMediaMetaData() + for i in range(0, QMediaMetaData.NumMetaData): + val = self.m_metaDataDialog.m_metaDataFields[i].text() + if val: + key = QMediaMetaData.Key(i) + if key == QMediaMetaData.CoverArtImage: + cover_art = QImage(val) + data.insert(key, cover_art) + elif key == QMediaMetaData.ThumbnailImage: + thumbnail = QImage(val) + data.insert(key, thumbnail) + elif key == QMediaMetaData.Date: + date = QDateTime.fromString(val) + data.insert(key, date) + else: + data.insert(key, val) + + self.m_mediaRecorder.setMetaData(data) |