Revision control

Copy as Markdown

// Copyright (C) 2021 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 <QtFFmpegMediaPluginImpl/private/qandroidcamera_p.h>
#include <jni.h>
#include <memory>
#include <optional>
#include <utility>
#include <QtCore/qdebug.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qmetaobject.h>
#include <QtCore/qpermissions.h>
#include <QtCore/qreadwritelock.h>
#include <QtCore/private/qandroidextras_p.h>
#include <QtGui/qguiapplication.h>
#include <QtGui/qscreen.h>
#include <QtFFmpegMediaPluginImpl/private/qandroidvideoframefactory_p.h>
#include <QtFFmpegMediaPluginImpl/private/qffmpegvideobuffer_p.h>
#include <QtMultimedia/qmediaformat.h>
#include <QtMultimedia/qmediadevices.h>
#include <QtMultimedia/private/qcameradevice_p.h>
#include <QtMultimedia/private/qmultimediautils_p.h>
#include <QtMultimedia/private/qvideoframe_p.h>
#include <QtMultimedia/private/qvideoframeconverter_p.h>
#include <QtMultimedia/private/qvideotexturehelper_p.h>
extern "C" {
#include "libavutil/hwcontext.h"
}
QT_BEGIN_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(qLCAndroidCamera, "qt.multimedia.ffmpeg.androidCamera");
// TODO: Should be reworked to just pass a pointer of the QAndroidCamera object into the QtCamera2
// Java instance.
typedef QMap<QString, QFFmpeg::QAndroidCamera *> QAndroidCameraMap;
Q_GLOBAL_STATIC(QAndroidCameraMap, g_qcameras)
Q_GLOBAL_STATIC(QReadWriteLock, rwLock)
namespace {
// TODO: Unify format selection with cross-platform layer
QCameraFormat getDefaultCameraFormat(const QCameraDevice & cameraDevice)
{
// default settings
const auto defaultFrameFormat = QVideoFrameFormat::Format_YUV420P;
const auto defaultResolution = QSize(1920, 1080);
QCameraFormatPrivate *defaultFormat = new QCameraFormatPrivate{
.pixelFormat = defaultFrameFormat,
.resolution = defaultResolution,
.minFrameRate = 12,
.maxFrameRate = 30,
};
QCameraFormat resultFormat = defaultFormat->create();
const auto &supportedFormats = cameraDevice.videoFormats();
if (supportedFormats.empty() || supportedFormats.contains(resultFormat))
return resultFormat;
auto pixelCount = [](const QSize& resolution) {
Q_ASSERT(resolution.isValid());
return resolution.width() * resolution.height();
};
const int defaultPixelCount = pixelCount(defaultResolution);
// The lower the score, the better the format suits
int differenceScore = std::numeric_limits<int>::max();
auto calcDifferenceScore = [defaultPixelCount, pixelCount](const QCameraFormat& format) {
const int pixelDifference = pixelCount(format.resolution()) - defaultPixelCount;
// prefer:
// 1. 'pixels count >= default' over 'pixels count < default'
// 2. lower abs(pixelDifference)
return pixelDifference < 0
? -pixelDifference
: std::numeric_limits<int>::min() + pixelDifference;
};
for (const auto &supportedFormat : supportedFormats) {
if (supportedFormat.pixelFormat() == defaultFrameFormat) {
if (supportedFormat.resolution() == defaultResolution)
return supportedFormat;
const int currentDifferenceScore = calcDifferenceScore(supportedFormat);
if (currentDifferenceScore < differenceScore) {
differenceScore = currentDifferenceScore;
resultFormat = supportedFormat;
}
}
}
return resultFormat;
}
bool checkCameraPermission()
{
QCameraPermission permission;
const bool granted = qApp->checkPermission(permission) == Qt::PermissionStatus::Granted;
if (!granted)
qCWarning(qLCAndroidCamera) << "Access to camera not granted!";
return granted;
}
int sensorOrientation(QString cameraId)
{
QJniObject deviceManager(QtJniTypes::Traits<QtJniTypes::QtVideoDeviceManager>::className(),
QNativeInterface::QAndroidApplication::context());
if (!deviceManager.isValid()) {
qCWarning(qLCAndroidCamera) << "Failed to connect to Qt Video Device Manager.";
return 0;
}
return deviceManager.callMethod<jint>("getSensorOrientation",
QJniObject::fromString(cameraId).object<jstring>());
}
// Returns the FocusModes that are available on the physical device, for which we also have
// an implementation.
[[nodiscard]] static QList<QCamera::FocusMode> qGetSupportedFocusModesFromAndroidCamera(
const QJniObject &deviceManager,
const QCameraDevice &cameraDevice)
{
QList<QCamera::FocusMode> returnValue;
const QStringList focusModeStrings = deviceManager.callMethod<QStringList>(
"getSupportedQCameraFocusModesAsStrings",
QJniObject::fromString(QString::fromUtf8(cameraDevice.id())).object<jstring>());
// Translate the strings into enums if possible.
for (const QString &focusModeString : focusModeStrings) {
bool ok = false;
const auto focusMode = static_cast<QCamera::FocusMode>(
QMetaEnum::fromType<QCamera::FocusMode>()
.keyToValue(
focusModeString.toLatin1().data(),
&ok));
if (ok)
returnValue.push_back(focusMode);
else
qCDebug(qLCAndroidCamera) <<
"received a QCamera::FocusMode string from Android "
"QtVideoDeviceManager.java that was not recognized.";
}
return returnValue;
}
} // namespace
namespace QFFmpeg {
// QAndroidCamera
QAndroidCamera::QAndroidCamera(QCamera *camera)
: QPlatformCamera(camera)
, m_jniCamera(QNativeInterface::QAndroidApplication::context())
{
m_hwAccel = QFFmpeg::HWAccel::create(AVHWDeviceType::AV_HWDEVICE_TYPE_MEDIACODEC);
if (camera) {
m_cameraDevice = camera->cameraDevice();
m_cameraFormat = !camera->cameraFormat().isNull() ? camera->cameraFormat()
: getDefaultCameraFormat(m_cameraDevice);
updateCameraCharacteristics();
}
if (qApp) {
connect(qApp, &QGuiApplication::applicationStateChanged,
this, &QAndroidCamera::onApplicationStateChanged);
}
};
QAndroidCamera::~QAndroidCamera()
{
// stopAndClose() waits for onClosed(), possibly queued behind other calls
// which need rwLock
m_jniCamera.callMethod<void>("stopAndClose");
setState(State::Closed);
{
QWriteLocker locker(rwLock);
g_qcameras->remove(QString::fromUtf8(m_cameraDevice.id()));
}
m_jniCamera.callMethod<void>("stopBackgroundThread");
}
void QAndroidCamera::setCamera(const QCameraDevice &camera)
{
const bool oldActive = isActive();
if (oldActive)
setActive(false);
// Reset all our control-members on the Java-side to default
// values. Then populate them again during updateCameraCharacteristics()
m_jniCamera.callMethod<void>("resetControlProperties");
m_cameraDevice = camera;
updateCameraCharacteristics();
m_cameraFormat = getDefaultCameraFormat(camera);
if (oldActive)
setActive(true);
}
std::optional<int> QAndroidCamera::ffmpegHWPixelFormat() const
{
// TODO: m_androidFramePixelFormat is continuously being written to by
// the Java-side capture-processing background thread when receiving frame, while this
// function is commonly called by the media recording engine on other threads.
// A potential solution might include a mutex-lock and/or determining
// the pixelFormat ahead of time by checking what format we request
// when starting the Android camera capture session.
return QFFmpegVideoBuffer::toAVPixelFormat(m_androidFramePixelFormat);
}
QVideoFrameFormat QAndroidCamera::frameFormat() const
{
QVideoFrameFormat result = QPlatformCamera::frameFormat();
// Apply rotation for surface only
result.setRotation(rotation());
return result;
}
// Called by the Java-side processing background thread.
void QAndroidCamera::frameAvailable(QJniObject image, bool takePhoto)
{
if ((!(m_state == State::WaitingStart || m_state == State::Started) && !m_waitingForFirstFrame)
|| m_frameFactory == nullptr) {
qCWarning(qLCAndroidCamera) << "Received frame when not active... ignoring";
qCWarning(qLCAndroidCamera) << "state:" << m_state;
image.callMethod<void>("close");
return;
}
QVideoFrame videoFrame = m_frameFactory->createVideoFrame(image, rotation());
if (!videoFrame.isValid())
return;
// TODO: m_androidFramePixelFormat is written by the Java-side processing
// background thread, but read by the QCamera thread during QAndroid::ffmpegHWPixelFormat().
// This causes a race condition (not severe). We should eventually implement some
// synchronization strategy.
m_androidFramePixelFormat = videoFrame.pixelFormat(); // NOTE: Should this be updated on stills?
if (m_waitingForFirstFrame) {
m_waitingForFirstFrame = false;
setState(State::Started);
// Apply a still resolution changed during startup
QMetaObject::invokeMethod(this, &QAndroidCamera::applyStillResolution,
Qt::QueuedConnection);
}
videoFrame.setMirrored(m_cameraDevice.position() == QCameraDevice::Position::FrontFace);
if (!takePhoto)
Q_EMIT newVideoFrame(videoFrame);
else
Q_EMIT onStillPhotoCaptured(videoFrame);
}
QtVideo::Rotation QAndroidCamera::rotation() const
{
// If the camera is not attached to the main display, we don't want to apply rotation
// based on primary screen. We assume we are an external camera.
const QCameraDevice::Position cameraPosition = m_cameraDevice.position();
const bool isExternalCamera = cameraPosition == QCameraDevice::Position::UnspecifiedPosition;
if (isExternalCamera)
return QtVideo::Rotation::None;
auto screen = QGuiApplication::primaryScreen();
auto screenOrientation = screen->orientation();
if (screenOrientation == Qt::PrimaryOrientation)
screenOrientation = screen->primaryOrientation();
// Display rotation is the opposite direction of the physical device rotation. We need the
// device rotation, that's why Landscape is 270 and InvertedLandscape is 90
int deviceOrientation = 0;
switch (screenOrientation) {
case Qt::PrimaryOrientation:
case Qt::PortraitOrientation:
break;
case Qt::LandscapeOrientation:
deviceOrientation = 270;
break;
case Qt::InvertedPortraitOrientation:
deviceOrientation = 180;
break;
case Qt::InvertedLandscapeOrientation:
deviceOrientation = 90;
break;
}
const int sign = (cameraPosition == QCameraDevice::Position::FrontFace) ? 1 : -1;
int rotation = (sensorOrientation(QString::fromUtf8(m_cameraDevice.id()))
- deviceOrientation * sign + 360) % 360;
return QtVideo::Rotation(rotation);
}
void QAndroidCamera::setActive(bool active)
{
Q_ASSERT(thread()->isCurrentThread());
if (isActive() == active)
return;
if (!m_jniCamera.isValid()) {
updateError(QCamera::CameraError, QStringLiteral("No connection to Android Camera2 API"));
return;
}
if (!active || !checkCameraPermission()) {
m_jniCamera.callMethod<void>("stopAndClose");
m_jniCamera.callMethod<void>("clearSurfaces");
setState(State::Closed);
return;
}
QWriteLocker locker(rwLock);
QString cameraId = QString::fromUtf8(m_cameraDevice.id());
int width = m_cameraFormat.resolution().width();
int height = m_cameraFormat.resolution().height();
if (width < 0 || height < 0) {
m_cameraFormat = getDefaultCameraFormat(m_cameraDevice);
width = m_cameraFormat.resolution().width();
height = m_cameraFormat.resolution().height();
}
m_videoResolution = { FFALIGN(width, 16), FFALIGN(height, 16) };
m_stillReaderSize = stillResolution();
setState(State::WaitingOpen);
g_qcameras->insert(cameraId, this);
// Create frameFactory when ImageReader is created;
m_frameFactory = QAndroidVideoFrameFactory::create();
// Most supported format combination for ImageReaders in a session is YUV_420_888 for
// preview/video, and JPEG for stills
// TODO: Set session combinations based on device capabilities:
m_jniCamera.callMethod<void>("prepareCamera", static_cast<jint>(m_videoResolution.width()),
static_cast<jint>(m_videoResolution.height()),
static_cast<jint>(m_cameraFormat.minFrameRate()),
static_cast<jint>(m_cameraFormat.maxFrameRate()),
static_cast<jint>(m_stillReaderSize.width()),
static_cast<jint>(m_stillReaderSize.height()));
bool canOpen = m_jniCamera.callMethod<jboolean>("open", cameraId);
if (!canOpen) {
g_qcameras->remove(cameraId);
setState(State::Closed);
updateError(QCamera::CameraError,
QString(u"Failed to start camera: ").append(m_cameraDevice.description()));
}
}
// TODO: setState is currently being used by the C++ thread owning
// the QCamera object, and the Java-side capture-processing background thread.
// This can lead to race conditions and the m_state ending up in inconsistent states.
// We should have a synchronization strategy in the future.
void QAndroidCamera::setState(QAndroidCamera::State newState)
{
if (newState == m_state)
return;
bool wasActive = isActive();
if (newState == State::Started)
m_state = State::Started;
if (m_state == State::Started && newState == State::Closed)
m_state = State::Closed;
if ((m_state == State::WaitingOpen || m_state == State::WaitingStart)
&& newState == State::Closed) {
m_state = State::Closed;
updateError(QCamera::CameraError,
QString(u"Failed to start Camera %1").arg(m_cameraDevice.description()));
}
if (m_state == State::Closed && newState == State::WaitingOpen)
m_state = State::WaitingOpen;
if (m_state == State::WaitingOpen && newState == State::WaitingStart)
m_state = State::WaitingStart;
if (wasActive != isActive())
Q_EMIT activeChanged(isActive());
}
bool QAndroidCamera::setCameraFormat(const QCameraFormat &format)
{
const auto chosenFormat = format.isNull() ? getDefaultCameraFormat(m_cameraDevice) : format;
if (chosenFormat == m_cameraFormat)
return true;
if (!m_cameraDevice.videoFormats().contains(chosenFormat))
return false;
m_cameraFormat = chosenFormat;
restartCamera();
return true;
}
void QAndroidCamera::updateCameraCharacteristics()
{
if (m_cameraDevice.id().isEmpty()) {
cleanCameraCharacteristics();
return;
}
QJniObject deviceManager(QtJniTypes::Traits<QtJniTypes::QtVideoDeviceManager>::className(),
QNativeInterface::QAndroidApplication::context());
if (!deviceManager.isValid()) {
qCWarning(qLCAndroidCamera) << "Failed to connect to Qt Video Device Manager.";
cleanCameraCharacteristics();
return;
}
// Gather capabilities.
QCamera::Features newSupportedFeatures = {};
float newMaxZoom = 1.f;
float newMinZoom = 1.f;
const auto newZoomRange = deviceManager.callMethod<jfloat[]>(
"getZoomRange",
QJniObject::fromString(QString::fromUtf8(m_cameraDevice.id())).object<jstring>());
if (newZoomRange.isValid() && newZoomRange.size() == 2) {
newMinZoom = newZoomRange[0];
newMaxZoom = newZoomRange[1];
} else {
qCDebug(qLCAndroidCamera) <<
"received invalid float array when querying zoomRange from Android Camera2. "
"Likely Qt developer bug";
}
m_supportedFlashModes.clear();
m_supportedFlashModes.append(QCamera::FlashOff);
const QStringList flashModes = deviceManager.callMethod<QStringList>(
"getSupportedFlashModes",
QJniObject::fromString(QString::fromUtf8(m_cameraDevice.id())).object<jstring>());
for (const auto &flashMode : flashModes) {
if (flashMode == QLatin1String("auto"))
m_supportedFlashModes.append(QCamera::FlashAuto);
else if (flashMode == QLatin1String("on"))
m_supportedFlashModes.append(QCamera::FlashOn);
}
m_TorchModeSupported = deviceManager.callMethod<jboolean>(
"isTorchModeSupported",
QJniObject::fromString(QString::fromUtf8(m_cameraDevice.id())).object<jstring>());
m_supportedFocusModes = qGetSupportedFocusModesFromAndroidCamera(
deviceManager,
m_cameraDevice);
if (m_supportedFocusModes.contains(QCamera::FocusMode::FocusModeManual))
newSupportedFeatures |= QCamera::Feature::FocusDistance;
// Signal capability changes
minimumZoomFactorChanged(newMinZoom);
maximumZoomFactorChanged(newMaxZoom);
supportedFeaturesChanged(newSupportedFeatures);
// Apply properties
if (minZoomFactor() < maxZoomFactor()) {
// New device supports zooming. Clamp it and apply it to new camera device.
const float newZoomFactor = qBound(minZoomFactor(), zoomFactor(), maxZoomFactor());
zoomTo(newZoomFactor, -1);
}
if (isFlashModeSupported(flashMode()))
setFlashMode(flashMode());
if (isTorchModeSupported(torchMode()))
setTorchMode(torchMode());
if (isFocusModeSupported(focusMode()))
setFocusMode(focusMode());
// Reset properties if needed.
if (minZoomFactor() >= maxZoomFactor())
zoomFactorChanged(defaultZoomFactor());
if (!isFlashModeSupported(flashMode()))
flashModeChanged(defaultFlashMode());
if (!isTorchModeSupported(torchMode()))
torchModeChanged(defaultTorchMode());
if (!isFocusModeSupported(focusMode()))
focusModeChanged(defaultFocusMode());
}
// Should only be called when the camera device is set to null.
void QAndroidCamera::cleanCameraCharacteristics()
{
maximumZoomFactorChanged(1.0);
if (zoomFactor() != 1.0) {
zoomTo(1.0, -1.0);
}
if (torchMode() != QCamera::TorchOff) {
setTorchMode(QCamera::TorchOff);
}
m_TorchModeSupported = false;
if (flashMode() != QCamera::FlashOff) {
setFlashMode(QCamera::FlashOff);
}
m_supportedFlashModes.clear();
m_supportedFlashModes.append(QCamera::FlashOff);
// Reset focus mode.
if (focusMode() != QCamera::FocusModeAuto)
setFocusMode(QCamera::FocusModeAuto);
m_supportedFocusModes.clear();
supportedFeaturesChanged({});
}
QSize QAndroidCamera::stillResolution() const
{
if (m_requestedStillResolution.isEmpty())
return m_videoResolution; // Not set, will use video resolution
const QList<QSize> photoResolutions = m_cameraDevice.photoResolutions();
// Use as-is with empty list, might fail
if (photoResolutions.isEmpty() || photoResolutions.contains(m_requestedStillResolution))
return m_requestedStillResolution;
return qClosestSupportedResolution(m_requestedStillResolution, photoResolutions);
}
// Restart camera now, must be called from owning thread
void QAndroidCamera::restartCamera()
{
if (!isActive())
return;
setActive(false);
setActive(true);
}
// Queue a camera restart, can be called from a background Java thread
void QAndroidCamera::queueCameraRestart()
{
if (m_restartPending.exchange(true))
return;
// NOTE: Queued on event loop to avoid re-entering onVideoSourceChanged
QMetaObject::invokeMethod(
this,
[this]() {
m_restartPending = false;
restartCamera(); // No-op if inactive
},
Qt::QueuedConnection);
}
void QAndroidCamera::setFocusDistance(float distance)
{
if (QtPrivate::fuzzyCompare(focusDistance(), distance))
return;
if (!(supportedFeatures() & QCamera::Feature::FocusDistance)) {
qCWarning(qLCAndroidCamera) <<
Q_FUNC_INFO <<
"attmpted to set focus-distance on camera without support for FocusDistance feature";
return;
}
if (distance < 0 || distance > 1) {
qCWarning(qLCAndroidCamera) <<
Q_FUNC_INFO <<
"attempted to set camera focus-distance with out-of-bounds value";
return;
}
// focusDistance should only be applied if the focusMode is currently set to Manual.
// The Java function will handle this behavior. Either way, the value should be accepted so
// we can apply it if FocusModeManual is activated
m_jniCamera.callMethod<void>("setFocusDistance", distance);
focusDistanceChanged(distance);
}
void QAndroidCamera::setFocusMode(QCamera::FocusMode mode)
{
if (focusMode() == mode)
return;
if (!isFocusModeSupported(mode)) {
qCWarning(qLCAndroidCamera) <<
Q_FUNC_INFO <<
QLatin1String("attempted to set focus-mode '%1' on camera where it is unsupported.")
.arg(QMetaEnum::fromType<QCamera::FocusMode>().valueToKey(mode));
return;
}
// If the new focus-mode is Manual, then the focusDistance
// needs to be applied as well. The Java function handles this.
m_jniCamera.callMethod<void>(
"setFocusMode",
static_cast<jint>(mode));
focusModeChanged(mode);
}
void QAndroidCamera::setFlashMode(QCamera::FlashMode mode)
{
if (!isFlashModeSupported(mode))
return;
QString flashMode;
switch (mode) {
case QCamera::FlashAuto:
flashMode = QLatin1String("auto");
break;
case QCamera::FlashOn:
flashMode = QLatin1String("on");
break;
case QCamera::FlashOff:
default:
flashMode = QLatin1String("off");
break;
}
m_jniCamera.callMethod<void>("setFlashMode", QJniObject::fromString(flashMode).object<jstring>());
flashModeChanged(mode);
}
bool QAndroidCamera::isFlashModeSupported(QCamera::FlashMode mode) const
{
return m_supportedFlashModes.contains(mode);
}
bool QAndroidCamera::isFlashReady() const
{
// Android doesn't have an API for that.
// Only check if device supports more flash modes than just FlashOff.
return m_supportedFlashModes.size() > 1;
}
bool QAndroidCamera::isFocusModeSupported(QCamera::FocusMode mode) const
{
return QPlatformCamera::isFocusModeSupported(mode) || m_supportedFocusModes.contains(mode);
}
bool QAndroidCamera::isTorchModeSupported(QCamera::TorchMode mode) const
{
if (mode == QCamera::TorchOff)
return true;
else if (mode == QCamera::TorchOn)
return m_TorchModeSupported;
return false;
}
void QAndroidCamera::setTorchMode(QCamera::TorchMode mode)
{
bool torchMode;
if (mode == QCamera::TorchOff) {
torchMode = false;
} else if (mode == QCamera::TorchOn) {
torchMode = true;
} else {
qWarning() << "Unknown Torch mode";
return;
}
m_jniCamera.callMethod<void>("setTorchMode", jboolean(torchMode));
torchModeChanged(mode);
}
void QAndroidCamera::zoomTo(float factor, float rate)
{
Q_UNUSED(rate);
if (!m_cameraDevice.id().isEmpty()) {
m_jniCamera.callMethod<void>("zoomTo", factor);
}
zoomFactorChanged(factor);
}
void QAndroidCamera::onApplicationStateChanged()
{
switch (QGuiApplication::applicationState()) {
case Qt::ApplicationInactive:
if (isActive()) {
setActive(false);
m_wasActive = true;
}
break;
case Qt::ApplicationActive:
if (m_wasActive) {
setActive(true);
m_wasActive = false;
}
break;
default:
break;
}
}
// Called by Java-side processing background thread.
void QAndroidCamera::onCaptureSessionConfigured()
{
bool canStart = m_jniCamera.callMethod<jboolean>("start");
setState(canStart ? State::WaitingStart : State::Closed);
}
// Called by Java-side processing background thread.
void QAndroidCamera::onCaptureSessionConfigureFailed()
{
setState(State::Closed);
}
// Called by Java-side processing background thread.
// The session rebuilt for a new still reader size failed. Fall back to a full restart.
void QAndroidCamera::onStillReaderSwapFailed()
{
queueCameraRestart();
}
// Called by Java-side processing background thread.
void QAndroidCamera::onCameraOpened()
{
bool canStart = m_jniCamera.callMethod<jboolean>("createSession");
setState(canStart ? State::WaitingStart : State::Closed);
}
// Called by Java-side processing background thread.
void QAndroidCamera::onCameraDisconnect()
{
setState(State::Closed);
}
// Called by Java-side processing background thread.
void QAndroidCamera::onCameraError(int reason)
{
updateError(QCamera::CameraError,
QString(u"Capture error with Camera %1. Camera2 Api error code: %2")
.arg(m_cameraDevice.description())
.arg(reason));
}
// Called by Java-side processing background thread.
void QAndroidCamera::onSessionActive()
{
m_waitingForFirstFrame = true;
}
// Called by Java-side processing background thread.
void QAndroidCamera::onSessionClosed()
{
m_waitingForFirstFrame = false;
setState(State::Closed);
}
void QAndroidCamera::capture()
{
m_jniCamera.callMethod<void>("beginStillPhotoCapture");
}
void QAndroidCamera::updateExif(const QString &filename)
{
m_jniCamera.callMethod<void>("saveExifToFile", QJniObject::fromString(filename).object<jstring>());
}
void QAndroidCamera::setStillCaptureResolution(QSize resolution)
{
Q_ASSERT(thread()->isCurrentThread());
if (resolution != m_requestedStillResolution && !resolution.isEmpty()
&& m_cameraDevice.photoResolutions().isEmpty()) {
qCWarning(qLCAndroidCamera) << "Image capture resolution set to" << resolution
<< "while list of valid photo resolutions is empty. Image "
"capture can fail if resolution is not supported by device.";
}
m_requestedStillResolution = resolution;
applyStillResolution();
}
void QAndroidCamera::applyStillResolution()
{
Q_ASSERT(thread()->isCurrentThread());
const QSize newSize = stillResolution();
// Not started: the next start uses the new size or re-runs this, see frameAvailable()
if (!isActive() || m_stillReaderSize == newSize)
return;
if (m_jniCamera.callMethod<jboolean>("updateStillImageReader", newSize.width(),
newSize.height())) {
m_stillReaderSize = newSize;
return;
}
queueCameraRestart();
}
void QAndroidCamera::onCaptureSessionFailed(int reason, long frameNumber)
{
Q_UNUSED(frameNumber);
updateError(QCamera::CameraError,
QStringLiteral("Capture session failure with Camera %1. Camera2 Api error code: %2")
.arg(m_cameraDevice.description())
.arg(reason));
}
// Called by Java background thread if the on-going still-photo capture fails.
void QAndroidCamera::onStillPhotoCaptureFailed()
{
// TODO: Emit a more descriptive error signal. At the time of writing, there is no
// suitable QImageCapture::Error enumerator for this scenario.
Q_EMIT onImageCaptureFailed(
QImageCapture::Error::ResourceError,
QStringLiteral("Unknown error"));
}
} // namespace QFFmpeg
// JNI logic
// The following static functions can only be called by the Java-side processing background
// thread.
#define GET_CAMERA(cameraId) \
QString key = QJniObject(cameraId).toString(); \
QReadLocker locker(rwLock); \
if (!g_qcameras->contains(key)) { \
qCWarning(qLCAndroidCamera) << "Calling back a QtCamera2 after being destroyed."; \
return; \
} \
QFFmpeg::QAndroidCamera *camera = g_qcameras->find(key).value();
static void onPreviewFrameAvailable(JNIEnv *env, jobject obj, jstring cameraId,
QtJniTypes::Image image)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->frameAvailable(QJniObject(image));
}
Q_DECLARE_JNI_NATIVE_METHOD(onPreviewFrameAvailable)
static void onStillPhotoAvailable(JNIEnv *env, jobject obj, jstring cameraId,
QtJniTypes::Image image)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->frameAvailable(QJniObject(image), true);
}
Q_DECLARE_JNI_NATIVE_METHOD(onStillPhotoAvailable)
static void onCameraOpened(JNIEnv *env, jobject obj, jstring cameraId)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->onCameraOpened();
}
Q_DECLARE_JNI_NATIVE_METHOD(onCameraOpened)
static void onCameraDisconnect(JNIEnv *env, jobject obj, jstring cameraId)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->onCameraDisconnect();
}
Q_DECLARE_JNI_NATIVE_METHOD(onCameraDisconnect)
static void onCameraError(JNIEnv *env, jobject obj, jstring cameraId, jint error)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->onCameraError(error);
}
Q_DECLARE_JNI_NATIVE_METHOD(onCameraError)
static void onCaptureSessionConfigured(JNIEnv *env, jobject obj, jstring cameraId)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->onCaptureSessionConfigured();
}
Q_DECLARE_JNI_NATIVE_METHOD(onCaptureSessionConfigured)
static void onCaptureSessionConfigureFailed(JNIEnv *env, jobject obj, jstring cameraId)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->onCaptureSessionConfigureFailed();
}
Q_DECLARE_JNI_NATIVE_METHOD(onCaptureSessionConfigureFailed)
static void onStillReaderSwapFailed(JNIEnv *env, jobject obj, jstring cameraId)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->onStillReaderSwapFailed();
}
Q_DECLARE_JNI_NATIVE_METHOD(onStillReaderSwapFailed)
static void onSessionActive(JNIEnv *env, jobject obj, jstring cameraId)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->onSessionActive();
}
Q_DECLARE_JNI_NATIVE_METHOD(onSessionActive)
static void onSessionClosed(JNIEnv *env, jobject obj, jstring cameraId)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->onSessionClosed();
}
Q_DECLARE_JNI_NATIVE_METHOD(onSessionClosed)
static void onCaptureSessionFailed(JNIEnv *env, jobject obj, jstring cameraId, jint reason,
jlong framenumber)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->onCaptureSessionFailed(reason, framenumber);
}
Q_DECLARE_JNI_NATIVE_METHOD(onCaptureSessionFailed)
static void onStillPhotoCaptureFailed(JNIEnv *env, jobject obj, jstring cameraId)
{
Q_UNUSED(env);
Q_UNUSED(obj);
GET_CAMERA(cameraId);
camera->onStillPhotoCaptureFailed();
}
Q_DECLARE_JNI_NATIVE_METHOD(onStillPhotoCaptureFailed)
bool QFFmpeg::QAndroidCamera::registerNativeMethods()
{
static const bool registered = []() {
return QtJniCamera2::registerNativeMethods({
Q_JNI_NATIVE_METHOD(onCameraOpened),
Q_JNI_NATIVE_METHOD(onCameraDisconnect),
Q_JNI_NATIVE_METHOD(onCameraError),
Q_JNI_NATIVE_METHOD(onCaptureSessionConfigured),
Q_JNI_NATIVE_METHOD(onCaptureSessionConfigureFailed),
Q_JNI_NATIVE_METHOD(onStillReaderSwapFailed),
Q_JNI_NATIVE_METHOD(onCaptureSessionFailed),
Q_JNI_NATIVE_METHOD(onPreviewFrameAvailable),
Q_JNI_NATIVE_METHOD(onStillPhotoAvailable),
Q_JNI_NATIVE_METHOD(onSessionActive),
Q_JNI_NATIVE_METHOD(onSessionClosed),
Q_JNI_NATIVE_METHOD(onStillPhotoCaptureFailed),
});
}();
return registered;
}
QT_END_NAMESPACE