/**************************************************************************** ** ** 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 #include #include #include #include #include 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::max()) return false; if (isoValue <= std::numeric_limits::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 service, QPointer 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 service, QPointer 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()) { qDebugCamera() << Q_FUNC_INFO << "invalid exposure mode value," << "QCameraExposure::ExposureMode expected"; return false; } const QCameraExposure::ExposureMode qtMode = value.value(); 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()) { 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()) { 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()) { 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(); 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"