blob: 7540ab86c99c0f885d74ace822836aa1e4a94237 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "avfcameraexposurecontrol.h"
#include "avfcamerautility.h"
#include "avfcamerasession.h"
#include "avfcameraservice.h"
#include "avfcameradebug.h"
#include <QtCore/qvariant.h>
#include <QtCore/qpointer.h>
#include <QtCore/qdebug.h>
#include <QtCore/qpair.h>
#include <AVFoundation/AVFoundation.h>
#include <limits>
QT_BEGIN_NAMESPACE
namespace {
// All these methods to work with exposure/ISO/SS in custom mode do not support macOS.
#ifdef Q_OS_IOS
// Misc. helpers to check values/ranges:
bool qt_check_ISO_conversion(float isoValue)
{
if (isoValue >= std::numeric_limits<int>::max())
return false;
if (isoValue <= std::numeric_limits<int>::min())
return false;
return true;
}
bool qt_check_ISO_range(AVCaptureDeviceFormat *format)
{
// Qt is using int for ISO, AVFoundation - float. It looks like the ISO range
// at the moment can be represented by int (it's max - min > 100, etc.).
Q_ASSERT(format);
if (format.maxISO - format.minISO < 1.) {
// ISO is in some strange units?
return false;
}
return qt_check_ISO_conversion(format.minISO)
&& qt_check_ISO_conversion(format.maxISO);
}
bool qt_check_exposure_duration(AVCaptureDevice *captureDevice, CMTime duration)
{
Q_ASSERT(captureDevice);
AVCaptureDeviceFormat *activeFormat = captureDevice.activeFormat;
if (!activeFormat) {
qDebugCamera() << Q_FUNC_INFO << "failed to obtain capture device format";
return false;
}
return CMTimeCompare(duration, activeFormat.minExposureDuration) != -1
&& CMTimeCompare(activeFormat.maxExposureDuration, duration) != -1;
}
bool qt_check_ISO_value(AVCaptureDevice *captureDevice, int newISO)
{
Q_ASSERT(captureDevice);
AVCaptureDeviceFormat *activeFormat = captureDevice.activeFormat;
if (!activeFormat) {
qDebugCamera() << Q_FUNC_INFO << "failed to obtain capture device format";
return false;
}
return !(newISO < activeFormat.minISO || newISO > activeFormat.maxISO);
}
bool qt_exposure_duration_equal(AVCaptureDevice *captureDevice, qreal qDuration)
{
Q_ASSERT(captureDevice);
const CMTime avDuration = CMTimeMakeWithSeconds(qDuration, captureDevice.exposureDuration.timescale);
return !CMTimeCompare(avDuration, captureDevice.exposureDuration);
}
bool qt_iso_equal(AVCaptureDevice *captureDevice, int iso)
{
Q_ASSERT(captureDevice);
return qFuzzyCompare(float(iso), captureDevice.ISO);
}
bool qt_exposure_bias_equal(AVCaptureDevice *captureDevice, qreal bias)
{
Q_ASSERT(captureDevice);
return qFuzzyCompare(bias, qreal(captureDevice.exposureTargetBias));
}
// Converters:
bool qt_convert_exposure_mode(AVCaptureDevice *captureDevice, QCameraExposure::ExposureMode mode,
AVCaptureExposureMode &avMode)
{
// Test if mode supported and convert.
Q_ASSERT(captureDevice);
if (mode == QCameraExposure::ExposureAuto) {
if ([captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) {
avMode = AVCaptureExposureModeContinuousAutoExposure;
return true;
}
}
if (mode == QCameraExposure::ExposureManual) {
if ([captureDevice isExposureModeSupported:AVCaptureExposureModeCustom]) {
avMode = AVCaptureExposureModeCustom;
return true;
}
}
return false;
}
// We set ISO/exposure duration with completion handlers, completion handlers try
// to avoid dangling pointers (thus QPointer for QObjects) and not to create
// a reference loop (in case we have ARC).
void qt_set_exposure_bias(QPointer<AVFCameraService> service, QPointer<AVFCameraExposureControl> control,
AVCaptureDevice *captureDevice, float bias)
{
Q_ASSERT(captureDevice);
__block AVCaptureDevice *device = captureDevice; //For ARC.
void (^completionHandler)(CMTime syncTime) = ^(CMTime) {
// Test that service control is still alive and that
// capture device is our device, if yes - emit actual value changed.
if (service) {
if (control) {
if (service->session() && service->session()->videoCaptureDevice() == device)
Q_EMIT control->actualValueChanged(int(QCameraExposureControl::ExposureCompensation));
}
}
device = nil;
};
[captureDevice setExposureTargetBias:bias completionHandler:completionHandler];
}
void qt_set_duration_iso(QPointer<AVFCameraService> service, QPointer<AVFCameraExposureControl> control,
AVCaptureDevice *captureDevice, CMTime duration, float iso)
{
Q_ASSERT(captureDevice);
__block AVCaptureDevice *device = captureDevice; //For ARC.
const bool setDuration = CMTimeCompare(duration, AVCaptureExposureDurationCurrent);
const bool setISO = !qFuzzyCompare(iso, AVCaptureISOCurrent);
void (^completionHandler)(CMTime syncTime) = ^(CMTime) {
// Test that service control is still alive and that
// capture device is our device, if yes - emit actual value changed.
if (service) {
if (control) {
if (service->session() && service->session()->videoCaptureDevice() == device) {
if (setDuration)
Q_EMIT control->actualValueChanged(int(QCameraExposureControl::ShutterSpeed));
if (setISO)
Q_EMIT control->actualValueChanged(int(QCameraExposureControl::ISO));
}
}
}
device = nil;
};
[captureDevice setExposureModeCustomWithDuration:duration
ISO:iso
completionHandler:completionHandler];
}
#endif // defined(Q_OS_IOS)
} // Unnamed namespace.
AVFCameraExposureControl::AVFCameraExposureControl(AVFCameraService *service)
: m_service(service),
m_session(nullptr)
{
Q_ASSERT(service);
m_session = m_service->session();
Q_ASSERT(m_session);
connect(m_session, SIGNAL(stateChanged(QCamera::State)), SLOT(cameraStateChanged()));
}
bool AVFCameraExposureControl::isParameterSupported(ExposureParameter parameter) const
{
#ifdef Q_OS_IOS
AVCaptureDevice *captureDevice = m_session->videoCaptureDevice();
if (!captureDevice)
return false;
// These are the parameters we have an API to support:
return parameter == QCameraExposureControl::ISO
|| parameter == QCameraExposureControl::ShutterSpeed
|| parameter == QCameraExposureControl::ExposureCompensation
|| parameter == QCameraExposureControl::ExposureMode;
#else
Q_UNUSED(parameter)
return false;
#endif
}
QVariantList AVFCameraExposureControl::supportedParameterRange(ExposureParameter parameter,
bool *continuous) const
{
QVariantList parameterRange;
#ifdef Q_OS_IOS
AVCaptureDevice *captureDevice = m_session->videoCaptureDevice();
if (!captureDevice || !isParameterSupported(parameter)) {
qDebugCamera() << Q_FUNC_INFO << "parameter not supported";
return parameterRange;
}
if (continuous)
*continuous = false;
AVCaptureDeviceFormat *activeFormat = captureDevice.activeFormat;
if (parameter == QCameraExposureControl::ISO) {
if (!activeFormat) {
qDebugCamera() << Q_FUNC_INFO << "failed to obtain capture device format";
return parameterRange;
}
if (!qt_check_ISO_range(activeFormat)) {
qDebugCamera() << Q_FUNC_INFO << "ISO range can not be represented as int";
return parameterRange;
}
parameterRange << QVariant(int(activeFormat.minISO));
parameterRange << QVariant(int(activeFormat.maxISO));
if (continuous)
*continuous = true;
} else if (parameter == QCameraExposureControl::ExposureCompensation) {
parameterRange << captureDevice.minExposureTargetBias;
parameterRange << captureDevice.maxExposureTargetBias;
if (continuous)
*continuous = true;
} else if (parameter == QCameraExposureControl::ShutterSpeed) {
if (!activeFormat) {
qDebugCamera() << Q_FUNC_INFO << "failed to obtain capture device format";
return parameterRange;
}
// CMTimeGetSeconds returns Float64, test the conversion below, if it's valid?
parameterRange << qreal(CMTimeGetSeconds(activeFormat.minExposureDuration));
parameterRange << qreal(CMTimeGetSeconds(activeFormat.maxExposureDuration));
if (continuous)
*continuous = true;
} else if (parameter == QCameraExposureControl::ExposureMode) {
if ([captureDevice isExposureModeSupported:AVCaptureExposureModeCustom])
parameterRange << QVariant::fromValue(QCameraExposure::ExposureManual);
if ([captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure])
parameterRange << QVariant::fromValue(QCameraExposure::ExposureAuto);
}
#else
Q_UNUSED(parameter)
Q_UNUSED(continuous)
#endif
return parameterRange;
}
QVariant AVFCameraExposureControl::requestedValue(ExposureParameter parameter) const
{
if (!isParameterSupported(parameter)) {
qDebugCamera() << Q_FUNC_INFO << "parameter not supported";
return QVariant();
}
if (parameter == QCameraExposureControl::ExposureMode)
return m_requestedMode;
if (parameter == QCameraExposureControl::ExposureCompensation)
return m_requestedCompensation;
if (parameter == QCameraExposureControl::ShutterSpeed)
return m_requestedShutterSpeed;
if (parameter == QCameraExposureControl::ISO)
return m_requestedISO;
return QVariant();
}
QVariant AVFCameraExposureControl::actualValue(ExposureParameter parameter) const
{
#ifdef Q_OS_IOS
AVCaptureDevice *captureDevice = m_session->videoCaptureDevice();
if (!captureDevice || !isParameterSupported(parameter)) {
// Actually, at the moment !captiredevice => !isParameterSupported.
qDebugCamera() << Q_FUNC_INFO << "parameter not supported";
return QVariant();
}
if (parameter == QCameraExposureControl::ExposureMode) {
// This code expects exposureMode to be continuous by default ...
if (captureDevice.exposureMode == AVCaptureExposureModeContinuousAutoExposure)
return QVariant::fromValue(QCameraExposure::ExposureAuto);
return QVariant::fromValue(QCameraExposure::ExposureManual);
}
if (parameter == QCameraExposureControl::ExposureCompensation)
return captureDevice.exposureTargetBias;
if (parameter == QCameraExposureControl::ShutterSpeed)
return qreal(CMTimeGetSeconds(captureDevice.exposureDuration));
if (parameter == QCameraExposureControl::ISO) {
if (captureDevice.activeFormat && qt_check_ISO_range(captureDevice.activeFormat)
&& qt_check_ISO_conversion(captureDevice.ISO)) {
// Can be represented as int ...
return int(captureDevice.ISO);
} else {
qDebugCamera() << Q_FUNC_INFO << "ISO can not be represented as int";
return QVariant();
}
}
#else
Q_UNUSED(parameter)
#endif
return QVariant();
}
bool AVFCameraExposureControl::setValue(ExposureParameter parameter, const QVariant &value)
{
if (parameter == QCameraExposureControl::ExposureMode)
return setExposureMode(value);
else if (parameter == QCameraExposureControl::ExposureCompensation)
return setExposureCompensation(value);
else if (parameter == QCameraExposureControl::ShutterSpeed)
return setShutterSpeed(value);
else if (parameter == QCameraExposureControl::ISO)
return setISO(value);
return false;
}
bool AVFCameraExposureControl::setExposureMode(const QVariant &value)
{
#ifdef Q_OS_IOS
if (!value.canConvert<QCameraExposure::ExposureMode>()) {
qDebugCamera() << Q_FUNC_INFO << "invalid exposure mode value,"
<< "QCameraExposure::ExposureMode expected";
return false;
}
const QCameraExposure::ExposureMode qtMode = value.value<QCameraExposure::ExposureMode>();
if (qtMode != QCameraExposure::ExposureAuto && qtMode != QCameraExposure::ExposureManual) {
qDebugCamera() << Q_FUNC_INFO << "exposure mode not supported";
return false;
}
AVCaptureDevice *captureDevice = m_session->videoCaptureDevice();
if (!captureDevice) {
m_requestedMode = value;
Q_EMIT requestedValueChanged(int(QCameraExposureControl::ExposureMode));
return true;
}
AVCaptureExposureMode avMode = AVCaptureExposureModeAutoExpose;
if (!qt_convert_exposure_mode(captureDevice, qtMode, avMode)) {
qDebugCamera() << Q_FUNC_INFO << "exposure mode not supported";
return false;
}
const AVFConfigurationLock lock(captureDevice);
if (!lock) {
qDebugCamera() << Q_FUNC_INFO << "failed to lock a capture device"
<< "for configuration";
return false;
}
m_requestedMode = value;
[captureDevice setExposureMode:avMode];
Q_EMIT requestedValueChanged(int(QCameraExposureControl::ExposureMode));
Q_EMIT actualValueChanged(int(QCameraExposureControl::ExposureMode));
return true;
#else
Q_UNUSED(value)
return false;
#endif
}
bool AVFCameraExposureControl::setExposureCompensation(const QVariant &value)
{
#ifdef Q_OS_IOS
if (!value.canConvert<qreal>()) {
qDebugCamera() << Q_FUNC_INFO << "invalid exposure compensation"
<<"value, floating point number expected";
return false;
}
const qreal bias = value.toReal();
AVCaptureDevice *captureDevice = m_session->videoCaptureDevice();
if (!captureDevice) {
m_requestedCompensation = value;
Q_EMIT requestedValueChanged(int(QCameraExposureControl::ExposureCompensation));
return true;
}
if (bias < captureDevice.minExposureTargetBias || bias > captureDevice.maxExposureTargetBias) {
// TODO: mixed fp types!
qDebugCamera() << Q_FUNC_INFO << "exposure compenstation value is"
<< "out of range";
return false;
}
const AVFConfigurationLock lock(captureDevice);
if (!lock) {
qDebugCamera() << Q_FUNC_INFO << "failed to lock for configuration";
return false;
}
qt_set_exposure_bias(m_service, this, captureDevice, bias);
m_requestedCompensation = value;
Q_EMIT requestedValueChanged(int(QCameraExposureControl::ExposureCompensation));
return true;
#else
Q_UNUSED(value)
return false;
#endif
}
bool AVFCameraExposureControl::setShutterSpeed(const QVariant &value)
{
#ifdef Q_OS_IOS
if (value.isNull())
return setExposureMode(QVariant::fromValue(QCameraExposure::ExposureAuto));
if (!value.canConvert<qreal>()) {
qDebugCamera() << Q_FUNC_INFO << "invalid shutter speed"
<< "value, floating point number expected";
return false;
}
AVCaptureDevice *captureDevice = m_session->videoCaptureDevice();
if (!captureDevice) {
m_requestedShutterSpeed = value;
Q_EMIT requestedValueChanged(int(QCameraExposureControl::ShutterSpeed));
return true;
}
const CMTime newDuration = CMTimeMakeWithSeconds(value.toReal(),
captureDevice.exposureDuration.timescale);
if (!qt_check_exposure_duration(captureDevice, newDuration)) {
qDebugCamera() << Q_FUNC_INFO << "shutter speed value is out of range";
return false;
}
const AVFConfigurationLock lock(captureDevice);
if (!lock) {
qDebugCamera() << Q_FUNC_INFO << "failed to lock for configuration";
return false;
}
// Setting the shutter speed (exposure duration in Apple's terms,
// since there is no shutter actually) will also reset
// exposure mode into custom mode.
qt_set_duration_iso(m_service, this, captureDevice, newDuration, AVCaptureISOCurrent);
m_requestedShutterSpeed = value;
Q_EMIT requestedValueChanged(int(QCameraExposureControl::ShutterSpeed));
return true;
#else
Q_UNUSED(value)
return false;
#endif
}
bool AVFCameraExposureControl::setISO(const QVariant &value)
{
#ifdef Q_OS_IOS
if (value.isNull())
return setExposureMode(QVariant::fromValue(QCameraExposure::ExposureAuto));
if (!value.canConvert<int>()) {
qDebugCamera() << Q_FUNC_INFO << "invalid ISO value, int expected";
return false;
}
AVCaptureDevice *captureDevice = m_session->videoCaptureDevice();
if (!captureDevice) {
m_requestedISO = value;
Q_EMIT requestedValueChanged(int(QCameraExposureControl::ISO));
return true;
}
if (!qt_check_ISO_value(captureDevice, value.toInt())) {
qDebugCamera() << Q_FUNC_INFO << "ISO value is out of range";
return false;
}
const AVFConfigurationLock lock(captureDevice);
if (!lock) {
qDebugCamera() << Q_FUNC_INFO << "failed to lock a capture device"
<< "for configuration";
return false;
}
// Setting the ISO will also reset
// exposure mode to the custom mode.
qt_set_duration_iso(m_service, this, captureDevice, AVCaptureExposureDurationCurrent, value.toInt());
m_requestedISO = value;
Q_EMIT requestedValueChanged(int(QCameraExposureControl::ISO));
return true;
#else
Q_UNUSED(value)
return false;
#endif
}
void AVFCameraExposureControl::cameraStateChanged()
{
#ifdef Q_OS_IOS
if (m_session->state() != QCamera::ActiveState)
return;
AVCaptureDevice *captureDevice = m_session->videoCaptureDevice();
if (!captureDevice) {
qDebugCamera() << Q_FUNC_INFO << "capture device is nil, but the session"
<< "state is 'active'";
return;
}
Q_EMIT parameterRangeChanged(int(QCameraExposureControl::ExposureCompensation));
Q_EMIT parameterRangeChanged(int(QCameraExposureControl::ExposureMode));
Q_EMIT parameterRangeChanged(int(QCameraExposureControl::ShutterSpeed));
Q_EMIT parameterRangeChanged(int(QCameraExposureControl::ISO));
const AVFConfigurationLock lock(captureDevice);
CMTime newDuration = AVCaptureExposureDurationCurrent;
bool setCustomMode = false;
if (!m_requestedShutterSpeed.isNull()
&& !qt_exposure_duration_equal(captureDevice, m_requestedShutterSpeed.toReal())) {
newDuration = CMTimeMakeWithSeconds(m_requestedShutterSpeed.toReal(),
captureDevice.exposureDuration.timescale);
if (!qt_check_exposure_duration(captureDevice, newDuration)) {
qDebugCamera() << Q_FUNC_INFO << "requested exposure duration is out of range";
return;
}
setCustomMode = true;
}
float newISO = AVCaptureISOCurrent;
if (!m_requestedISO.isNull() && !qt_iso_equal(captureDevice, m_requestedISO.toInt())) {
newISO = m_requestedISO.toInt();
if (!qt_check_ISO_value(captureDevice, newISO)) {
qDebugCamera() << Q_FUNC_INFO << "requested ISO value is out of range";
return;
}
setCustomMode = true;
}
if (!m_requestedCompensation.isNull()
&& !qt_exposure_bias_equal(captureDevice, m_requestedCompensation.toReal())) {
// TODO: mixed fpns.
const qreal bias = m_requestedCompensation.toReal();
if (bias < captureDevice.minExposureTargetBias || bias > captureDevice.maxExposureTargetBias) {
qDebugCamera() << Q_FUNC_INFO << "exposure compenstation value is"
<< "out of range";
return;
}
if (!lock) {
qDebugCamera() << Q_FUNC_INFO << "failed to lock for configuration";
return;
}
qt_set_exposure_bias(m_service, this, captureDevice, bias);
}
// Setting shutter speed (exposure duration) or ISO values
// also reset exposure mode into Custom. With this settings
// we ignore any attempts to set exposure mode.
if (setCustomMode) {
if (!lock)
qDebugCamera() << Q_FUNC_INFO << "failed to lock for configuration";
else
qt_set_duration_iso(m_service, this, captureDevice, newDuration, newISO);
return;
}
if (!m_requestedMode.isNull()) {
QCameraExposure::ExposureMode qtMode = m_requestedMode.value<QCameraExposure::ExposureMode>();
AVCaptureExposureMode avMode = AVCaptureExposureModeContinuousAutoExposure;
if (!qt_convert_exposure_mode(captureDevice, qtMode, avMode)) {
qDebugCamera() << Q_FUNC_INFO << "requested exposure mode is not supported";
return;
}
if (avMode == captureDevice.exposureMode)
return;
if (!lock) {
qDebugCamera() << Q_FUNC_INFO << "failed to lock for configuration";
return;
}
[captureDevice setExposureMode:avMode];
Q_EMIT actualValueChanged(int(QCameraExposureControl::ExposureMode));
}
#endif
}
QT_END_NAMESPACE
#include "moc_avfcameraexposurecontrol.cpp"