Revision control

Copy as Markdown

// 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);
Q_EMIT activeChanged(m_active);
}
void QAndroidCameraSession::setActiveHelper(bool active)
{
if (!active) {
stopPreview();
close();
} else {
if (!m_camera && !open()) {
Q_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);
Q_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 (QtPrivate::fuzzyCompare(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;
Q_EMIT readyForCaptureChanged(ready);
}
int QAndroidCameraSession::captureImage()
{
const int newImageCaptureId = m_currentImageCaptureId + 1;
if (!isReadyForCapture()) {
Q_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()
{
Q_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;
Q_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);
Q_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());
Q_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());
Q_EMIT imageCaptureError(id, QImageCapture::Error::ResourceError, errorMessage);
return;
}
writer.close();
if (fileName.isEmpty() || QFileInfo(fileName).isRelative())
AndroidMultimediaUtils::registerMediaFile(actualFileName);
Q_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));
Q_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"