diff options
Diffstat (limited to 'src/plugins/multimedia/android/mediacapture')
12 files changed, 2690 insertions, 0 deletions
diff --git a/src/plugins/multimedia/android/mediacapture/qandroidcamera.cpp b/src/plugins/multimedia/android/mediacapture/qandroidcamera.cpp new file mode 100644 index 000000000..52d2e00f6 --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidcamera.cpp @@ -0,0 +1,562 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qandroidcamera_p.h" +#include "qandroidcamerasession_p.h" +#include "qandroidcapturesession_p.h" +#include "qandroidmediacapturesession_p.h" +#include <qmediadevices.h> +#include <qcameradevice.h> +#include <qtimer.h> +#include "qandroidmultimediautils_p.h" + +QT_BEGIN_NAMESPACE + +QAndroidCamera::QAndroidCamera(QCamera *camera) + : QPlatformCamera(camera) +{ + Q_ASSERT(camera); +} + +QAndroidCamera::~QAndroidCamera() +{ +} + +void QAndroidCamera::setActive(bool active) +{ + if (m_cameraSession) { + m_cameraSession->setActive(active); + } else { + isPendingSetActive = active; + } +} + +bool QAndroidCamera::isActive() const +{ + return m_cameraSession ? m_cameraSession->isActive() : false; +} + +void QAndroidCamera::setCamera(const QCameraDevice &camera) +{ + m_cameraDev = camera; + + if (m_cameraSession) { + int id = 0; + auto cameras = QMediaDevices::videoInputs(); + for (int i = 0; i < cameras.size(); ++i) { + if (cameras.at(i) == camera) { + id = i; + break; + } + } + if (id != m_cameraSession->getSelectedCameraId()) { + m_cameraSession->setSelectedCameraId(id); + reactivateCameraSession(); + } + } +} + +void QAndroidCamera::reactivateCameraSession() +{ + if (m_cameraSession->isActive()) { + if (m_service->captureSession() && + m_service->captureSession()->state() == QMediaRecorder::RecordingState) { + m_service->captureSession()->stop(); + qWarning() << "Changing camera during recording not supported"; + } + m_cameraSession->setActive(false); + m_cameraSession->setActive(true); + } +} + +bool QAndroidCamera::setCameraFormat(const QCameraFormat &format) +{ + m_cameraFormat = format; + + if (m_cameraSession) + m_cameraSession->setCameraFormat(m_cameraFormat); + + return true; +} + +void QAndroidCamera::setCaptureSession(QPlatformMediaCaptureSession *session) +{ + QAndroidMediaCaptureSession *captureSession = static_cast<QAndroidMediaCaptureSession *>(session); + if (m_service == captureSession) + return; + + m_service = captureSession; + if (!m_service) { + disconnect(m_cameraSession,nullptr,this,nullptr); + m_cameraSession = nullptr; + return; + } + + m_cameraSession = m_service->cameraSession(); + Q_ASSERT(m_cameraSession); + if (!m_cameraFormat.isNull()) + m_cameraSession->setCameraFormat(m_cameraFormat); + + setCamera(m_cameraDev); + + connect(m_cameraSession, &QAndroidCameraSession::activeChanged, this, &QAndroidCamera::activeChanged); + connect(m_cameraSession, &QAndroidCameraSession::error, this, &QAndroidCamera::error); + connect(m_cameraSession, &QAndroidCameraSession::opened, this, &QAndroidCamera::onCameraOpened); + + if (isPendingSetActive) { + setActive(true); + isPendingSetActive = false; + } +} + +void QAndroidCamera::setFocusMode(QCamera::FocusMode mode) +{ + if (!m_cameraSession || !m_cameraSession->camera()) + return; + + if (isFocusModeSupported(mode)) { + QString focusMode; + + switch (mode) { + case QCamera::FocusModeHyperfocal: + focusMode = QLatin1String("edof"); + break; + case QCamera::FocusModeInfinity: // not 100%, but close + focusMode = QLatin1String("infinity"); + break; + case QCamera::FocusModeManual: + focusMode = QLatin1String("fixed"); + break; + case QCamera::FocusModeAutoNear: + focusMode = QLatin1String("macro"); + break; + case QCamera::FocusModeAuto: + case QCamera::FocusModeAutoFar: + if (1) { // ###? + focusMode = QLatin1String("continuous-video"); + } else { + focusMode = QLatin1String("continuous-picture"); + } + break; + } + + m_cameraSession->camera()->setFocusMode(focusMode); + + // reset focus position + m_cameraSession->camera()->cancelAutoFocus(); + + focusModeChanged(mode); + } +} + +bool QAndroidCamera::isFocusModeSupported(QCamera::FocusMode mode) const +{ + return (m_cameraSession && m_cameraSession->camera()) ? m_supportedFocusModes.contains(mode) : false; +} + +void QAndroidCamera::onCameraOpened() +{ + Q_ASSERT(m_cameraSession); + connect(m_cameraSession->camera(), &AndroidCamera::previewSizeChanged, this, &QAndroidCamera::setCameraFocusArea); + + m_supportedFocusModes.clear(); + m_continuousPictureFocusSupported = false; + m_continuousVideoFocusSupported = false; + m_focusPointSupported = false; + + QStringList focusModes = m_cameraSession->camera()->getSupportedFocusModes(); + for (int i = 0; i < focusModes.size(); ++i) { + const QString &focusMode = focusModes.at(i); + if (focusMode == QLatin1String("continuous-picture")) { + m_supportedFocusModes << QCamera::FocusModeAuto; + m_continuousPictureFocusSupported = true; + } else if (focusMode == QLatin1String("continuous-video")) { + m_supportedFocusModes << QCamera::FocusModeAuto; + m_continuousVideoFocusSupported = true; + } else if (focusMode == QLatin1String("edof")) { + m_supportedFocusModes << QCamera::FocusModeHyperfocal; + } else if (focusMode == QLatin1String("fixed")) { + m_supportedFocusModes << QCamera::FocusModeManual; + } else if (focusMode == QLatin1String("infinity")) { + m_supportedFocusModes << QCamera::FocusModeInfinity; + } else if (focusMode == QLatin1String("macro")) { + m_supportedFocusModes << QCamera::FocusModeAutoNear; + } + } + + if (m_cameraSession->camera()->getMaxNumFocusAreas() > 0) + m_focusPointSupported = true; + + auto m = focusMode(); + if (!m_supportedFocusModes.contains(m)) + m = QCamera::FocusModeAuto; + + setFocusMode(m); + setCustomFocusPoint(focusPoint()); + + if (m_cameraSession->camera()->isZoomSupported()) { + m_zoomRatios = m_cameraSession->camera()->getZoomRatios(); + qreal maxZoom = m_zoomRatios.last() / qreal(100); + maximumZoomFactorChanged(maxZoom); + zoomTo(1, -1); + } else { + m_zoomRatios.clear(); + maximumZoomFactorChanged(1.0); + } + + m_minExposureCompensationIndex = m_cameraSession->camera()->getMinExposureCompensation(); + m_maxExposureCompensationIndex = m_cameraSession->camera()->getMaxExposureCompensation(); + m_exposureCompensationStep = m_cameraSession->camera()->getExposureCompensationStep(); + exposureCompensationRangeChanged(m_minExposureCompensationIndex*m_exposureCompensationStep, + m_maxExposureCompensationIndex*m_exposureCompensationStep); + + m_supportedExposureModes.clear(); + QStringList sceneModes = m_cameraSession->camera()->getSupportedSceneModes(); + if (!sceneModes.isEmpty()) { + for (int i = 0; i < sceneModes.size(); ++i) { + const QString &sceneMode = sceneModes.at(i); + if (sceneMode == QLatin1String("auto")) + m_supportedExposureModes << QCamera::ExposureAuto; + else if (sceneMode == QLatin1String("beach")) + m_supportedExposureModes << QCamera::ExposureBeach; + else if (sceneMode == QLatin1String("night")) + m_supportedExposureModes << QCamera::ExposureNight; + else if (sceneMode == QLatin1String("portrait")) + m_supportedExposureModes << QCamera::ExposurePortrait; + else if (sceneMode == QLatin1String("snow")) + m_supportedExposureModes << QCamera::ExposureSnow; + else if (sceneMode == QLatin1String("sports")) + m_supportedExposureModes << QCamera::ExposureSports; + else if (sceneMode == QLatin1String("action")) + m_supportedExposureModes << QCamera::ExposureAction; + else if (sceneMode == QLatin1String("landscape")) + m_supportedExposureModes << QCamera::ExposureLandscape; + else if (sceneMode == QLatin1String("night-portrait")) + m_supportedExposureModes << QCamera::ExposureNightPortrait; + else if (sceneMode == QLatin1String("theatre")) + m_supportedExposureModes << QCamera::ExposureTheatre; + else if (sceneMode == QLatin1String("sunset")) + m_supportedExposureModes << QCamera::ExposureSunset; + else if (sceneMode == QLatin1String("steadyphoto")) + m_supportedExposureModes << QCamera::ExposureSteadyPhoto; + else if (sceneMode == QLatin1String("fireworks")) + m_supportedExposureModes << QCamera::ExposureFireworks; + else if (sceneMode == QLatin1String("party")) + m_supportedExposureModes << QCamera::ExposureParty; + else if (sceneMode == QLatin1String("candlelight")) + m_supportedExposureModes << QCamera::ExposureCandlelight; + else if (sceneMode == QLatin1String("barcode")) + m_supportedExposureModes << QCamera::ExposureBarcode; + } + } + + setExposureCompensation(exposureCompensation()); + setExposureMode(exposureMode()); + + isFlashSupported = false; + isFlashAutoSupported = false; + isTorchSupported = false; + + QStringList flashModes = m_cameraSession->camera()->getSupportedFlashModes(); + for (int i = 0; i < flashModes.size(); ++i) { + const QString &flashMode = flashModes.at(i); + if (flashMode == QLatin1String("auto")) + isFlashAutoSupported = true; + else if (flashMode == QLatin1String("on")) + isFlashSupported = true; + else if (flashMode == QLatin1String("torch")) + isTorchSupported = true; + } + + setFlashMode(flashMode()); + + m_supportedWhiteBalanceModes.clear(); + QStringList whiteBalanceModes = m_cameraSession->camera()->getSupportedWhiteBalance(); + for (int i = 0; i < whiteBalanceModes.size(); ++i) { + const QString &wb = whiteBalanceModes.at(i); + if (wb == QLatin1String("auto")) { + m_supportedWhiteBalanceModes.insert(QCamera::WhiteBalanceAuto, + QStringLiteral("auto")); + } else if (wb == QLatin1String("cloudy-daylight")) { + m_supportedWhiteBalanceModes.insert(QCamera::WhiteBalanceCloudy, + QStringLiteral("cloudy-daylight")); + } else if (wb == QLatin1String("daylight")) { + m_supportedWhiteBalanceModes.insert(QCamera::WhiteBalanceSunlight, + QStringLiteral("daylight")); + } else if (wb == QLatin1String("fluorescent")) { + m_supportedWhiteBalanceModes.insert(QCamera::WhiteBalanceFluorescent, + QStringLiteral("fluorescent")); + } else if (wb == QLatin1String("incandescent")) { + m_supportedWhiteBalanceModes.insert(QCamera::WhiteBalanceTungsten, + QStringLiteral("incandescent")); + } else if (wb == QLatin1String("shade")) { + m_supportedWhiteBalanceModes.insert(QCamera::WhiteBalanceShade, + QStringLiteral("shade")); + } else if (wb == QLatin1String("twilight")) { + m_supportedWhiteBalanceModes.insert(QCamera::WhiteBalanceSunset, + QStringLiteral("twilight")); + } else if (wb == QLatin1String("warm-fluorescent")) { + m_supportedWhiteBalanceModes.insert(QCamera::WhiteBalanceFlash, + QStringLiteral("warm-fluorescent")); + } + } + +} + +//void QAndroidCameraFocusControl::onCameraCaptureModeChanged() +//{ +// if (m_cameraSession->camera() && m_focusMode == QCamera::FocusModeAudio) { +// QString focusMode; +// if ((m_cameraSession->captureMode().testFlag(QCamera::CaptureVideo) && m_continuousVideoFocusSupported) +// || !m_continuousPictureFocusSupported) { +// focusMode = QLatin1String("continuous-video"); +// } else { +// focusMode = QLatin1String("continuous-picture"); +// } +// m_cameraSession->camera()->setFocusMode(focusMode); +// m_cameraSession->camera()->cancelAutoFocus(); +// } +//} + +static QRect adjustedArea(const QRectF &area) +{ + // Qt maps focus points in the range (0.0, 0.0) -> (1.0, 1.0) + // Android maps focus points in the range (-1000, -1000) -> (1000, 1000) + // Converts an area in Qt coordinates to Android coordinates + return QRect(-1000 + qRound(area.x() * 2000), + -1000 + qRound(area.y() * 2000), + qRound(area.width() * 2000), + qRound(area.height() * 2000)) + .intersected(QRect(-1000, -1000, 2000, 2000)); +} + +void QAndroidCamera::setCameraFocusArea() +{ + if (!m_cameraSession) + return; + + QList<QRect> areas; + auto focusPoint = customFocusPoint(); + if (QRectF(0., 0., 1., 1.).contains(focusPoint)) { + // in FocusPointAuto mode, leave the area list empty + // to let the driver choose the focus point. + QSize viewportSize = m_cameraSession->camera()->previewSize(); + + if (!viewportSize.isValid()) + return; + + // Set up a 50x50 pixel focus area around the focal point + QSizeF focusSize(50.f / viewportSize.width(), 50.f / viewportSize.height()); + float x = qBound(qreal(0), + focusPoint.x() - (focusSize.width() / 2), + 1.f - focusSize.width()); + float y = qBound(qreal(0), + focusPoint.y() - (focusSize.height() / 2), + 1.f - focusSize.height()); + + QRectF area(QPointF(x, y), focusSize); + + areas.append(adjustedArea(area)); + } + m_cameraSession->camera()->setFocusAreas(areas); +} + +void QAndroidCamera::zoomTo(float factor, float rate) +{ + Q_UNUSED(rate); + + if (zoomFactor() == factor) + return; + + if (!m_cameraSession || !m_cameraSession->camera()) + return; + + factor = qBound(qreal(1), factor, maxZoomFactor()); + int validZoomIndex = qt_findClosestValue(m_zoomRatios, qRound(factor * 100)); + float newZoom = m_zoomRatios.at(validZoomIndex) / qreal(100); + m_cameraSession->camera()->setZoom(validZoomIndex); + zoomFactorChanged(newZoom); +} + +void QAndroidCamera::setFlashMode(QCamera::FlashMode mode) +{ + if (!m_cameraSession || !m_cameraSession->camera()) + return; + + if (!isFlashModeSupported(mode)) + return; + + QString flashMode; + if (mode == QCamera::FlashAuto) + flashMode = QLatin1String("auto"); + else if (mode == QCamera::FlashOn) + flashMode = QLatin1String("on"); + else // FlashOff + flashMode = QLatin1String("off"); + + m_cameraSession->camera()->setFlashMode(flashMode); + flashModeChanged(mode); +} + +bool QAndroidCamera::isFlashModeSupported(QCamera::FlashMode mode) const +{ + if (!m_cameraSession || !m_cameraSession->camera()) + return false; + switch (mode) { + case QCamera::FlashOff: + return true; + case QCamera::FlashOn: + return isFlashSupported; + case QCamera::FlashAuto: + return isFlashAutoSupported; + } +} + +bool QAndroidCamera::isFlashReady() const +{ + // Android doesn't have an API for that + return true; +} + +void QAndroidCamera::setTorchMode(QCamera::TorchMode mode) +{ + if (!m_cameraSession) + return; + auto *camera = m_cameraSession->camera(); + if (!camera || !isTorchSupported || mode == QCamera::TorchAuto) + return; + + if (mode == QCamera::TorchOn) { + camera->setFlashMode(QLatin1String("torch")); + } else if (mode == QCamera::TorchOff) { + // if torch was enabled, it first needs to be turned off before restoring the flash mode + camera->setFlashMode(QLatin1String("off")); + setFlashMode(flashMode()); + } + torchModeChanged(mode); +} + +bool QAndroidCamera::isTorchModeSupported(QCamera::TorchMode mode) const +{ + if (!m_cameraSession || !m_cameraSession->camera()) + return false; + switch (mode) { + case QCamera::TorchOff: + return true; + case QCamera::TorchOn: + return isTorchSupported; + case QCamera::TorchAuto: + return false; + } +} + +void QAndroidCamera::setExposureMode(QCamera::ExposureMode mode) +{ + if (exposureMode() == mode) + return; + + if (!m_cameraSession || !m_cameraSession->camera()) + return; + + if (!m_supportedExposureModes.contains(mode)) + return; + + QString sceneMode; + switch (mode) { + case QCamera::ExposureAuto: + sceneMode = QLatin1String("auto"); + break; + case QCamera::ExposureSports: + sceneMode = QLatin1String("sports"); + break; + case QCamera::ExposurePortrait: + sceneMode = QLatin1String("portrait"); + break; + case QCamera::ExposureBeach: + sceneMode = QLatin1String("beach"); + break; + case QCamera::ExposureSnow: + sceneMode = QLatin1String("snow"); + break; + case QCamera::ExposureNight: + sceneMode = QLatin1String("night"); + break; + case QCamera::ExposureAction: + sceneMode = QLatin1String("action"); + break; + case QCamera::ExposureLandscape: + sceneMode = QLatin1String("landscape"); + break; + case QCamera::ExposureNightPortrait: + sceneMode = QLatin1String("night-portrait"); + break; + case QCamera::ExposureTheatre: + sceneMode = QLatin1String("theatre"); + break; + case QCamera::ExposureSunset: + sceneMode = QLatin1String("sunset"); + break; + case QCamera::ExposureSteadyPhoto: + sceneMode = QLatin1String("steadyphoto"); + break; + case QCamera::ExposureFireworks: + sceneMode = QLatin1String("fireworks"); + break; + case QCamera::ExposureParty: + sceneMode = QLatin1String("party"); + break; + case QCamera::ExposureCandlelight: + sceneMode = QLatin1String("candlelight"); + break; + case QCamera::ExposureBarcode: + sceneMode = QLatin1String("barcode"); + break; + default: + sceneMode = QLatin1String("auto"); + mode = QCamera::ExposureAuto; + break; + } + + m_cameraSession->camera()->setSceneMode(sceneMode); + exposureModeChanged(mode); +} + +bool QAndroidCamera::isExposureModeSupported(QCamera::ExposureMode mode) const +{ + return m_supportedExposureModes.contains(mode); +} + +void QAndroidCamera::setExposureCompensation(float bias) +{ + if (exposureCompensation() == bias || !m_cameraSession || !m_cameraSession->camera()) + return; + + int biasIndex = qRound(bias / m_exposureCompensationStep); + biasIndex = qBound(m_minExposureCompensationIndex, biasIndex, m_maxExposureCompensationIndex); + float comp = biasIndex * m_exposureCompensationStep; + m_cameraSession->camera()->setExposureCompensation(biasIndex); + exposureCompensationChanged(comp); +} + +bool QAndroidCamera::isWhiteBalanceModeSupported(QCamera::WhiteBalanceMode mode) const +{ + return m_supportedWhiteBalanceModes.contains(mode); +} + +void QAndroidCamera::setWhiteBalanceMode(QCamera::WhiteBalanceMode mode) +{ + if (!m_cameraSession) + return; + auto *camera = m_cameraSession->camera(); + if (!camera) + return; + QString wb = m_supportedWhiteBalanceModes.value(mode, QString()); + if (!wb.isEmpty()) { + camera->setWhiteBalance(wb); + whiteBalanceModeChanged(mode); + } +} + +QT_END_NAMESPACE + +#include "moc_qandroidcamera_p.cpp" diff --git a/src/plugins/multimedia/android/mediacapture/qandroidcamera_p.h b/src/plugins/multimedia/android/mediacapture/qandroidcamera_p.h new file mode 100644 index 000000000..77bbc3133 --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidcamera_p.h @@ -0,0 +1,99 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + + +#ifndef QANDROIDCAMERACONTROL_H +#define QANDROIDCAMERACONTROL_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qplatformcamera_p.h> + +#include <qmap.h> + +QT_BEGIN_NAMESPACE + +class QAndroidCameraSession; +class QAndroidCameraVideoRendererControl; +class QAndroidMediaCaptureSession; + +class QAndroidCamera : public QPlatformCamera +{ + Q_OBJECT +public: + explicit QAndroidCamera(QCamera *camera); + virtual ~QAndroidCamera(); + + bool isActive() const override; + void setActive(bool active) override; + + void setCamera(const QCameraDevice &camera) override; + bool setCameraFormat(const QCameraFormat &format) override; + + void setCaptureSession(QPlatformMediaCaptureSession *session) override; + + void setFocusMode(QCamera::FocusMode mode) override; + bool isFocusModeSupported(QCamera::FocusMode mode) const override; + + void zoomTo(float factor, float rate) override; + + void setFlashMode(QCamera::FlashMode mode) override; + bool isFlashModeSupported(QCamera::FlashMode mode) const override; + bool isFlashReady() const override; + + void setTorchMode(QCamera::TorchMode mode) override; + bool isTorchModeSupported(QCamera::TorchMode mode) const override; + + void setExposureMode(QCamera::ExposureMode mode) override; + bool isExposureModeSupported(QCamera::ExposureMode mode) const override; + + void setExposureCompensation(float bias) override; + + bool isWhiteBalanceModeSupported(QCamera::WhiteBalanceMode mode) const override; + void setWhiteBalanceMode(QCamera::WhiteBalanceMode mode) override; + +private Q_SLOTS: + void onCameraOpened(); + void setCameraFocusArea(); + +private: + void reactivateCameraSession(); + + QAndroidCameraSession *m_cameraSession = nullptr; + QAndroidMediaCaptureSession *m_service = nullptr; + + QList<QCamera::FocusMode> m_supportedFocusModes; + bool m_continuousPictureFocusSupported = false; + bool m_continuousVideoFocusSupported = false; + bool m_focusPointSupported = false; + + QList<int> m_zoomRatios; + + QList<QCamera::ExposureMode> m_supportedExposureModes; + int m_minExposureCompensationIndex; + int m_maxExposureCompensationIndex; + qreal m_exposureCompensationStep; + + bool isFlashSupported = false; + bool isFlashAutoSupported = false; + bool isTorchSupported = false; + bool isPendingSetActive = false; + QCameraDevice m_cameraDev; + + QMap<QCamera::WhiteBalanceMode, QString> m_supportedWhiteBalanceModes; + QCameraFormat m_cameraFormat; +}; + + +QT_END_NAMESPACE + +#endif // QANDROIDCAMERACONTROL_H diff --git a/src/plugins/multimedia/android/mediacapture/qandroidcamerasession.cpp b/src/plugins/multimedia/android/mediacapture/qandroidcamerasession.cpp new file mode 100644 index 000000000..7eda1175f --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidcamerasession.cpp @@ -0,0 +1,808 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2016 Ruslan Baratov +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qandroidcamerasession_p.h" + +#include "androidcamera_p.h" +#include "androidmultimediautils_p.h" +#include "qandroidvideooutput_p.h" +#include "qandroidmultimediautils_p.h" +#include "androidmediarecorder_p.h" +#include <qvideosink.h> +#include <QtConcurrent/qtconcurrentrun.h> +#include <qfile.h> +#include <qguiapplication.h> +#include <qscreen.h> +#include <qdebug.h> +#include <qvideoframe.h> +#include <private/qplatformimagecapture_p.h> +#include <private/qplatformvideosink_p.h> +#include <private/qmemoryvideobuffer_p.h> +#include <private/qcameradevice_p.h> +#include <private/qmediastoragelocation_p.h> +#include <private/qvideoframe_p.h> +#include <QImageWriter> + +QT_BEGIN_NAMESPACE + +Q_GLOBAL_STATIC(QList<QCameraDevice>, g_availableCameras) + +QAndroidCameraSession::QAndroidCameraSession(QObject *parent) + : QObject(parent) + , m_selectedCamera(0) + , m_camera(0) + , m_videoOutput(0) + , m_savedState(-1) + , m_previewStarted(false) + , m_readyForCapture(false) + , m_currentImageCaptureId(-1) + , m_previewCallback(0) + , m_keepActive(false) +{ + if (qApp) { + connect(qApp, &QGuiApplication::applicationStateChanged, + this, &QAndroidCameraSession::onApplicationStateChanged); + + auto screen = qApp->primaryScreen(); + if (screen) { + connect(screen, &QScreen::orientationChanged, this, + &QAndroidCameraSession::updateOrientation); + enableRotation(); + } + } +} + +QAndroidCameraSession::~QAndroidCameraSession() +{ + if (m_sink) + disconnect(m_retryPreviewConnection); + close(); +} + +//void QAndroidCameraSession::setCaptureMode(QCamera::CaptureModes mode) +//{ +// if (m_captureMode == mode || !isCaptureModeSupported(mode)) +// return; + +// m_captureMode = mode; +// emit captureModeChanged(m_captureMode); + +// if (m_previewStarted && m_captureMode.testFlag(QCamera::CaptureStillImage)) +// applyResolution(m_actualImageSettings.resolution()); +//} + +void QAndroidCameraSession::setActive(bool active) +{ + if (m_active == active) + return; + + // If the application is inactive, the camera shouldn't be started. Save the desired state + // instead and it will be set when the application becomes active. + if (active && qApp->applicationState() == Qt::ApplicationInactive) { + m_isStateSaved = true; + m_savedState = active; + return; + } + + m_isStateSaved = false; + m_active = active; + setActiveHelper(m_active); + emit activeChanged(m_active); +} + +void QAndroidCameraSession::setActiveHelper(bool active) +{ + if (!active) { + stopPreview(); + close(); + } else { + if (!m_camera && !open()) { + emit error(QCamera::CameraError, QStringLiteral("Failed to open camera")); + return; + } + startPreview(); + } +} + +void QAndroidCameraSession::updateAvailableCameras() +{ + g_availableCameras->clear(); + + const int numCameras = AndroidCamera::getNumberOfCameras(); + for (int i = 0; i < numCameras; ++i) { + QCameraDevicePrivate *info = new QCameraDevicePrivate; + AndroidCamera::getCameraInfo(i, info); + + if (!info->id.isEmpty()) { + // Add supported picture and video sizes to the camera info + AndroidCamera *camera = AndroidCamera::open(i); + + if (camera) { + info->videoFormats = camera->getSupportedFormats(); + info->photoResolutions = camera->getSupportedPictureSizes(); + } + + delete camera; + g_availableCameras->append(info->create()); + } + } +} + +const QList<QCameraDevice> &QAndroidCameraSession::availableCameras() +{ + if (g_availableCameras->isEmpty()) + updateAvailableCameras(); + + return *g_availableCameras; +} + +bool QAndroidCameraSession::open() +{ + close(); + + m_camera = AndroidCamera::open(m_selectedCamera); + + if (m_camera) { + connect(m_camera, &AndroidCamera::pictureExposed, + this, &QAndroidCameraSession::onCameraPictureExposed); + connect(m_camera, &AndroidCamera::lastPreviewFrameFetched, + this, &QAndroidCameraSession::onLastPreviewFrameFetched, + Qt::DirectConnection); + connect(m_camera, &AndroidCamera::newPreviewFrame, + this, &QAndroidCameraSession::onNewPreviewFrame, + Qt::DirectConnection); + connect(m_camera, &AndroidCamera::pictureCaptured, + this, &QAndroidCameraSession::onCameraPictureCaptured); + connect(m_camera, &AndroidCamera::previewStarted, + this, &QAndroidCameraSession::onCameraPreviewStarted); + connect(m_camera, &AndroidCamera::previewStopped, + this, &QAndroidCameraSession::onCameraPreviewStopped); + connect(m_camera, &AndroidCamera::previewFailedToStart, + this, &QAndroidCameraSession::onCameraPreviewFailedToStart); + connect(m_camera, &AndroidCamera::takePictureFailed, + this, &QAndroidCameraSession::onCameraTakePictureFailed); + + if (m_camera->getPreviewFormat() != AndroidCamera::NV21) + m_camera->setPreviewFormat(AndroidCamera::NV21); + + m_camera->notifyNewFrames(m_previewCallback); + + emit opened(); + setActive(true); + } + + return m_camera != 0; +} + +void QAndroidCameraSession::close() +{ + if (!m_camera) + return; + + stopPreview(); + + m_readyForCapture = false; + m_currentImageCaptureId = -1; + m_currentImageCaptureFileName.clear(); + m_actualImageSettings = m_requestedImageSettings; + + m_camera->release(); + delete m_camera; + m_camera = 0; + + setActive(false); +} + +void QAndroidCameraSession::setVideoOutput(QAndroidVideoOutput *output) +{ + if (m_videoOutput) { + m_videoOutput->stop(); + m_videoOutput->reset(); + } + + if (output) { + m_videoOutput = output; + if (m_videoOutput->isReady()) { + onVideoOutputReady(true); + } else { + connect(m_videoOutput, &QAndroidVideoOutput::readyChanged, + this, &QAndroidCameraSession::onVideoOutputReady); + } + } else { + m_videoOutput = 0; + } +} + +void QAndroidCameraSession::setCameraFormat(const QCameraFormat &format) +{ + m_requestedFpsRange.min = format.minFrameRate(); + m_requestedFpsRange.max = format.maxFrameRate(); + m_requestedPixelFromat = AndroidCamera::AndroidImageFormatFromQtPixelFormat(format.pixelFormat()); + + m_requestedImageSettings.setResolution(format.resolution()); + m_actualImageSettings.setResolution(format.resolution()); + if (m_readyForCapture) + applyResolution(m_actualImageSettings.resolution()); +} + +void QAndroidCameraSession::applyResolution(const QSize &captureSize, bool restartPreview) +{ + if (!m_camera) + return; + + const QSize currentViewfinderResolution = m_camera->previewSize(); + const AndroidCamera::ImageFormat currentPreviewFormat = m_camera->getPreviewFormat(); + const AndroidCamera::FpsRange currentFpsRange = m_camera->getPreviewFpsRange(); + + // -- adjust resolution + QSize adjustedViewfinderResolution; + const QList<QSize> previewSizes = m_camera->getSupportedPreviewSizes(); + + const bool validCaptureSize = captureSize.width() > 0 && captureSize.height() > 0; + if (validCaptureSize + && m_camera->getPreferredPreviewSizeForVideo().isEmpty()) { + // According to the Android doc, if getPreferredPreviewSizeForVideo() returns null, it means + // the preview size cannot be different from the capture size + adjustedViewfinderResolution = captureSize; + } else { + qreal captureAspectRatio = 0; + if (validCaptureSize) + captureAspectRatio = qreal(captureSize.width()) / qreal(captureSize.height()); + + if (validCaptureSize) { + // search for viewfinder resolution with the same aspect ratio + qreal minAspectDiff = 1; + QSize closestResolution; + for (int i = previewSizes.count() - 1; i >= 0; --i) { + const QSize &size = previewSizes.at(i); + const qreal sizeAspect = qreal(size.width()) / size.height(); + if (qFuzzyCompare(captureAspectRatio, sizeAspect)) { + adjustedViewfinderResolution = size; + break; + } else if (minAspectDiff > qAbs(sizeAspect - captureAspectRatio)) { + closestResolution = size; + minAspectDiff = qAbs(sizeAspect - captureAspectRatio); + } + } + if (!adjustedViewfinderResolution.isValid()) { + qWarning("Cannot find a viewfinder resolution matching the capture aspect ratio."); + if (closestResolution.isValid()) { + adjustedViewfinderResolution = closestResolution; + qWarning("Using closest viewfinder resolution."); + } else { + return; + } + } + } else { + adjustedViewfinderResolution = previewSizes.last(); + } + } + + // -- adjust pixel format + + AndroidCamera::ImageFormat adjustedPreviewFormat = m_requestedPixelFromat; + if (adjustedPreviewFormat == AndroidCamera::UnknownImageFormat) + adjustedPreviewFormat = AndroidCamera::NV21; + + // -- adjust FPS + + AndroidCamera::FpsRange adjustedFps = m_requestedFpsRange; + if (adjustedFps.min == 0 || adjustedFps.max == 0) + adjustedFps = currentFpsRange; + + // -- Set values on camera + + // fix the resolution of output based on the orientation + QSize cameraOutputResolution = adjustedViewfinderResolution; + QSize videoOutputResolution = adjustedViewfinderResolution; + QSize currentVideoOutputResolution = m_videoOutput ? m_videoOutput->getVideoSize() : QSize(0, 0); + const int rotation = currentCameraRotation(); + // only transpose if it's valid for the preview + if (rotation == 90 || rotation == 270) { + videoOutputResolution.transpose(); + if (previewSizes.contains(cameraOutputResolution.transposed())) + cameraOutputResolution.transpose(); + } + + if (currentViewfinderResolution != cameraOutputResolution + || (m_videoOutput && currentVideoOutputResolution != videoOutputResolution) + || currentPreviewFormat != adjustedPreviewFormat || currentFpsRange.min != adjustedFps.min + || currentFpsRange.max != adjustedFps.max) { + if (m_videoOutput) { + m_videoOutput->setVideoSize(videoOutputResolution); + } + + // if preview is started, we have to stop it first before changing its size + if (m_previewStarted && restartPreview) + m_camera->stopPreview(); + + m_camera->setPreviewSize(cameraOutputResolution); + m_camera->setPreviewFormat(adjustedPreviewFormat); + m_camera->setPreviewFpsRange(adjustedFps); + + // restart preview + if (m_previewStarted && restartPreview) + m_camera->startPreview(); + } +} + +QList<QSize> QAndroidCameraSession::getSupportedPreviewSizes() const +{ + return m_camera ? m_camera->getSupportedPreviewSizes() : QList<QSize>(); +} + +QList<QVideoFrameFormat::PixelFormat> QAndroidCameraSession::getSupportedPixelFormats() const +{ + QList<QVideoFrameFormat::PixelFormat> formats; + + if (!m_camera) + return formats; + + const QList<AndroidCamera::ImageFormat> nativeFormats = m_camera->getSupportedPreviewFormats(); + + formats.reserve(nativeFormats.size()); + + for (AndroidCamera::ImageFormat nativeFormat : nativeFormats) { + QVideoFrameFormat::PixelFormat format = AndroidCamera::QtPixelFormatFromAndroidImageFormat(nativeFormat); + if (format != QVideoFrameFormat::Format_Invalid) + formats.append(format); + } + + return formats; +} + +QList<AndroidCamera::FpsRange> QAndroidCameraSession::getSupportedPreviewFpsRange() const +{ + return m_camera ? m_camera->getSupportedPreviewFpsRange() : QList<AndroidCamera::FpsRange>(); +} + + +bool QAndroidCameraSession::startPreview() +{ + if (!m_camera || !m_videoOutput) + return false; + + if (m_previewStarted) + return true; + + if (!m_videoOutput->isReady()) + return true; // delay starting until the video output is ready + + Q_ASSERT(m_videoOutput->surfaceTexture() || m_videoOutput->surfaceHolder()); + + if ((m_videoOutput->surfaceTexture() && !m_camera->setPreviewTexture(m_videoOutput->surfaceTexture())) + || (m_videoOutput->surfaceHolder() && !m_camera->setPreviewDisplay(m_videoOutput->surfaceHolder()))) + return false; + + applyResolution(m_actualImageSettings.resolution()); + + AndroidMultimediaUtils::enableOrientationListener(true); + + updateOrientation(); + m_camera->startPreview(); + m_previewStarted = true; + m_videoOutput->start(); + + return true; +} + +QSize QAndroidCameraSession::getDefaultResolution() const +{ + const bool hasHighQualityProfile = AndroidCamcorderProfile::hasProfile( + m_camera->cameraId(), + AndroidCamcorderProfile::Quality(AndroidCamcorderProfile::QUALITY_HIGH)); + + if (hasHighQualityProfile) { + const AndroidCamcorderProfile camProfile = AndroidCamcorderProfile::get( + m_camera->cameraId(), + AndroidCamcorderProfile::Quality(AndroidCamcorderProfile::QUALITY_HIGH)); + + return QSize(camProfile.getValue(AndroidCamcorderProfile::videoFrameWidth), + camProfile.getValue(AndroidCamcorderProfile::videoFrameHeight)); + } + return QSize(); +} + +void QAndroidCameraSession::stopPreview() +{ + if (!m_camera || !m_previewStarted) + return; + + AndroidMultimediaUtils::enableOrientationListener(false); + + m_camera->stopPreview(); + m_camera->setPreviewSize(QSize()); + m_camera->setPreviewTexture(0); + m_camera->setPreviewDisplay(0); + + if (m_videoOutput) { + m_videoOutput->stop(); + } + m_previewStarted = false; +} + +void QAndroidCameraSession::setImageSettings(const QImageEncoderSettings &settings) +{ + if (m_requestedImageSettings == settings) + return; + + m_requestedImageSettings = m_actualImageSettings = settings; + + applyImageSettings(); + + if (m_readyForCapture) + applyResolution(m_actualImageSettings.resolution()); +} + +void QAndroidCameraSession::enableRotation() +{ + m_rotationEnabled = true; +} + +void QAndroidCameraSession::disableRotation() +{ + m_rotationEnabled = false; +} + +void QAndroidCameraSession::updateOrientation() +{ + if (!m_camera || !m_rotationEnabled) + return; + + m_camera->setDisplayOrientation(currentCameraRotation()); + applyResolution(m_actualImageSettings.resolution()); +} + + +int QAndroidCameraSession::currentCameraRotation() const +{ + if (!m_camera) + return 0; + + auto screen = QGuiApplication::primaryScreen(); + auto screenOrientation = screen->orientation(); + if (screenOrientation == Qt::PrimaryOrientation) + screenOrientation = screen->primaryOrientation(); + + int deviceOrientation = 0; + switch (screenOrientation) { + case Qt::PrimaryOrientation: + case Qt::PortraitOrientation: + break; + case Qt::LandscapeOrientation: + deviceOrientation = 90; + break; + case Qt::InvertedPortraitOrientation: + deviceOrientation = 180; + break; + case Qt::InvertedLandscapeOrientation: + deviceOrientation = 270; + break; + } + + int nativeCameraOrientation = m_camera->getNativeOrientation(); + + int rotation; + // subtract natural camera orientation and physical device orientation + if (m_camera->getFacing() == AndroidCamera::CameraFacingFront) { + rotation = (nativeCameraOrientation + deviceOrientation) % 360; + rotation = (360 - rotation) % 360; // compensate the mirror + } else { // back-facing camera + rotation = (nativeCameraOrientation - deviceOrientation + 360) % 360; + } + return rotation; +} + +void QAndroidCameraSession::setPreviewFormat(AndroidCamera::ImageFormat format) +{ + if (format == AndroidCamera::UnknownImageFormat) + return; + + m_camera->setPreviewFormat(format); +} + +void QAndroidCameraSession::setPreviewCallback(PreviewCallback *callback) +{ + m_videoFrameCallbackMutex.lock(); + m_previewCallback = callback; + if (m_camera) + m_camera->notifyNewFrames(m_previewCallback); + m_videoFrameCallbackMutex.unlock(); +} + +void QAndroidCameraSession::applyImageSettings() +{ + if (!m_camera) + return; + + // only supported format right now. + m_actualImageSettings.setFormat(QImageCapture::JPEG); + + const QSize requestedResolution = m_requestedImageSettings.resolution(); + const QList<QSize> supportedResolutions = m_camera->getSupportedPictureSizes(); + if (!requestedResolution.isValid()) { + m_actualImageSettings.setResolution(getDefaultResolution()); + } else if (!supportedResolutions.contains(requestedResolution)) { + // if the requested resolution is not supported, find the closest one + int reqPixelCount = requestedResolution.width() * requestedResolution.height(); + QList<int> supportedPixelCounts; + for (int i = 0; i < supportedResolutions.size(); ++i) { + const QSize &s = supportedResolutions.at(i); + supportedPixelCounts.append(s.width() * s.height()); + } + int closestIndex = qt_findClosestValue(supportedPixelCounts, reqPixelCount); + m_actualImageSettings.setResolution(supportedResolutions.at(closestIndex)); + } + m_camera->setPictureSize(m_actualImageSettings.resolution()); + + int jpegQuality = 100; + switch (m_requestedImageSettings.quality()) { + case QImageCapture::VeryLowQuality: + jpegQuality = 20; + break; + case QImageCapture::LowQuality: + jpegQuality = 40; + break; + case QImageCapture::NormalQuality: + jpegQuality = 60; + break; + case QImageCapture::HighQuality: + jpegQuality = 80; + break; + case QImageCapture::VeryHighQuality: + jpegQuality = 100; + break; + } + m_camera->setJpegQuality(jpegQuality); +} + +bool QAndroidCameraSession::isReadyForCapture() const +{ + return isActive() && m_readyForCapture; +} + +void QAndroidCameraSession::setReadyForCapture(bool ready) +{ + if (m_readyForCapture == ready) + return; + + m_readyForCapture = ready; + emit readyForCaptureChanged(ready); +} + +int QAndroidCameraSession::captureImage() +{ + const int newImageCaptureId = m_currentImageCaptureId + 1; + + if (!isReadyForCapture()) { + emit imageCaptureError(newImageCaptureId, QImageCapture::NotReadyError, + QPlatformImageCapture::msgCameraNotReady()); + return newImageCaptureId; + } + + setReadyForCapture(false); + + m_currentImageCaptureId = newImageCaptureId; + + applyResolution(m_actualImageSettings.resolution()); + m_camera->takePicture(); + + return m_currentImageCaptureId; +} + +int QAndroidCameraSession::capture(const QString &fileName) +{ + m_currentImageCaptureFileName = fileName; + m_imageCaptureToBuffer = false; + return captureImage(); +} + +int QAndroidCameraSession::captureToBuffer() +{ + m_currentImageCaptureFileName.clear(); + m_imageCaptureToBuffer = true; + return captureImage(); +} + +void QAndroidCameraSession::onCameraTakePictureFailed() +{ + emit imageCaptureError(m_currentImageCaptureId, QImageCapture::ResourceError, + tr("Failed to capture image")); + + // Preview needs to be restarted and the preview call back must be setup again + m_camera->startPreview(); +} + +void QAndroidCameraSession::onCameraPictureExposed() +{ + if (!m_camera) + return; + + emit imageExposed(m_currentImageCaptureId); + m_camera->fetchLastPreviewFrame(); +} + +void QAndroidCameraSession::onLastPreviewFrameFetched(const QVideoFrame &frame) +{ + if (!m_camera) + return; + + updateOrientation(); + + (void)QtConcurrent::run(&QAndroidCameraSession::processPreviewImage, this, + m_currentImageCaptureId, frame, currentCameraRotation()); +} + +void QAndroidCameraSession::processPreviewImage(int id, const QVideoFrame &frame, int rotation) +{ + // Preview display of front-facing cameras is flipped horizontally, but the frame data + // we get here is not. Flip it ourselves if the camera is front-facing to match what the user + // sees on the viewfinder. + QTransform transform; + transform.rotate(rotation); + + if (m_camera->getFacing() == AndroidCamera::CameraFacingFront) + transform.scale(-1, 1); + + emit imageCaptured(id, frame.toImage().transformed(transform)); +} + +void QAndroidCameraSession::onNewPreviewFrame(const QVideoFrame &frame) +{ + if (!m_camera) + return; + + m_videoFrameCallbackMutex.lock(); + + if (m_previewCallback) + m_previewCallback->onFrameAvailable(frame); + + m_videoFrameCallbackMutex.unlock(); +} + +void QAndroidCameraSession::onCameraPictureCaptured(const QByteArray &bytes, + QVideoFrameFormat::PixelFormat format, QSize size,int bytesPerLine) +{ + if (m_imageCaptureToBuffer) { + processCapturedImageToBuffer(m_currentImageCaptureId, bytes, format, size, bytesPerLine); + } else { + // Loading and saving the captured image can be slow, do it in a separate thread + (void)QtConcurrent::run(&QAndroidCameraSession::processCapturedImage, this, + m_currentImageCaptureId, bytes, m_currentImageCaptureFileName); + } + + // Preview needs to be restarted after taking a picture + if (m_camera) + m_camera->startPreview(); +} + +void QAndroidCameraSession::onCameraPreviewStarted() +{ + setReadyForCapture(true); +} + +void QAndroidCameraSession::onCameraPreviewFailedToStart() +{ + if (isActive()) { + Q_EMIT error(QCamera::CameraError, tr("Camera preview failed to start.")); + + AndroidMultimediaUtils::enableOrientationListener(false); + m_camera->setPreviewSize(QSize()); + m_camera->setPreviewTexture(0); + if (m_videoOutput) { + m_videoOutput->stop(); + m_videoOutput->reset(); + } + m_previewStarted = false; + + setActive(false); + setReadyForCapture(false); + } +} + +void QAndroidCameraSession::onCameraPreviewStopped() +{ + if (!m_previewStarted) + setActive(false); + setReadyForCapture(false); +} + +void QAndroidCameraSession::processCapturedImage(int id, const QByteArray &bytes, const QString &fileName) +{ + const QString actualFileName = QMediaStorageLocation::generateFileName( + fileName, QStandardPaths::PicturesLocation, QLatin1String("jpg")); + QFile writer(actualFileName); + if (!writer.open(QIODeviceBase::WriteOnly)) { + const QString errorMessage = tr("File is not available: %1").arg(writer.errorString()); + emit imageCaptureError(id, QImageCapture::Error::ResourceError, errorMessage); + return; + } + + if (writer.write(bytes) < 0) { + const QString errorMessage = tr("Could not save to file: %1").arg(writer.errorString()); + emit imageCaptureError(id, QImageCapture::Error::ResourceError, errorMessage); + return; + } + + writer.close(); + if (fileName.isEmpty() || QFileInfo(fileName).isRelative()) + AndroidMultimediaUtils::registerMediaFile(actualFileName); + + emit imageSaved(id, actualFileName); +} + +void QAndroidCameraSession::processCapturedImageToBuffer(int id, const QByteArray &bytes, + QVideoFrameFormat::PixelFormat format, QSize size, int bytesPerLine) +{ + QVideoFrame frame = QVideoFramePrivate::createFrame( + std::make_unique<QMemoryVideoBuffer>(bytes, bytesPerLine), + QVideoFrameFormat(size, format)); + emit imageAvailable(id, frame); +} + +void QAndroidCameraSession::onVideoOutputReady(bool ready) +{ + if (ready && m_active) + startPreview(); +} + +void QAndroidCameraSession::onApplicationStateChanged() +{ + + switch (QGuiApplication::applicationState()) { + case Qt::ApplicationInactive: + if (!m_keepActive && m_active) { + m_savedState = m_active; + setActive(false); + m_isStateSaved = true; + } + break; + case Qt::ApplicationActive: + if (m_isStateSaved) { + setActive(m_savedState); + m_isStateSaved = false; + } + break; + default: + break; + } +} + +void QAndroidCameraSession::setKeepAlive(bool keepAlive) +{ + m_keepActive = keepAlive; +} + +void QAndroidCameraSession::setVideoSink(QVideoSink *sink) +{ + if (m_sink == sink) + return; + + if (m_sink) + disconnect(m_retryPreviewConnection); + + m_sink = sink; + + if (m_sink) + m_retryPreviewConnection = + connect(m_sink->platformVideoSink(), &QPlatformVideoSink::rhiChanged, this, [&]() + { + if (m_active) { + setActive(false); + setActive(true); + } + }, Qt::DirectConnection); + if (m_sink) { + delete m_textureOutput; + m_textureOutput = nullptr; + + m_textureOutput = new QAndroidTextureVideoOutput(m_sink, this); + } + + setVideoOutput(m_textureOutput); +} + +QT_END_NAMESPACE + +#include "moc_qandroidcamerasession_p.cpp" diff --git a/src/plugins/multimedia/android/mediacapture/qandroidcamerasession_p.h b/src/plugins/multimedia/android/mediacapture/qandroidcamerasession_p.h new file mode 100644 index 000000000..3b56d9c3b --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidcamerasession_p.h @@ -0,0 +1,166 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// Copyright (C) 2016 Ruslan Baratov +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QANDROIDCAMERASESSION_H +#define QANDROIDCAMERASESSION_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <qcamera.h> +#include <QImageCapture> +#include <QSet> +#include <QMutex> +#include <private/qplatformimagecapture_p.h> +#include "androidcamera_p.h" + +QT_BEGIN_NAMESPACE + +class QAndroidVideoOutput; +class QAndroidTextureVideoOutput ; +class QVideoSink; + +class QAndroidCameraSession : public QObject +{ + Q_OBJECT +public: + explicit QAndroidCameraSession(QObject *parent = 0); + ~QAndroidCameraSession(); + + static const QList<QCameraDevice> &availableCameras(); + + void setSelectedCameraId(int cameraId) { m_selectedCamera = cameraId; } + int getSelectedCameraId() { return m_selectedCamera; } + AndroidCamera *camera() const { return m_camera; } + + bool isActive() const { return m_active; } + void setActive(bool active); + + void applyResolution(const QSize &captureSize = QSize(), bool restartPreview = true); + + QAndroidVideoOutput *videoOutput() const { return m_videoOutput; } + void setVideoOutput(QAndroidVideoOutput *output); + + void setCameraFormat(const QCameraFormat &format); + + QList<QSize> getSupportedPreviewSizes() const; + QList<QVideoFrameFormat::PixelFormat> getSupportedPixelFormats() const; + QList<AndroidCamera::FpsRange> getSupportedPreviewFpsRange() const; + + QImageEncoderSettings imageSettings() const { return m_actualImageSettings; } + void setImageSettings(const QImageEncoderSettings &settings); + + bool isReadyForCapture() const; + void setReadyForCapture(bool ready); + int capture(const QString &fileName); + int captureToBuffer(); + + int currentCameraRotation() const; + + void setPreviewFormat(AndroidCamera::ImageFormat format); + + struct PreviewCallback + { + virtual void onFrameAvailable(const QVideoFrame &frame) = 0; + }; + void setPreviewCallback(PreviewCallback *callback); + + void setVideoSink(QVideoSink *surface); + + void disableRotation(); + void enableRotation(); + + void setKeepAlive(bool keepAlive); + +Q_SIGNALS: + void activeChanged(bool); + void error(int error, const QString &errorString); + void opened(); + + void readyForCaptureChanged(bool); + void imageExposed(int id); + void imageCaptured(int id, const QImage &preview); + void imageMetadataAvailable(int id, const QMediaMetaData &key); + void imageAvailable(int id, const QVideoFrame &buffer); + void imageSaved(int id, const QString &fileName); + void imageCaptureError(int id, int error, const QString &errorString); + +private Q_SLOTS: + void onVideoOutputReady(bool ready); + void updateOrientation(); + + void onApplicationStateChanged(); + + void onCameraTakePictureFailed(); + void onCameraPictureExposed(); + void onCameraPictureCaptured(const QByteArray &bytes, QVideoFrameFormat::PixelFormat format, QSize size, int bytesPerLine); + void onLastPreviewFrameFetched(const QVideoFrame &frame); + void onNewPreviewFrame(const QVideoFrame &frame); + void onCameraPreviewStarted(); + void onCameraPreviewFailedToStart(); + void onCameraPreviewStopped(); + +private: + static void updateAvailableCameras(); + + bool open(); + void close(); + + bool startPreview(); + void stopPreview(); + + void applyImageSettings(); + + void processPreviewImage(int id, const QVideoFrame &frame, int rotation); + void processCapturedImage(int id, const QByteArray &bytes, const QString &fileName); + void processCapturedImageToBuffer(int id, const QByteArray &bytes, + QVideoFrameFormat::PixelFormat format, QSize size, int bytesPerLine); + + void setActiveHelper(bool active); + + int captureImage(); + + QSize getDefaultResolution() const; + + int m_selectedCamera; + AndroidCamera *m_camera; + QAndroidVideoOutput *m_videoOutput; + + bool m_active = false; + bool m_isStateSaved = false; + bool m_savedState = false; + bool m_previewStarted; + + bool m_rotationEnabled = false; + + QVideoSink *m_sink = nullptr; + QAndroidTextureVideoOutput *m_textureOutput = nullptr; + + QImageEncoderSettings m_requestedImageSettings; + QImageEncoderSettings m_actualImageSettings; + AndroidCamera::FpsRange m_requestedFpsRange; + AndroidCamera::ImageFormat m_requestedPixelFromat = AndroidCamera::ImageFormat::NV21; + + bool m_readyForCapture; + int m_currentImageCaptureId; + QString m_currentImageCaptureFileName; + bool m_imageCaptureToBuffer; + + QMutex m_videoFrameCallbackMutex; + PreviewCallback *m_previewCallback; + bool m_keepActive; + QMetaObject::Connection m_retryPreviewConnection; +}; + +QT_END_NAMESPACE + +#endif // QANDROIDCAMERASESSION_H diff --git a/src/plugins/multimedia/android/mediacapture/qandroidcapturesession.cpp b/src/plugins/multimedia/android/mediacapture/qandroidcapturesession.cpp new file mode 100644 index 000000000..3b005e4a5 --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidcapturesession.cpp @@ -0,0 +1,473 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qandroidcapturesession_p.h" + +#include "androidcamera_p.h" +#include "qandroidcamerasession_p.h" +#include "qaudioinput.h" +#include "qaudiooutput.h" +#include "androidmediaplayer_p.h" +#include "androidmultimediautils_p.h" +#include "qandroidmultimediautils_p.h" +#include "qandroidvideooutput_p.h" +#include "qandroidglobal_p.h" +#include <private/qplatformaudioinput_p.h> +#include <private/qplatformaudiooutput_p.h> +#include <private/qmediarecorder_p.h> +#include <private/qmediastoragelocation_p.h> +#include <QtCore/qmimetype.h> + +#include <algorithm> + +QT_BEGIN_NAMESPACE + +QAndroidCaptureSession::QAndroidCaptureSession() + : QObject() + , m_mediaRecorder(0) + , m_cameraSession(0) + , m_duration(0) + , m_state(QMediaRecorder::StoppedState) + , m_outputFormat(AndroidMediaRecorder::DefaultOutputFormat) + , m_audioEncoder(AndroidMediaRecorder::DefaultAudioEncoder) + , m_videoEncoder(AndroidMediaRecorder::DefaultVideoEncoder) +{ + m_notifyTimer.setInterval(1000); + connect(&m_notifyTimer, &QTimer::timeout, this, &QAndroidCaptureSession::updateDuration); +} + +QAndroidCaptureSession::~QAndroidCaptureSession() +{ + stop(); + m_mediaRecorder = nullptr; + if (m_audioInput && m_audioOutput) + AndroidMediaPlayer::stopSoundStreaming(); +} + +void QAndroidCaptureSession::setCameraSession(QAndroidCameraSession *cameraSession) +{ + if (m_cameraSession) { + disconnect(m_connOpenCamera); + disconnect(m_connActiveChangedCamera); + } + + m_cameraSession = cameraSession; + if (m_cameraSession) { + m_connOpenCamera = connect(cameraSession, &QAndroidCameraSession::opened, + this, &QAndroidCaptureSession::onCameraOpened); + m_connActiveChangedCamera = connect(cameraSession, &QAndroidCameraSession::activeChanged, + this, [this](bool isActive) { + if (!isActive) + stop(); + }); + } +} + +void QAndroidCaptureSession::setAudioInput(QPlatformAudioInput *input) +{ + if (m_audioInput == input) + return; + + if (m_audioInput) { + disconnect(m_audioInputChanged); + } + + m_audioInput = input; + + if (m_audioInput) { + m_audioInputChanged = connect(m_audioInput->q, &QAudioInput::deviceChanged, this, [this]() { + if (m_state == QMediaRecorder::RecordingState) + m_mediaRecorder->setAudioInput(m_audioInput->device.id()); + updateStreamingState(); + }); + } + updateStreamingState(); +} + +void QAndroidCaptureSession::setAudioOutput(QPlatformAudioOutput *output) +{ + if (m_audioOutput == output) + return; + + if (m_audioOutput) + disconnect(m_audioOutputChanged); + + m_audioOutput = output; + + if (m_audioOutput) { + m_audioOutputChanged = connect(m_audioOutput->q, &QAudioOutput::deviceChanged, this, + [this] () { + AndroidMediaPlayer::setAudioOutput(m_audioOutput->device.id()); + updateStreamingState(); + }); + AndroidMediaPlayer::setAudioOutput(m_audioOutput->device.id()); + } + updateStreamingState(); +} + +void QAndroidCaptureSession::updateStreamingState() +{ + if (m_audioInput && m_audioOutput) { + AndroidMediaPlayer::startSoundStreaming(m_audioInput->device.id().toInt(), + m_audioOutput->device.id().toInt()); + } else { + AndroidMediaPlayer::stopSoundStreaming(); + } +} + +QMediaRecorder::RecorderState QAndroidCaptureSession::state() const +{ + return m_state; +} + +void QAndroidCaptureSession::setKeepAlive(bool keepAlive) +{ + if (m_cameraSession) + m_cameraSession->setKeepAlive(keepAlive); +} + + +void QAndroidCaptureSession::start(QMediaEncoderSettings &settings, const QUrl &outputLocation) +{ + if (m_state == QMediaRecorder::RecordingState) + return; + + if (!m_cameraSession && !m_audioInput) { + updateError(QMediaRecorder::ResourceError, QLatin1String("No devices are set")); + return; + } + + setKeepAlive(true); + + const bool validCameraSession = m_cameraSession && m_cameraSession->camera(); + + if (validCameraSession && !qt_androidCheckCameraPermission()) { + updateError(QMediaRecorder::ResourceError, QLatin1String("Camera permission denied.")); + setKeepAlive(false); + return; + } + + if (m_audioInput && !qt_androidCheckMicrophonePermission()) { + updateError(QMediaRecorder::ResourceError, QLatin1String("Microphone permission denied.")); + setKeepAlive(false); + return; + } + + m_mediaRecorder = std::make_shared<AndroidMediaRecorder>(); + connect(m_mediaRecorder.get(), &AndroidMediaRecorder::error, this, + &QAndroidCaptureSession::onError); + connect(m_mediaRecorder.get(), &AndroidMediaRecorder::info, this, + &QAndroidCaptureSession::onInfo); + + applySettings(settings); + + // Set audio/video sources + if (validCameraSession) { + m_cameraSession->camera()->stopPreviewSynchronous(); + m_cameraSession->camera()->unlock(); + + m_mediaRecorder->setCamera(m_cameraSession->camera()); + m_mediaRecorder->setVideoSource(AndroidMediaRecorder::Camera); + } + + if (m_audioInput) { + m_mediaRecorder->setAudioInput(m_audioInput->device.id()); + if (!m_mediaRecorder->isAudioSourceSet()) + m_mediaRecorder->setAudioSource(AndroidMediaRecorder::DefaultAudioSource); + } + + // Set output format + m_mediaRecorder->setOutputFormat(m_outputFormat); + + // Set video encoder settings + if (validCameraSession) { + m_mediaRecorder->setVideoSize(settings.videoResolution()); + m_mediaRecorder->setVideoFrameRate(qRound(settings.videoFrameRate())); + m_mediaRecorder->setVideoEncodingBitRate(settings.videoBitRate()); + m_mediaRecorder->setVideoEncoder(m_videoEncoder); + + // media recorder is also compensanting the mirror on front camera + auto rotation = m_cameraSession->currentCameraRotation(); + if (m_cameraSession->camera()->getFacing() == AndroidCamera::CameraFacingFront) + rotation = (360 - rotation) % 360; // remove mirror compensation + + m_mediaRecorder->setOrientationHint(rotation); + } + + // Set audio encoder settings + if (m_audioInput) { + m_mediaRecorder->setAudioChannels(settings.audioChannelCount()); + m_mediaRecorder->setAudioEncodingBitRate(settings.audioBitRate()); + m_mediaRecorder->setAudioSamplingRate(settings.audioSampleRate()); + m_mediaRecorder->setAudioEncoder(m_audioEncoder); + } + + QString extension = settings.mimeType().preferredSuffix(); + // Set output file + auto location = outputLocation.toString(QUrl::PreferLocalFile); + QString filePath = location; + if (QUrl(filePath).scheme() != QLatin1String("content")) { + filePath = QMediaStorageLocation::generateFileName( + location, m_cameraSession ? QStandardPaths::MoviesLocation + : QStandardPaths::MusicLocation, extension); + } + + m_usedOutputLocation = QUrl::fromLocalFile(filePath); + m_outputLocationIsStandard = location.isEmpty() || QFileInfo(location).isRelative(); + m_mediaRecorder->setOutputFile(filePath); + + if (validCameraSession) { + m_cameraSession->disableRotation(); + } + + if (!m_mediaRecorder->prepare()) { + updateError(QMediaRecorder::FormatError, + QLatin1String("Unable to prepare the media recorder.")); + restartViewfinder(); + + return; + } + + if (!m_mediaRecorder->start()) { + updateError(QMediaRecorder::FormatError, QMediaRecorderPrivate::msgFailedStartRecording()); + restartViewfinder(); + + return; + } + + m_elapsedTime.start(); + m_notifyTimer.start(); + updateDuration(); + + if (validCameraSession) { + m_cameraSession->setReadyForCapture(false); + + // Preview frame callback is cleared when setting up the camera with the media recorder. + // We need to reset it. + m_cameraSession->camera()->setupPreviewFrameCallback(); + } + + m_state = QMediaRecorder::RecordingState; + emit stateChanged(m_state); +} + +void QAndroidCaptureSession::stop(bool error) +{ + if (m_state == QMediaRecorder::StoppedState || m_mediaRecorder == nullptr) + return; + + m_mediaRecorder->stop(); + m_notifyTimer.stop(); + updateDuration(); + m_elapsedTime.invalidate(); + + m_mediaRecorder = nullptr; + + if (m_cameraSession && m_cameraSession->isActive()) { + // Viewport needs to be restarted after recording + restartViewfinder(); + } + + if (!error) { + // if the media is saved into the standard media location, register it + // with the Android media scanner so it appears immediately in apps + // such as the gallery. + if (m_outputLocationIsStandard) + AndroidMultimediaUtils::registerMediaFile(m_usedOutputLocation.toLocalFile()); + + emit actualLocationChanged(m_usedOutputLocation); + } + + m_state = QMediaRecorder::StoppedState; + emit stateChanged(m_state); +} + +qint64 QAndroidCaptureSession::duration() const +{ + return m_duration; +} + +void QAndroidCaptureSession::applySettings(QMediaEncoderSettings &settings) +{ + // container settings + auto fileFormat = settings.mediaFormat().fileFormat(); + if (!m_cameraSession && fileFormat == QMediaFormat::AAC) { + m_outputFormat = AndroidMediaRecorder::AAC_ADTS; + } else if (fileFormat == QMediaFormat::Ogg) { + m_outputFormat = AndroidMediaRecorder::OGG; + } else if (fileFormat == QMediaFormat::WebM) { + m_outputFormat = AndroidMediaRecorder::WEBM; +// } else if (fileFormat == QLatin1String("3gp")) { +// m_outputFormat = AndroidMediaRecorder::THREE_GPP; + } else { + // fallback to MP4 + m_outputFormat = AndroidMediaRecorder::MPEG_4; + } + + // audio settings + if (settings.audioChannelCount() <= 0) + settings.setAudioChannelCount(m_defaultSettings.audioChannels); + if (settings.audioBitRate() <= 0) + settings.setAudioBitRate(m_defaultSettings.audioBitRate); + if (settings.audioSampleRate() <= 0) + settings.setAudioSampleRate(m_defaultSettings.audioSampleRate); + + if (settings.audioCodec() == QMediaFormat::AudioCodec::AAC) + m_audioEncoder = AndroidMediaRecorder::AAC; + else if (settings.audioCodec() == QMediaFormat::AudioCodec::Opus) + m_audioEncoder = AndroidMediaRecorder::OPUS; + else if (settings.audioCodec() == QMediaFormat::AudioCodec::Vorbis) + m_audioEncoder = AndroidMediaRecorder::VORBIS; + else + m_audioEncoder = m_defaultSettings.audioEncoder; + + + // video settings + if (m_cameraSession && m_cameraSession->camera()) { + if (settings.videoResolution().isEmpty()) { + settings.setVideoResolution(m_defaultSettings.videoResolution); + } else if (!m_supportedResolutions.contains(settings.videoResolution())) { + // if the requested resolution is not supported, find the closest one + QSize reqSize = settings.videoResolution(); + int reqPixelCount = reqSize.width() * reqSize.height(); + QList<int> supportedPixelCounts; + for (int i = 0; i < m_supportedResolutions.size(); ++i) { + const QSize &s = m_supportedResolutions.at(i); + supportedPixelCounts.append(s.width() * s.height()); + } + int closestIndex = qt_findClosestValue(supportedPixelCounts, reqPixelCount); + settings.setVideoResolution(m_supportedResolutions.at(closestIndex)); + } + + if (settings.videoFrameRate() <= 0) + settings.setVideoFrameRate(m_defaultSettings.videoFrameRate); + if (settings.videoBitRate() <= 0) + settings.setVideoBitRate(m_defaultSettings.videoBitRate); + + if (settings.videoCodec() == QMediaFormat::VideoCodec::H264) + m_videoEncoder = AndroidMediaRecorder::H264; + else if (settings.videoCodec() == QMediaFormat::VideoCodec::H265) + m_videoEncoder = AndroidMediaRecorder::HEVC; + else if (settings.videoCodec() == QMediaFormat::VideoCodec::MPEG4) + m_videoEncoder = AndroidMediaRecorder::MPEG_4_SP; + else + m_videoEncoder = m_defaultSettings.videoEncoder; + + } +} + +void QAndroidCaptureSession::restartViewfinder() +{ + + setKeepAlive(false); + + if (!m_cameraSession) + return; + + if (m_cameraSession && m_cameraSession->camera()) { + m_cameraSession->camera()->reconnect(); + m_cameraSession->camera()->stopPreviewSynchronous(); + m_cameraSession->camera()->startPreview(); + m_cameraSession->setReadyForCapture(true); + m_cameraSession->enableRotation(); + } + + m_mediaRecorder = nullptr; +} + +void QAndroidCaptureSession::updateDuration() +{ + if (m_elapsedTime.isValid()) + m_duration = m_elapsedTime.elapsed(); + + emit durationChanged(m_duration); +} + +void QAndroidCaptureSession::onCameraOpened() +{ + m_supportedResolutions.clear(); + m_supportedFramerates.clear(); + + // get supported resolutions from predefined profiles + for (int i = 0; i < 8; ++i) { + CaptureProfile profile = getProfile(i); + if (!profile.isNull) { + if (i == AndroidCamcorderProfile::QUALITY_HIGH) + m_defaultSettings = profile; + + if (!m_supportedResolutions.contains(profile.videoResolution)) + m_supportedResolutions.append(profile.videoResolution); + if (!m_supportedFramerates.contains(profile.videoFrameRate)) + m_supportedFramerates.append(profile.videoFrameRate); + } + } + + std::sort(m_supportedResolutions.begin(), m_supportedResolutions.end(), qt_sizeLessThan); + std::sort(m_supportedFramerates.begin(), m_supportedFramerates.end()); + + QMediaEncoderSettings defaultSettings; + applySettings(defaultSettings); + m_cameraSession->applyResolution(defaultSettings.videoResolution()); +} + +QAndroidCaptureSession::CaptureProfile QAndroidCaptureSession::getProfile(int id) +{ + CaptureProfile profile; + const bool hasProfile = AndroidCamcorderProfile::hasProfile(m_cameraSession->camera()->cameraId(), + AndroidCamcorderProfile::Quality(id)); + + if (hasProfile) { + AndroidCamcorderProfile camProfile = AndroidCamcorderProfile::get(m_cameraSession->camera()->cameraId(), + AndroidCamcorderProfile::Quality(id)); + + profile.outputFormat = AndroidMediaRecorder::OutputFormat(camProfile.getValue(AndroidCamcorderProfile::fileFormat)); + profile.audioEncoder = AndroidMediaRecorder::AudioEncoder(camProfile.getValue(AndroidCamcorderProfile::audioCodec)); + profile.audioBitRate = camProfile.getValue(AndroidCamcorderProfile::audioBitRate); + profile.audioChannels = camProfile.getValue(AndroidCamcorderProfile::audioChannels); + profile.audioSampleRate = camProfile.getValue(AndroidCamcorderProfile::audioSampleRate); + profile.videoEncoder = AndroidMediaRecorder::VideoEncoder(camProfile.getValue(AndroidCamcorderProfile::videoCodec)); + profile.videoBitRate = camProfile.getValue(AndroidCamcorderProfile::videoBitRate); + profile.videoFrameRate = camProfile.getValue(AndroidCamcorderProfile::videoFrameRate); + profile.videoResolution = QSize(camProfile.getValue(AndroidCamcorderProfile::videoFrameWidth), + camProfile.getValue(AndroidCamcorderProfile::videoFrameHeight)); + + if (profile.outputFormat == AndroidMediaRecorder::MPEG_4) + profile.outputFileExtension = QStringLiteral("mp4"); + else if (profile.outputFormat == AndroidMediaRecorder::THREE_GPP) + profile.outputFileExtension = QStringLiteral("3gp"); + else if (profile.outputFormat == AndroidMediaRecorder::AMR_NB_Format) + profile.outputFileExtension = QStringLiteral("amr"); + else if (profile.outputFormat == AndroidMediaRecorder::AMR_WB_Format) + profile.outputFileExtension = QStringLiteral("awb"); + + profile.isNull = false; + } + + return profile; +} + +void QAndroidCaptureSession::onError(int what, int extra) +{ + Q_UNUSED(what); + Q_UNUSED(extra); + stop(true); + updateError(QMediaRecorder::ResourceError, QLatin1String("Unknown error.")); +} + +void QAndroidCaptureSession::onInfo(int what, int extra) +{ + Q_UNUSED(extra); + if (what == 800) { + // MEDIA_RECORDER_INFO_MAX_DURATION_REACHED + stop(); + updateError(QMediaRecorder::OutOfSpaceError, QLatin1String("Maximum duration reached.")); + } else if (what == 801) { + // MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED + stop(); + updateError(QMediaRecorder::OutOfSpaceError, QLatin1String("Maximum file size reached.")); + } +} + +QT_END_NAMESPACE + +#include "moc_qandroidcapturesession_p.cpp" diff --git a/src/plugins/multimedia/android/mediacapture/qandroidcapturesession_p.h b/src/plugins/multimedia/android/mediacapture/qandroidcapturesession_p.h new file mode 100644 index 000000000..161d47994 --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidcapturesession_p.h @@ -0,0 +1,158 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QANDROIDCAPTURESESSION_H +#define QANDROIDCAPTURESESSION_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <qobject.h> +#include <qmediarecorder.h> +#include <qurl.h> +#include <qelapsedtimer.h> +#include <qtimer.h> +#include "androidmediarecorder_p.h" +#include "qandroidmediaencoder_p.h" + +QT_BEGIN_NAMESPACE + +class QAudioInput; +class QAndroidCameraSession; + +class QAndroidCaptureSession : public QObject +{ + Q_OBJECT +public: + explicit QAndroidCaptureSession(); + ~QAndroidCaptureSession(); + + QList<QSize> supportedResolutions() const { return m_supportedResolutions; } + QList<qreal> supportedFrameRates() const { return m_supportedFramerates; } + + void setCameraSession(QAndroidCameraSession *cameraSession = 0); + void setAudioInput(QPlatformAudioInput *input); + void setAudioOutput(QPlatformAudioOutput *output); + + QMediaRecorder::RecorderState state() const; + + void start(QMediaEncoderSettings &settings, const QUrl &outputLocation); + void stop(bool error = false); + + qint64 duration() const; + + QMediaEncoderSettings encoderSettings() { return m_encoderSettings; } + + void setMediaEncoder(QAndroidMediaEncoder *encoder) { m_mediaEncoder = encoder; } + + void stateChanged(QMediaRecorder::RecorderState state) { + if (m_mediaEncoder) + m_mediaEncoder->stateChanged(state); + } + void durationChanged(qint64 position) + { + if (m_mediaEncoder) + m_mediaEncoder->durationChanged(position); + } + void actualLocationChanged(const QUrl &location) + { + if (m_mediaEncoder) + m_mediaEncoder->actualLocationChanged(location); + } + void updateError(int error, const QString &errorString) + { + if (m_mediaEncoder) + m_mediaEncoder->updateError(QMediaRecorder::Error(error), errorString); + } + +private Q_SLOTS: + void updateDuration(); + void onCameraOpened(); + + void onError(int what, int extra); + void onInfo(int what, int extra); + +private: + void applySettings(QMediaEncoderSettings &settings); + + struct CaptureProfile { + AndroidMediaRecorder::OutputFormat outputFormat; + QString outputFileExtension; + + AndroidMediaRecorder::AudioEncoder audioEncoder; + int audioBitRate; + int audioChannels; + int audioSampleRate; + + AndroidMediaRecorder::VideoEncoder videoEncoder; + int videoBitRate; + int videoFrameRate; + QSize videoResolution; + + bool isNull; + + CaptureProfile() + : outputFormat(AndroidMediaRecorder::MPEG_4) + , outputFileExtension(QLatin1String("mp4")) + , audioEncoder(AndroidMediaRecorder::DefaultAudioEncoder) + , audioBitRate(128000) + , audioChannels(2) + , audioSampleRate(44100) + , videoEncoder(AndroidMediaRecorder::DefaultVideoEncoder) + , videoBitRate(1) + , videoFrameRate(-1) + , videoResolution(1280, 720) + , isNull(true) + { } + }; + + CaptureProfile getProfile(int id); + + void restartViewfinder(); + void updateStreamingState(); + + QAndroidMediaEncoder *m_mediaEncoder = nullptr; + std::shared_ptr<AndroidMediaRecorder> m_mediaRecorder; + QAndroidCameraSession *m_cameraSession; + + QPlatformAudioInput *m_audioInput = nullptr; + QPlatformAudioOutput *m_audioOutput = nullptr; + + QElapsedTimer m_elapsedTime; + QTimer m_notifyTimer; + qint64 m_duration; + + QMediaRecorder::RecorderState m_state; + QUrl m_usedOutputLocation; + bool m_outputLocationIsStandard = false; + + CaptureProfile m_defaultSettings; + + QMediaEncoderSettings m_encoderSettings; + AndroidMediaRecorder::OutputFormat m_outputFormat; + AndroidMediaRecorder::AudioEncoder m_audioEncoder; + AndroidMediaRecorder::VideoEncoder m_videoEncoder; + + QList<QSize> m_supportedResolutions; + QList<qreal> m_supportedFramerates; + + QMetaObject::Connection m_audioInputChanged; + QMetaObject::Connection m_audioOutputChanged; + QMetaObject::Connection m_connOpenCamera; + QMetaObject::Connection m_connActiveChangedCamera; + + void setKeepAlive(bool keepAlive); + +}; + +QT_END_NAMESPACE + +#endif // QANDROIDCAPTURESESSION_H diff --git a/src/plugins/multimedia/android/mediacapture/qandroidimagecapture.cpp b/src/plugins/multimedia/android/mediacapture/qandroidimagecapture.cpp new file mode 100644 index 000000000..4105851ed --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidimagecapture.cpp @@ -0,0 +1,73 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qandroidimagecapture_p.h" + +#include "qandroidcamerasession_p.h" +#include "qandroidmediacapturesession_p.h" + +QT_BEGIN_NAMESPACE + +QAndroidImageCapture::QAndroidImageCapture(QImageCapture *parent) + : QPlatformImageCapture(parent) +{ +} + +bool QAndroidImageCapture::isReadyForCapture() const +{ + return m_session->isReadyForCapture(); +} + +int QAndroidImageCapture::capture(const QString &fileName) +{ + return m_session->capture(fileName); +} + +int QAndroidImageCapture::captureToBuffer() +{ + return m_session->captureToBuffer(); +} + +QImageEncoderSettings QAndroidImageCapture::imageSettings() const +{ + return m_session->imageSettings(); +} + +void QAndroidImageCapture::setImageSettings(const QImageEncoderSettings &settings) +{ + m_session->setImageSettings(settings); +} + +void QAndroidImageCapture::setCaptureSession(QPlatformMediaCaptureSession *session) +{ + QAndroidMediaCaptureSession *captureSession = static_cast<QAndroidMediaCaptureSession *>(session); + if (m_service == captureSession) + return; + + m_service = captureSession; + if (!m_service) { + disconnect(m_session, nullptr, this, nullptr); + return; + } + + m_session = m_service->cameraSession(); + Q_ASSERT(m_session); + + connect(m_session, &QAndroidCameraSession::readyForCaptureChanged, + this, &QAndroidImageCapture::readyForCaptureChanged); + connect(m_session, &QAndroidCameraSession::imageExposed, + this, &QAndroidImageCapture::imageExposed); + connect(m_session, &QAndroidCameraSession::imageCaptured, + this, &QAndroidImageCapture::imageCaptured); + connect(m_session, &QAndroidCameraSession::imageMetadataAvailable, + this, &QAndroidImageCapture::imageMetadataAvailable); + connect(m_session, &QAndroidCameraSession::imageAvailable, + this, &QAndroidImageCapture::imageAvailable); + connect(m_session, &QAndroidCameraSession::imageSaved, + this, &QAndroidImageCapture::imageSaved); + connect(m_session, &QAndroidCameraSession::imageCaptureError, + this, &QAndroidImageCapture::error); +} +QT_END_NAMESPACE + +#include "moc_qandroidimagecapture_p.cpp" diff --git a/src/plugins/multimedia/android/mediacapture/qandroidimagecapture_p.h b/src/plugins/multimedia/android/mediacapture/qandroidimagecapture_p.h new file mode 100644 index 000000000..ac273c195 --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidimagecapture_p.h @@ -0,0 +1,48 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QANDROIDCAMERAIMAGECAPTURECONTROL_H +#define QANDROIDCAMERAIMAGECAPTURECONTROL_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qplatformimagecapture_p.h> + +QT_BEGIN_NAMESPACE + +class QAndroidCameraSession; +class QAndroidMediaCaptureSession; + +class QAndroidImageCapture : public QPlatformImageCapture +{ + Q_OBJECT +public: + explicit QAndroidImageCapture(QImageCapture *parent = nullptr); + + bool isReadyForCapture() const override; + + int capture(const QString &fileName) override; + int captureToBuffer() override; + + QImageEncoderSettings imageSettings() const override; + void setImageSettings(const QImageEncoderSettings &settings) override; + + void setCaptureSession(QPlatformMediaCaptureSession *session); + +private: + QAndroidCameraSession *m_session; + QAndroidMediaCaptureSession *m_service; +}; + +QT_END_NAMESPACE + +#endif // QANDROIDCAMERAIMAGECAPTURECONTROL_H diff --git a/src/plugins/multimedia/android/mediacapture/qandroidmediacapturesession.cpp b/src/plugins/multimedia/android/mediacapture/qandroidmediacapturesession.cpp new file mode 100644 index 000000000..e2b551d35 --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidmediacapturesession.cpp @@ -0,0 +1,115 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// Copyright (C) 2016 Ruslan Baratov +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qandroidmediacapturesession_p.h" + +#include "qandroidmediaencoder_p.h" +#include "qandroidcapturesession_p.h" +#include "qandroidcamera_p.h" +#include "qandroidcamerasession_p.h" +#include "qandroidimagecapture_p.h" +#include "qmediadevices.h" +#include "qaudiodevice.h" + +QT_BEGIN_NAMESPACE + +QAndroidMediaCaptureSession::QAndroidMediaCaptureSession() + : m_captureSession(new QAndroidCaptureSession()) + , m_cameraSession(new QAndroidCameraSession()) +{ +} + +QAndroidMediaCaptureSession::~QAndroidMediaCaptureSession() +{ + delete m_captureSession; + delete m_cameraSession; +} + +QPlatformCamera *QAndroidMediaCaptureSession::camera() +{ + return m_cameraControl; +} + +void QAndroidMediaCaptureSession::setCamera(QPlatformCamera *camera) +{ + if (camera) { + m_captureSession->setCameraSession(m_cameraSession); + } else { + m_captureSession->setCameraSession(nullptr); + } + + QAndroidCamera *control = static_cast<QAndroidCamera *>(camera); + if (m_cameraControl == control) + return; + + if (m_cameraControl) + m_cameraControl->setCaptureSession(nullptr); + + m_cameraControl = control; + if (m_cameraControl) + m_cameraControl->setCaptureSession(this); + + emit cameraChanged(); +} + +QPlatformImageCapture *QAndroidMediaCaptureSession::imageCapture() +{ + return m_imageCaptureControl; +} + +void QAndroidMediaCaptureSession::setImageCapture(QPlatformImageCapture *imageCapture) +{ + QAndroidImageCapture *control = static_cast<QAndroidImageCapture *>(imageCapture); + if (m_imageCaptureControl == control) + return; + + if (m_imageCaptureControl) + m_imageCaptureControl->setCaptureSession(nullptr); + + m_imageCaptureControl = control; + if (m_imageCaptureControl) + m_imageCaptureControl->setCaptureSession(this); +} + +QPlatformMediaRecorder *QAndroidMediaCaptureSession::mediaRecorder() +{ + return m_encoder; +} + +void QAndroidMediaCaptureSession::setMediaRecorder(QPlatformMediaRecorder *recorder) +{ + QAndroidMediaEncoder *control = static_cast<QAndroidMediaEncoder *>(recorder); + + if (m_encoder == control) + return; + + if (m_encoder) + m_encoder->setCaptureSession(nullptr); + + m_encoder = control; + if (m_encoder) + m_encoder->setCaptureSession(this); + + emit encoderChanged(); + +} + +void QAndroidMediaCaptureSession::setAudioInput(QPlatformAudioInput *input) +{ + m_captureSession->setAudioInput(input); +} + +void QAndroidMediaCaptureSession::setAudioOutput(QPlatformAudioOutput *output) +{ + m_captureSession->setAudioOutput(output); +} + +void QAndroidMediaCaptureSession::setVideoPreview(QVideoSink *sink) +{ + m_cameraSession->setVideoSink(sink); +} + +QT_END_NAMESPACE + +#include "moc_qandroidmediacapturesession_p.cpp" diff --git a/src/plugins/multimedia/android/mediacapture/qandroidmediacapturesession_p.h b/src/plugins/multimedia/android/mediacapture/qandroidmediacapturesession_p.h new file mode 100644 index 000000000..90c792c32 --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidmediacapturesession_p.h @@ -0,0 +1,66 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// Copyright (C) 2016 Ruslan Baratov +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QANDROIDCAPTURESERVICE_H +#define QANDROIDCAPTURESERVICE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qplatformmediacapture_p.h> +#include <private/qplatformmediaintegration_p.h> + +QT_BEGIN_NAMESPACE + +class QAndroidMediaEncoder; +class QAndroidCaptureSession; +class QAndroidCamera; +class QAndroidCameraSession; +class QAndroidImageCapture; + +class QAndroidMediaCaptureSession : public QPlatformMediaCaptureSession +{ + Q_OBJECT + +public: + explicit QAndroidMediaCaptureSession(); + virtual ~QAndroidMediaCaptureSession(); + + QPlatformCamera *camera() override; + void setCamera(QPlatformCamera *camera) override; + + QPlatformImageCapture *imageCapture() override; + void setImageCapture(QPlatformImageCapture *imageCapture) override; + + QPlatformMediaRecorder *mediaRecorder() override; + void setMediaRecorder(QPlatformMediaRecorder *recorder) override; + + void setAudioInput(QPlatformAudioInput *input) override; + + void setVideoPreview(QVideoSink *sink) override; + + void setAudioOutput(QPlatformAudioOutput *output) override; + + QAndroidCaptureSession *captureSession() const { return m_captureSession; } + QAndroidCameraSession *cameraSession() const { return m_cameraSession; } + +private: + QAndroidMediaEncoder *m_encoder = nullptr; + QAndroidCaptureSession *m_captureSession = nullptr; + QAndroidCamera *m_cameraControl = nullptr; + QAndroidCameraSession *m_cameraSession = nullptr; + QAndroidImageCapture *m_imageCaptureControl = nullptr; +}; + +QT_END_NAMESPACE + +#endif // QANDROIDCAPTURESERVICE_H diff --git a/src/plugins/multimedia/android/mediacapture/qandroidmediaencoder.cpp b/src/plugins/multimedia/android/mediacapture/qandroidmediaencoder.cpp new file mode 100644 index 000000000..d3449312d --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidmediaencoder.cpp @@ -0,0 +1,72 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qandroidmediaencoder_p.h" +#include "qandroidmultimediautils_p.h" +#include "qandroidcapturesession_p.h" +#include "qandroidmediacapturesession_p.h" + +QT_BEGIN_NAMESPACE + +QAndroidMediaEncoder::QAndroidMediaEncoder(QMediaRecorder *parent) + : QPlatformMediaRecorder(parent) +{ +} + +bool QAndroidMediaEncoder::isLocationWritable(const QUrl &location) const +{ + return location.isValid() + && (location.isLocalFile() || location.isRelative()); +} + +QMediaRecorder::RecorderState QAndroidMediaEncoder::state() const +{ + return m_session ? m_session->state() : QMediaRecorder::StoppedState; +} + +qint64 QAndroidMediaEncoder::duration() const +{ + return m_session ? m_session->duration() : 0; + +} + +void QAndroidMediaEncoder::record(QMediaEncoderSettings &settings) +{ + if (m_session) + m_session->start(settings, outputLocation()); +} + +void QAndroidMediaEncoder::stop() +{ + if (m_session) + m_session->stop(); +} + +void QAndroidMediaEncoder::setOutputLocation(const QUrl &location) +{ + if (location.isLocalFile()) { + qt_androidRequestWriteStoragePermission(); + } + QPlatformMediaRecorder::setOutputLocation(location); +} + +void QAndroidMediaEncoder::setCaptureSession(QPlatformMediaCaptureSession *session) +{ + QAndroidMediaCaptureSession *captureSession = static_cast<QAndroidMediaCaptureSession *>(session); + if (m_service == captureSession) + return; + + if (m_service) + stop(); + if (m_session) + m_session->setMediaEncoder(nullptr); + + m_service = captureSession; + if (!m_service) + return; + m_session = m_service->captureSession(); + Q_ASSERT(m_session); + m_session->setMediaEncoder(this); +} + +QT_END_NAMESPACE diff --git a/src/plugins/multimedia/android/mediacapture/qandroidmediaencoder_p.h b/src/plugins/multimedia/android/mediacapture/qandroidmediaencoder_p.h new file mode 100644 index 000000000..b46268449 --- /dev/null +++ b/src/plugins/multimedia/android/mediacapture/qandroidmediaencoder_p.h @@ -0,0 +1,50 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QANDROIDMEDIAENCODER_H +#define QANDROIDMEDIAENCODER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qplatformmediarecorder_p.h> +#include <private/qplatformmediacapture_p.h> + +QT_BEGIN_NAMESPACE + +class QAndroidCaptureSession; +class QAndroidMediaCaptureSession; + +class QAndroidMediaEncoder : public QPlatformMediaRecorder +{ +public: + explicit QAndroidMediaEncoder(QMediaRecorder *parent); + + bool isLocationWritable(const QUrl &location) const override; + QMediaRecorder::RecorderState state() const override; + qint64 duration() const override; + + void setCaptureSession(QPlatformMediaCaptureSession *session); + + void setOutputLocation(const QUrl &location) override; + void record(QMediaEncoderSettings &settings) override; + void stop() override; + +private: + friend class QAndroidCaptureSession; + + QAndroidCaptureSession *m_session = nullptr; + QAndroidMediaCaptureSession *m_service = nullptr; +}; + +QT_END_NAMESPACE + +#endif // QANDROIDMEDIAENCODER_H |