diff options
Diffstat (limited to 'src/plugins/multimedia/android/wrappers')
14 files changed, 3901 insertions, 0 deletions
diff --git a/src/plugins/multimedia/android/wrappers/jni/androidcamera.cpp b/src/plugins/multimedia/android/wrappers/jni/androidcamera.cpp new file mode 100644 index 000000000..939debad7 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidcamera.cpp @@ -0,0 +1,1797 @@ +// 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 "androidcamera_p.h" +#include "androidsurfacetexture_p.h" +#include "androidsurfaceview_p.h" +#include "qandroidmultimediautils_p.h" +#include "qandroidglobal_p.h" + +#include <private/qvideoframe_p.h> + +#include <qhash.h> +#include <qstringlist.h> +#include <qdebug.h> +#include <QtCore/qthread.h> +#include <QtCore/qreadwritelock.h> +#include <QtCore/qmutex.h> +#include <QtMultimedia/private/qmemoryvideobuffer_p.h> +#include <QtCore/qcoreapplication.h> + +#include <mutex> + +QT_BEGIN_NAMESPACE + +Q_STATIC_LOGGING_CATEGORY(lcAndroidCamera, "qt.multimedia.android.camera"); + +static const char QtCameraListenerClassName[] = "org/qtproject/qt/android/multimedia/QtCameraListener"; + +typedef QHash<int, AndroidCamera *> CameraMap; +Q_GLOBAL_STATIC(CameraMap, cameras) +Q_GLOBAL_STATIC(QReadWriteLock, rwLock) + +static QRect areaToRect(jobject areaObj) +{ + QJniObject area(areaObj); + QJniObject rect = area.getObjectField("rect", "Landroid/graphics/Rect;"); + + return QRect(rect.getField<jint>("left"), + rect.getField<jint>("top"), + rect.callMethod<jint>("width"), + rect.callMethod<jint>("height")); +} + +static QJniObject rectToArea(const QRect &rect) +{ + QJniObject jrect("android/graphics/Rect", + "(IIII)V", + rect.left(), rect.top(), rect.right(), rect.bottom()); + + QJniObject area("android/hardware/Camera$Area", + "(Landroid/graphics/Rect;I)V", + jrect.object(), 500); + + return area; +} + +// native method for QtCameraLisener.java +static void notifyAutoFocusComplete(JNIEnv* , jobject, int id, jboolean success) +{ + QReadLocker locker(rwLock); + const auto it = cameras->constFind(id); + if (Q_UNLIKELY(it == cameras->cend())) + return; + + Q_EMIT (*it)->autoFocusComplete(success); +} + +static void notifyPictureExposed(JNIEnv* , jobject, int id) +{ + QReadLocker locker(rwLock); + const auto it = cameras->constFind(id); + if (Q_UNLIKELY(it == cameras->cend())) + return; + + Q_EMIT (*it)->pictureExposed(); +} + +static void notifyPictureCaptured(JNIEnv *env, jobject, int id, jbyteArray data) +{ + QReadLocker locker(rwLock); + const auto it = cameras->constFind(id); + if (Q_UNLIKELY(it == cameras->cend())) { + qCWarning(lcAndroidCamera) << "Could not obtain camera!"; + return; + } + + AndroidCamera *camera = (*it); + + const int arrayLength = env->GetArrayLength(data); + QByteArray bytes(arrayLength, Qt::Uninitialized); + env->GetByteArrayRegion(data, 0, arrayLength, reinterpret_cast<jbyte *>(bytes.data())); + + auto parameters = camera->getParametersObject(); + + QJniObject size = + parameters.callObjectMethod("getPictureSize", "()Landroid/hardware/Camera$Size;"); + + if (!size.isValid()) { + qCWarning(lcAndroidCamera) << "Picture Size is not valid!"; + return; + } + + QSize pictureSize(size.getField<jint>("width"), size.getField<jint>("height")); + + auto format = AndroidCamera::ImageFormat(parameters.callMethod<jint>("getPictureFormat")); + + if (format == AndroidCamera::ImageFormat::UnknownImageFormat) { + qCWarning(lcAndroidCamera) << "Android Camera Image Format is UnknownImageFormat!"; + return; + } + + int bytesPerLine = 0; + + switch (format) { + case AndroidCamera::ImageFormat::YV12: + bytesPerLine = (pictureSize.width() + 15) & ~15; + break; + case AndroidCamera::ImageFormat::NV21: + bytesPerLine = pictureSize.width(); + break; + case AndroidCamera::ImageFormat::RGB565: + case AndroidCamera::ImageFormat::YUY2: + bytesPerLine = pictureSize.width() * 2; + break; + default: + bytesPerLine = -1; + } + + auto pictureFormat = qt_pixelFormatFromAndroidImageFormat(format); + + emit camera->pictureCaptured(bytes, pictureFormat, pictureSize, bytesPerLine); +} + +static void notifyNewPreviewFrame(JNIEnv *env, jobject, int id, jbyteArray data, + int width, int height, int format, int bpl) +{ + QReadLocker locker(rwLock); + const auto it = cameras->constFind(id); + if (Q_UNLIKELY(it == cameras->cend())) + return; + + const int arrayLength = env->GetArrayLength(data); + if (arrayLength == 0) + return; + + QByteArray bytes(arrayLength, Qt::Uninitialized); + env->GetByteArrayRegion(data, 0, arrayLength, (jbyte*)bytes.data()); + + QVideoFrameFormat frameFormat( + QSize(width, height), + qt_pixelFormatFromAndroidImageFormat(AndroidCamera::ImageFormat(format))); + + QVideoFrame frame = QVideoFramePrivate::createFrame( + std::make_unique<QMemoryVideoBuffer>(std::move(bytes), bpl), std::move(frameFormat)); + + Q_EMIT (*it)->newPreviewFrame(frame); +} + +static void notifyFrameAvailable(JNIEnv *, jobject, int id) +{ + QReadLocker locker(rwLock); + const auto it = cameras->constFind(id); + if (Q_UNLIKELY(it == cameras->cend())) + return; + + (*it)->fetchLastPreviewFrame(); +} + +class AndroidCameraPrivate : public QObject +{ + Q_OBJECT +public: + AndroidCameraPrivate(); + ~AndroidCameraPrivate(); + + Q_INVOKABLE bool init(int cameraId); + + Q_INVOKABLE void release(); + Q_INVOKABLE bool lock(); + Q_INVOKABLE bool unlock(); + Q_INVOKABLE bool reconnect(); + + Q_INVOKABLE AndroidCamera::CameraFacing getFacing(); + Q_INVOKABLE int getNativeOrientation(); + + Q_INVOKABLE QSize getPreferredPreviewSizeForVideo(); + Q_INVOKABLE QList<QSize> getSupportedPreviewSizes(); + static QList<QSize> getSupportedPreviewSizes(QJniObject ¶meters); + + Q_INVOKABLE QList<AndroidCamera::FpsRange> getSupportedPreviewFpsRange(); + + Q_INVOKABLE AndroidCamera::FpsRange getPreviewFpsRange(); + static AndroidCamera::FpsRange getPreviewFpsRange(QJniObject ¶meters); + Q_INVOKABLE void setPreviewFpsRange(int min, int max); + + Q_INVOKABLE AndroidCamera::ImageFormat getPreviewFormat(); + Q_INVOKABLE void setPreviewFormat(AndroidCamera::ImageFormat fmt); + Q_INVOKABLE QList<AndroidCamera::ImageFormat> getSupportedPreviewFormats(); + static QList<AndroidCamera::ImageFormat> getSupportedPreviewFormats(QJniObject ¶meters); + + Q_INVOKABLE QSize previewSize() const { return m_previewSize; } + Q_INVOKABLE QSize getPreviewSize(); + Q_INVOKABLE void updatePreviewSize(); + Q_INVOKABLE bool setPreviewTexture(void *surfaceTexture); + Q_INVOKABLE bool setPreviewDisplay(void *surfaceHolder); + Q_INVOKABLE void setDisplayOrientation(int degrees); + + Q_INVOKABLE bool isZoomSupported(); + Q_INVOKABLE int getMaxZoom(); + Q_INVOKABLE QList<int> getZoomRatios(); + Q_INVOKABLE int getZoom(); + Q_INVOKABLE void setZoom(int value); + + Q_INVOKABLE QString getFlashMode(); + Q_INVOKABLE void setFlashMode(const QString &value); + + Q_INVOKABLE QString getFocusMode(); + Q_INVOKABLE void setFocusMode(const QString &value); + + Q_INVOKABLE int getMaxNumFocusAreas(); + Q_INVOKABLE QList<QRect> getFocusAreas(); + Q_INVOKABLE void setFocusAreas(const QList<QRect> &areas); + + Q_INVOKABLE void autoFocus(); + Q_INVOKABLE void cancelAutoFocus(); + + Q_INVOKABLE bool isAutoExposureLockSupported(); + Q_INVOKABLE bool getAutoExposureLock(); + Q_INVOKABLE void setAutoExposureLock(bool toggle); + + Q_INVOKABLE bool isAutoWhiteBalanceLockSupported(); + Q_INVOKABLE bool getAutoWhiteBalanceLock(); + Q_INVOKABLE void setAutoWhiteBalanceLock(bool toggle); + + Q_INVOKABLE int getExposureCompensation(); + Q_INVOKABLE void setExposureCompensation(int value); + Q_INVOKABLE float getExposureCompensationStep(); + Q_INVOKABLE int getMinExposureCompensation(); + Q_INVOKABLE int getMaxExposureCompensation(); + + Q_INVOKABLE QString getSceneMode(); + Q_INVOKABLE void setSceneMode(const QString &value); + + Q_INVOKABLE QString getWhiteBalance(); + Q_INVOKABLE void setWhiteBalance(const QString &value); + + Q_INVOKABLE void updateRotation(); + + Q_INVOKABLE QList<QSize> getSupportedPictureSizes(); + Q_INVOKABLE QList<QSize> getSupportedVideoSizes(); + Q_INVOKABLE void setPictureSize(const QSize &size); + Q_INVOKABLE void setJpegQuality(int quality); + + Q_INVOKABLE void startPreview(); + Q_INVOKABLE void stopPreview(); + + Q_INVOKABLE void takePicture(); + + Q_INVOKABLE void setupPreviewFrameCallback(); + Q_INVOKABLE void notifyNewFrames(bool notify); + Q_INVOKABLE void fetchLastPreviewFrame(); + + Q_INVOKABLE void applyParameters(); + + Q_INVOKABLE QStringList callParametersStringListMethod(const QByteArray &methodName); + + int m_cameraId; + QRecursiveMutex m_parametersMutex; + QSize m_previewSize; + int m_rotation; + QJniObject m_info; + QJniObject m_parameters; + QJniObject m_camera; + QJniObject m_cameraListener; + +Q_SIGNALS: + void previewSizeChanged(); + void previewStarted(); + void previewFailedToStart(); + void previewStopped(); + + void autoFocusStarted(); + + void whiteBalanceChanged(); + + void takePictureFailed(); + + void lastPreviewFrameFetched(const QVideoFrame &frame); +}; + +AndroidCamera::AndroidCamera(AndroidCameraPrivate *d, QThread *worker) + : QObject(), + d_ptr(d), + m_worker(worker) + +{ + connect(d, &AndroidCameraPrivate::previewSizeChanged, this, &AndroidCamera::previewSizeChanged); + connect(d, &AndroidCameraPrivate::previewStarted, this, &AndroidCamera::previewStarted); + connect(d, &AndroidCameraPrivate::previewFailedToStart, this, &AndroidCamera::previewFailedToStart); + connect(d, &AndroidCameraPrivate::previewStopped, this, &AndroidCamera::previewStopped); + connect(d, &AndroidCameraPrivate::autoFocusStarted, this, &AndroidCamera::autoFocusStarted); + connect(d, &AndroidCameraPrivate::whiteBalanceChanged, this, &AndroidCamera::whiteBalanceChanged); + connect(d, &AndroidCameraPrivate::takePictureFailed, this, &AndroidCamera::takePictureFailed); + connect(d, &AndroidCameraPrivate::lastPreviewFrameFetched, this, &AndroidCamera::lastPreviewFrameFetched); +} + +AndroidCamera::~AndroidCamera() +{ + Q_D(AndroidCamera); + if (d->m_camera.isValid()) { + release(); + QWriteLocker locker(rwLock); + cameras->remove(cameraId()); + } + + m_worker->exit(); + m_worker->wait(5000); +} + +AndroidCamera *AndroidCamera::open(int cameraId) +{ + if (!qt_androidCheckCameraPermission()) + return nullptr; + + AndroidCameraPrivate *d = new AndroidCameraPrivate(); + QThread *worker = new QThread; + worker->start(); + d->moveToThread(worker); + connect(worker, &QThread::finished, d, &AndroidCameraPrivate::deleteLater); + bool ok = true; + QMetaObject::invokeMethod(d, "init", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, ok), Q_ARG(int, cameraId)); + if (!ok) { + worker->quit(); + worker->wait(5000); + delete worker; + return 0; + } + + AndroidCamera *q = new AndroidCamera(d, worker); + QWriteLocker locker(rwLock); + cameras->insert(cameraId, q); + + return q; +} + +int AndroidCamera::cameraId() const +{ + Q_D(const AndroidCamera); + return d->m_cameraId; +} + +bool AndroidCamera::lock() +{ + Q_D(AndroidCamera); + bool ok = true; + QMetaObject::invokeMethod(d, "lock", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, ok)); + return ok; +} + +bool AndroidCamera::unlock() +{ + Q_D(AndroidCamera); + bool ok = true; + QMetaObject::invokeMethod(d, "unlock", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, ok)); + return ok; +} + +bool AndroidCamera::reconnect() +{ + Q_D(AndroidCamera); + bool ok = true; + QMetaObject::invokeMethod(d, "reconnect", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, ok)); + return ok; +} + +void AndroidCamera::release() +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "release", Qt::BlockingQueuedConnection); +} + +AndroidCamera::CameraFacing AndroidCamera::getFacing() +{ + Q_D(AndroidCamera); + return d->getFacing(); +} + +int AndroidCamera::getNativeOrientation() +{ + Q_D(AndroidCamera); + return d->getNativeOrientation(); +} + +QSize AndroidCamera::getPreferredPreviewSizeForVideo() +{ + Q_D(AndroidCamera); + return d->getPreferredPreviewSizeForVideo(); +} + +QList<QSize> AndroidCamera::getSupportedPreviewSizes() +{ + Q_D(AndroidCamera); + return d->getSupportedPreviewSizes(); +} + +QList<AndroidCamera::FpsRange> AndroidCamera::getSupportedPreviewFpsRange() +{ + Q_D(AndroidCamera); + return d->getSupportedPreviewFpsRange(); +} + +AndroidCamera::FpsRange AndroidCamera::getPreviewFpsRange() +{ + Q_D(AndroidCamera); + return d->getPreviewFpsRange(); +} + +void AndroidCamera::setPreviewFpsRange(FpsRange range) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setPreviewFpsRange", Q_ARG(int, range.min), Q_ARG(int, range.max)); +} + +AndroidCamera::ImageFormat AndroidCamera::getPreviewFormat() +{ + Q_D(AndroidCamera); + return d->getPreviewFormat(); +} + +void AndroidCamera::setPreviewFormat(ImageFormat fmt) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setPreviewFormat", Q_ARG(AndroidCamera::ImageFormat, fmt)); +} + +QList<AndroidCamera::ImageFormat> AndroidCamera::getSupportedPreviewFormats() +{ + Q_D(AndroidCamera); + return d->getSupportedPreviewFormats(); +} + +QSize AndroidCamera::previewSize() const +{ + Q_D(const AndroidCamera); + return d->m_previewSize; +} + +QSize AndroidCamera::actualPreviewSize() +{ + Q_D(AndroidCamera); + return d->getPreviewSize(); +} + +void AndroidCamera::setPreviewSize(const QSize &size) +{ + Q_D(AndroidCamera); + d->m_parametersMutex.lock(); + bool areParametersValid = d->m_parameters.isValid(); + d->m_parametersMutex.unlock(); + if (!areParametersValid) + return; + + d->m_previewSize = size; + QMetaObject::invokeMethod(d, "updatePreviewSize"); +} + +bool AndroidCamera::setPreviewTexture(AndroidSurfaceTexture *surfaceTexture) +{ + Q_D(AndroidCamera); + bool ok = true; + QMetaObject::invokeMethod(d, + "setPreviewTexture", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, ok), + Q_ARG(void *, surfaceTexture ? surfaceTexture->surfaceTexture() : 0)); + return ok; +} + +bool AndroidCamera::setPreviewDisplay(AndroidSurfaceHolder *surfaceHolder) +{ + Q_D(AndroidCamera); + bool ok = true; + QMetaObject::invokeMethod(d, + "setPreviewDisplay", + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, ok), + Q_ARG(void *, surfaceHolder ? surfaceHolder->surfaceHolder() : 0)); + return ok; +} + +void AndroidCamera::setDisplayOrientation(int degrees) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setDisplayOrientation", Qt::QueuedConnection, Q_ARG(int, degrees)); +} + +bool AndroidCamera::isZoomSupported() +{ + Q_D(AndroidCamera); + return d->isZoomSupported(); +} + +int AndroidCamera::getMaxZoom() +{ + Q_D(AndroidCamera); + return d->getMaxZoom(); +} + +QList<int> AndroidCamera::getZoomRatios() +{ + Q_D(AndroidCamera); + return d->getZoomRatios(); +} + +int AndroidCamera::getZoom() +{ + Q_D(AndroidCamera); + return d->getZoom(); +} + +void AndroidCamera::setZoom(int value) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setZoom", Q_ARG(int, value)); +} + +QStringList AndroidCamera::getSupportedFlashModes() +{ + Q_D(AndroidCamera); + return d->callParametersStringListMethod("getSupportedFlashModes"); +} + +QString AndroidCamera::getFlashMode() +{ + Q_D(AndroidCamera); + return d->getFlashMode(); +} + +void AndroidCamera::setFlashMode(const QString &value) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setFlashMode", Q_ARG(QString, value)); +} + +QStringList AndroidCamera::getSupportedFocusModes() +{ + Q_D(AndroidCamera); + return d->callParametersStringListMethod("getSupportedFocusModes"); +} + +QString AndroidCamera::getFocusMode() +{ + Q_D(AndroidCamera); + return d->getFocusMode(); +} + +void AndroidCamera::setFocusMode(const QString &value) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setFocusMode", Q_ARG(QString, value)); +} + +int AndroidCamera::getMaxNumFocusAreas() +{ + Q_D(AndroidCamera); + return d->getMaxNumFocusAreas(); +} + +QList<QRect> AndroidCamera::getFocusAreas() +{ + Q_D(AndroidCamera); + return d->getFocusAreas(); +} + +void AndroidCamera::setFocusAreas(const QList<QRect> &areas) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setFocusAreas", Q_ARG(QList<QRect>, areas)); +} + +void AndroidCamera::autoFocus() +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "autoFocus"); +} + +void AndroidCamera::cancelAutoFocus() +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "cancelAutoFocus", Qt::QueuedConnection); +} + +bool AndroidCamera::isAutoExposureLockSupported() +{ + Q_D(AndroidCamera); + return d->isAutoExposureLockSupported(); +} + +bool AndroidCamera::getAutoExposureLock() +{ + Q_D(AndroidCamera); + return d->getAutoExposureLock(); +} + +void AndroidCamera::setAutoExposureLock(bool toggle) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setAutoExposureLock", Q_ARG(bool, toggle)); +} + +bool AndroidCamera::isAutoWhiteBalanceLockSupported() +{ + Q_D(AndroidCamera); + return d->isAutoWhiteBalanceLockSupported(); +} + +bool AndroidCamera::getAutoWhiteBalanceLock() +{ + Q_D(AndroidCamera); + return d->getAutoWhiteBalanceLock(); +} + +void AndroidCamera::setAutoWhiteBalanceLock(bool toggle) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setAutoWhiteBalanceLock", Q_ARG(bool, toggle)); +} + +int AndroidCamera::getExposureCompensation() +{ + Q_D(AndroidCamera); + return d->getExposureCompensation(); +} + +void AndroidCamera::setExposureCompensation(int value) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setExposureCompensation", Q_ARG(int, value)); +} + +float AndroidCamera::getExposureCompensationStep() +{ + Q_D(AndroidCamera); + return d->getExposureCompensationStep(); +} + +int AndroidCamera::getMinExposureCompensation() +{ + Q_D(AndroidCamera); + return d->getMinExposureCompensation(); +} + +int AndroidCamera::getMaxExposureCompensation() +{ + Q_D(AndroidCamera); + return d->getMaxExposureCompensation(); +} + +QStringList AndroidCamera::getSupportedSceneModes() +{ + Q_D(AndroidCamera); + return d->callParametersStringListMethod("getSupportedSceneModes"); +} + +QString AndroidCamera::getSceneMode() +{ + Q_D(AndroidCamera); + return d->getSceneMode(); +} + +void AndroidCamera::setSceneMode(const QString &value) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setSceneMode", Q_ARG(QString, value)); +} + +QStringList AndroidCamera::getSupportedWhiteBalance() +{ + Q_D(AndroidCamera); + return d->callParametersStringListMethod("getSupportedWhiteBalance"); +} + +QString AndroidCamera::getWhiteBalance() +{ + Q_D(AndroidCamera); + return d->getWhiteBalance(); +} + +void AndroidCamera::setWhiteBalance(const QString &value) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setWhiteBalance", Q_ARG(QString, value)); +} + +void AndroidCamera::setRotation(int rotation) +{ + Q_D(AndroidCamera); + //We need to do it here and not in worker class because we cache rotation + d->m_parametersMutex.lock(); + bool areParametersValid = d->m_parameters.isValid(); + d->m_parametersMutex.unlock(); + if (!areParametersValid) + return; + + d->m_rotation = rotation; + QMetaObject::invokeMethod(d, "updateRotation"); +} + +int AndroidCamera::getRotation() const +{ + Q_D(const AndroidCamera); + return d->m_rotation; +} + +QList<QSize> AndroidCamera::getSupportedPictureSizes() +{ + Q_D(AndroidCamera); + return d->getSupportedPictureSizes(); +} + +QList<QSize> AndroidCamera::getSupportedVideoSizes() +{ + Q_D(AndroidCamera); + return d->getSupportedVideoSizes(); +} + +void AndroidCamera::setPictureSize(const QSize &size) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setPictureSize", Q_ARG(QSize, size)); +} + +void AndroidCamera::setJpegQuality(int quality) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setJpegQuality", Q_ARG(int, quality)); +} + +void AndroidCamera::takePicture() +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "takePicture", Qt::BlockingQueuedConnection); +} + +void AndroidCamera::setupPreviewFrameCallback() +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "setupPreviewFrameCallback"); +} + +void AndroidCamera::notifyNewFrames(bool notify) +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "notifyNewFrames", Q_ARG(bool, notify)); +} + +void AndroidCamera::fetchLastPreviewFrame() +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "fetchLastPreviewFrame"); +} + +QJniObject AndroidCamera::getCameraObject() +{ + Q_D(AndroidCamera); + return d->m_camera; +} + +int AndroidCamera::getNumberOfCameras() +{ + if (!qt_androidCheckCameraPermission()) + return 0; + + return QJniObject::callStaticMethod<jint>("android/hardware/Camera", + "getNumberOfCameras"); +} + +void AndroidCamera::getCameraInfo(int id, QCameraDevicePrivate *info) +{ + Q_ASSERT(info); + + QJniObject cameraInfo("android/hardware/Camera$CameraInfo"); + QJniObject::callStaticMethod<void>("android/hardware/Camera", + "getCameraInfo", + "(ILandroid/hardware/Camera$CameraInfo;)V", + id, cameraInfo.object()); + + AndroidCamera::CameraFacing facing = AndroidCamera::CameraFacing(cameraInfo.getField<jint>("facing")); + // The orientation provided by Android is counter-clockwise, we need it clockwise + info->orientation = (360 - cameraInfo.getField<jint>("orientation")) % 360; + + switch (facing) { + case AndroidCamera::CameraFacingBack: + info->id = QByteArray("back"); + info->description = QStringLiteral("Rear-facing camera"); + info->position = QCameraDevice::BackFace; + info->isDefault = true; + break; + case AndroidCamera::CameraFacingFront: + info->id = QByteArray("front"); + info->description = QStringLiteral("Front-facing camera"); + info->position = QCameraDevice::FrontFace; + break; + default: + break; + } + // Add a number to allow correct access to cameras on systems with two + // (and more) front/back cameras + if (id > 1) { + info->id.append(QByteArray::number(id)); + info->description.append(QString(" %1").arg(id)); + } +} + +QVideoFrameFormat::PixelFormat AndroidCamera::QtPixelFormatFromAndroidImageFormat(AndroidCamera::ImageFormat format) +{ + switch (format) { + case AndroidCamera::NV21: + return QVideoFrameFormat::Format_NV21; + case AndroidCamera::YUY2: + return QVideoFrameFormat::Format_YUYV; + case AndroidCamera::JPEG: + return QVideoFrameFormat::Format_Jpeg; + case AndroidCamera::YV12: + return QVideoFrameFormat::Format_YV12; + default: + return QVideoFrameFormat::Format_Invalid; + } +} + +AndroidCamera::ImageFormat AndroidCamera::AndroidImageFormatFromQtPixelFormat(QVideoFrameFormat::PixelFormat format) +{ + switch (format) { + case QVideoFrameFormat::Format_NV21: + return AndroidCamera::NV21; + case QVideoFrameFormat::Format_YUYV: + return AndroidCamera::YUY2; + case QVideoFrameFormat::Format_Jpeg: + return AndroidCamera::JPEG; + case QVideoFrameFormat::Format_YV12: + return AndroidCamera::YV12; + default: + return AndroidCamera::UnknownImageFormat; + } +} + +QList<QCameraFormat> AndroidCamera::getSupportedFormats() +{ + QList<QCameraFormat> formats; + AndroidCamera::FpsRange range = getPreviewFpsRange(); + for (const auto &previewSize : getSupportedVideoSizes()) { + for (const auto &previewFormat : getSupportedPreviewFormats()) { + QCameraFormatPrivate * format = new QCameraFormatPrivate(); + format->pixelFormat = QtPixelFormatFromAndroidImageFormat(previewFormat); + format->resolution = previewSize; + format->minFrameRate = range.min; + format->maxFrameRate = range.max; + formats.append(format->create()); + } + } + + return formats; +} + +void AndroidCamera::startPreview() +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "startPreview"); +} + +void AndroidCamera::stopPreview() +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "stopPreview"); +} + +void AndroidCamera::stopPreviewSynchronous() +{ + Q_D(AndroidCamera); + QMetaObject::invokeMethod(d, "stopPreview", Qt::BlockingQueuedConnection); +} + +QJniObject AndroidCamera::getParametersObject() +{ + Q_D(AndroidCamera); + return d->m_parameters; +} + +AndroidCameraPrivate::AndroidCameraPrivate() + : QObject() +{ +} + +AndroidCameraPrivate::~AndroidCameraPrivate() +{ +} + +static qint32 s_activeCameras = 0; + +bool AndroidCameraPrivate::init(int cameraId) +{ + m_cameraId = cameraId; + QJniEnvironment env; + + const bool opened = s_activeCameras & (1 << cameraId); + if (opened) + return false; + + m_camera = QJniObject::callStaticObjectMethod("android/hardware/Camera", + "open", + "(I)Landroid/hardware/Camera;", + cameraId); + if (!m_camera.isValid()) + return false; + + m_cameraListener = QJniObject(QtCameraListenerClassName, "(I)V", m_cameraId); + m_info = QJniObject("android/hardware/Camera$CameraInfo"); + m_camera.callStaticMethod<void>("android/hardware/Camera", + "getCameraInfo", + "(ILandroid/hardware/Camera$CameraInfo;)V", + cameraId, + m_info.object()); + + QJniObject params = m_camera.callObjectMethod("getParameters", + "()Landroid/hardware/Camera$Parameters;"); + m_parameters = QJniObject(params); + s_activeCameras |= 1 << cameraId; + + return true; +} + +void AndroidCameraPrivate::release() +{ + m_previewSize = QSize(); + m_parametersMutex.lock(); + m_parameters = QJniObject(); + m_parametersMutex.unlock(); + if (m_camera.isValid()) { + m_camera.callMethod<void>("release"); + s_activeCameras &= ~(1 << m_cameraId); + } +} + +bool AndroidCameraPrivate::lock() +{ + QJniEnvironment env; + auto methodId = env->GetMethodID(m_camera.objectClass(), "lock", "()V"); + env->CallVoidMethod(m_camera.object(), methodId); + + if (env.checkAndClearExceptions()) + return false; + return true; +} + +bool AndroidCameraPrivate::unlock() +{ + QJniEnvironment env; + auto methodId = env->GetMethodID(m_camera.objectClass(), "unlock", "()V"); + env->CallVoidMethod(m_camera.object(), methodId); + + if (env.checkAndClearExceptions()) + return false; + return true; +} + +bool AndroidCameraPrivate::reconnect() +{ + QJniEnvironment env; + auto methodId = env->GetMethodID(m_camera.objectClass(), "reconnect", "()V"); + env->CallVoidMethod(m_camera.object(), methodId); + + if (env.checkAndClearExceptions()) + return false; + return true; +} + +AndroidCamera::CameraFacing AndroidCameraPrivate::getFacing() +{ + return AndroidCamera::CameraFacing(m_info.getField<jint>("facing")); +} + +int AndroidCameraPrivate::getNativeOrientation() +{ + return m_info.getField<jint>("orientation"); +} + +QSize AndroidCameraPrivate::getPreferredPreviewSizeForVideo() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return QSize(); + + QJniObject size = m_parameters.callObjectMethod("getPreferredPreviewSizeForVideo", + "()Landroid/hardware/Camera$Size;"); + + if (!size.isValid()) + return QSize(); + + return QSize(size.getField<jint>("width"), size.getField<jint>("height")); +} + +QList<QSize> AndroidCameraPrivate::getSupportedPreviewSizes() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + return getSupportedPreviewSizes(m_parameters); +} + +QList<QSize> AndroidCameraPrivate::getSupportedPreviewSizes(QJniObject ¶meters) +{ + QList<QSize> list; + + if (parameters.isValid()) { + QJniObject sizeList = parameters.callObjectMethod("getSupportedPreviewSizes", + "()Ljava/util/List;"); + int count = sizeList.callMethod<jint>("size"); + for (int i = 0; i < count; ++i) { + QJniObject size = sizeList.callObjectMethod("get", + "(I)Ljava/lang/Object;", + i); + list.append(QSize(size.getField<jint>("width"), size.getField<jint>("height"))); + } + + std::sort(list.begin(), list.end(), qt_sizeLessThan); + } + + return list; +} + +QList<AndroidCamera::FpsRange> AndroidCameraPrivate::getSupportedPreviewFpsRange() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + QJniEnvironment env; + + QList<AndroidCamera::FpsRange> rangeList; + + if (m_parameters.isValid()) { + QJniObject rangeListNative = m_parameters.callObjectMethod("getSupportedPreviewFpsRange", + "()Ljava/util/List;"); + int count = rangeListNative.callMethod<jint>("size"); + + rangeList.reserve(count); + + for (int i = 0; i < count; ++i) { + QJniObject range = rangeListNative.callObjectMethod("get", + "(I)Ljava/lang/Object;", + i); + + jintArray jRange = static_cast<jintArray>(range.object()); + jint* rangeArray = env->GetIntArrayElements(jRange, 0); + + AndroidCamera::FpsRange fpsRange; + + fpsRange.min = rangeArray[0]; + fpsRange.max = rangeArray[1]; + + env->ReleaseIntArrayElements(jRange, rangeArray, 0); + + rangeList << fpsRange; + } + } + + return rangeList; +} + +AndroidCamera::FpsRange AndroidCameraPrivate::getPreviewFpsRange() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + return getPreviewFpsRange(m_parameters); +} + +AndroidCamera::FpsRange AndroidCameraPrivate::getPreviewFpsRange(QJniObject ¶meters) +{ + QJniEnvironment env; + + AndroidCamera::FpsRange range; + + if (!parameters.isValid()) + return range; + + jintArray jRangeArray = env->NewIntArray(2); + parameters.callMethod<void>("getPreviewFpsRange", "([I)V", jRangeArray); + + jint* jRangeElements = env->GetIntArrayElements(jRangeArray, 0); + + // Android Camera API returns values scaled by 1000, so divide here to report + // normal values for Qt + range.min = jRangeElements[0] / 1000; + range.max = jRangeElements[1] / 1000; + + env->ReleaseIntArrayElements(jRangeArray, jRangeElements, 0); + env->DeleteLocalRef(jRangeArray); + + return range; +} + +void AndroidCameraPrivate::setPreviewFpsRange(int min, int max) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + // Android Camera API returns values scaled by 1000, so multiply here to + // give Android API the scale is expects + m_parameters.callMethod<void>("setPreviewFpsRange", "(II)V", min * 1000, max * 1000); +} + +AndroidCamera::ImageFormat AndroidCameraPrivate::getPreviewFormat() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return AndroidCamera::UnknownImageFormat; + + return AndroidCamera::ImageFormat(m_parameters.callMethod<jint>("getPreviewFormat")); +} + +void AndroidCameraPrivate::setPreviewFormat(AndroidCamera::ImageFormat fmt) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setPreviewFormat", "(I)V", jint(fmt)); + applyParameters(); +} + +QList<AndroidCamera::ImageFormat> AndroidCameraPrivate::getSupportedPreviewFormats() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + return getSupportedPreviewFormats(m_parameters); +} + +QList<AndroidCamera::ImageFormat> AndroidCameraPrivate::getSupportedPreviewFormats(QJniObject ¶meters) +{ + QList<AndroidCamera::ImageFormat> list; + + if (parameters.isValid()) { + QJniObject formatList = parameters.callObjectMethod("getSupportedPreviewFormats", + "()Ljava/util/List;"); + int count = formatList.callMethod<jint>("size"); + for (int i = 0; i < count; ++i) { + QJniObject format = formatList.callObjectMethod("get", + "(I)Ljava/lang/Object;", + i); + list.append(AndroidCamera::ImageFormat(format.callMethod<jint>("intValue"))); + } + } + + return list; +} + +QSize AndroidCameraPrivate::getPreviewSize() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return QSize(); + + QJniObject size = m_parameters.callObjectMethod("getPreviewSize", + "()Landroid/hardware/Camera$Size;"); + + if (!size.isValid()) + return QSize(); + + return QSize(size.getField<jint>("width"), size.getField<jint>("height")); +} + +void AndroidCameraPrivate::updatePreviewSize() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (m_previewSize.isValid()) { + m_parameters.callMethod<void>("setPreviewSize", "(II)V", m_previewSize.width(), m_previewSize.height()); + applyParameters(); + } + + emit previewSizeChanged(); +} + +bool AndroidCameraPrivate::setPreviewTexture(void *surfaceTexture) +{ + QJniEnvironment env; + auto methodId = env->GetMethodID(m_camera.objectClass(), "setPreviewTexture", + "(Landroid/graphics/SurfaceTexture;)V"); + env->CallVoidMethod(m_camera.object(), methodId, static_cast<jobject>(surfaceTexture)); + + if (env.checkAndClearExceptions()) + return false; + return true; +} + +bool AndroidCameraPrivate::setPreviewDisplay(void *surfaceHolder) +{ + QJniEnvironment env; + auto methodId = env->GetMethodID(m_camera.objectClass(), "setPreviewDisplay", + "(Landroid/view/SurfaceHolder;)V"); + env->CallVoidMethod(m_camera.object(), methodId, static_cast<jobject>(surfaceHolder)); + + if (env.checkAndClearExceptions()) + return false; + return true; +} + +void AndroidCameraPrivate::setDisplayOrientation(int degrees) +{ + m_camera.callMethod<void>("setDisplayOrientation", "(I)V", degrees); + m_cameraListener.callMethod<void>("setPhotoRotation", "(I)V", degrees); +} + +bool AndroidCameraPrivate::isZoomSupported() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return false; + + return m_parameters.callMethod<jboolean>("isZoomSupported"); +} + +int AndroidCameraPrivate::getMaxZoom() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return 0; + + return m_parameters.callMethod<jint>("getMaxZoom"); +} + +QList<int> AndroidCameraPrivate::getZoomRatios() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + QList<int> ratios; + + if (m_parameters.isValid()) { + QJniObject ratioList = m_parameters.callObjectMethod("getZoomRatios", + "()Ljava/util/List;"); + int count = ratioList.callMethod<jint>("size"); + for (int i = 0; i < count; ++i) { + QJniObject zoomRatio = ratioList.callObjectMethod("get", + "(I)Ljava/lang/Object;", + i); + + ratios.append(zoomRatio.callMethod<jint>("intValue")); + } + } + + return ratios; +} + +int AndroidCameraPrivate::getZoom() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return 0; + + return m_parameters.callMethod<jint>("getZoom"); +} + +void AndroidCameraPrivate::setZoom(int value) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setZoom", "(I)V", value); + applyParameters(); +} + +QString AndroidCameraPrivate::getFlashMode() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + QString value; + + if (m_parameters.isValid()) { + QJniObject flashMode = m_parameters.callObjectMethod("getFlashMode", + "()Ljava/lang/String;"); + if (flashMode.isValid()) + value = flashMode.toString(); + } + + return value; +} + +void AndroidCameraPrivate::setFlashMode(const QString &value) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setFlashMode", + "(Ljava/lang/String;)V", + QJniObject::fromString(value).object()); + applyParameters(); +} + +QString AndroidCameraPrivate::getFocusMode() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + QString value; + + if (m_parameters.isValid()) { + QJniObject focusMode = m_parameters.callObjectMethod("getFocusMode", + "()Ljava/lang/String;"); + if (focusMode.isValid()) + value = focusMode.toString(); + } + + return value; +} + +void AndroidCameraPrivate::setFocusMode(const QString &value) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setFocusMode", + "(Ljava/lang/String;)V", + QJniObject::fromString(value).object()); + applyParameters(); +} + +int AndroidCameraPrivate::getMaxNumFocusAreas() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return 0; + + return m_parameters.callMethod<jint>("getMaxNumFocusAreas"); +} + +QList<QRect> AndroidCameraPrivate::getFocusAreas() +{ + QList<QRect> areas; + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (m_parameters.isValid()) { + QJniObject list = m_parameters.callObjectMethod("getFocusAreas", + "()Ljava/util/List;"); + + if (list.isValid()) { + int count = list.callMethod<jint>("size"); + for (int i = 0; i < count; ++i) { + QJniObject area = list.callObjectMethod("get", + "(I)Ljava/lang/Object;", + i); + + areas.append(areaToRect(area.object())); + } + } + } + + return areas; +} + +void AndroidCameraPrivate::setFocusAreas(const QList<QRect> &areas) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid() || areas.isEmpty()) + return; + + QJniObject list; + + if (!areas.isEmpty()) { + QJniEnvironment env; + QJniObject arrayList("java/util/ArrayList", "(I)V", areas.size()); + for (int i = 0; i < areas.size(); ++i) { + arrayList.callMethod<jboolean>("add", + "(Ljava/lang/Object;)Z", + rectToArea(areas.at(i)).object()); + } + list = arrayList; + } + + m_parameters.callMethod<void>("setFocusAreas", "(Ljava/util/List;)V", list.object()); + + applyParameters(); +} + +void AndroidCameraPrivate::autoFocus() +{ + QJniEnvironment env; + auto methodId = env->GetMethodID(m_camera.objectClass(), "autoFocus", + "(Landroid/hardware/Camera$AutoFocusCallback;)V"); + env->CallVoidMethod(m_camera.object(), methodId, m_cameraListener.object()); + + if (!env.checkAndClearExceptions()) + emit autoFocusStarted(); +} + +void AndroidCameraPrivate::cancelAutoFocus() +{ + QJniEnvironment env; + m_camera.callMethod<void>("cancelAutoFocus"); +} + +bool AndroidCameraPrivate::isAutoExposureLockSupported() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return false; + + return m_parameters.callMethod<jboolean>("isAutoExposureLockSupported"); +} + +bool AndroidCameraPrivate::getAutoExposureLock() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return false; + + return m_parameters.callMethod<jboolean>("getAutoExposureLock"); +} + +void AndroidCameraPrivate::setAutoExposureLock(bool toggle) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setAutoExposureLock", "(Z)V", toggle); + applyParameters(); +} + +bool AndroidCameraPrivate::isAutoWhiteBalanceLockSupported() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return false; + + return m_parameters.callMethod<jboolean>("isAutoWhiteBalanceLockSupported"); +} + +bool AndroidCameraPrivate::getAutoWhiteBalanceLock() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return false; + + return m_parameters.callMethod<jboolean>("getAutoWhiteBalanceLock"); +} + +void AndroidCameraPrivate::setAutoWhiteBalanceLock(bool toggle) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setAutoWhiteBalanceLock", "(Z)V", toggle); + applyParameters(); +} + +int AndroidCameraPrivate::getExposureCompensation() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return 0; + + return m_parameters.callMethod<jint>("getExposureCompensation"); +} + +void AndroidCameraPrivate::setExposureCompensation(int value) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setExposureCompensation", "(I)V", value); + applyParameters(); +} + +float AndroidCameraPrivate::getExposureCompensationStep() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return 0; + + return m_parameters.callMethod<jfloat>("getExposureCompensationStep"); +} + +int AndroidCameraPrivate::getMinExposureCompensation() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return 0; + + return m_parameters.callMethod<jint>("getMinExposureCompensation"); +} + +int AndroidCameraPrivate::getMaxExposureCompensation() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return 0; + + return m_parameters.callMethod<jint>("getMaxExposureCompensation"); +} + +QString AndroidCameraPrivate::getSceneMode() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + QString value; + + if (m_parameters.isValid()) { + QJniObject sceneMode = m_parameters.callObjectMethod("getSceneMode", + "()Ljava/lang/String;"); + if (sceneMode.isValid()) + value = sceneMode.toString(); + } + + return value; +} + +void AndroidCameraPrivate::setSceneMode(const QString &value) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setSceneMode", + "(Ljava/lang/String;)V", + QJniObject::fromString(value).object()); + applyParameters(); +} + +QString AndroidCameraPrivate::getWhiteBalance() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + QString value; + + if (m_parameters.isValid()) { + QJniObject wb = m_parameters.callObjectMethod("getWhiteBalance", + "()Ljava/lang/String;"); + if (wb.isValid()) + value = wb.toString(); + } + + return value; +} + +void AndroidCameraPrivate::setWhiteBalance(const QString &value) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setWhiteBalance", + "(Ljava/lang/String;)V", + QJniObject::fromString(value).object()); + applyParameters(); + + emit whiteBalanceChanged(); +} + +void AndroidCameraPrivate::updateRotation() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + m_parameters.callMethod<void>("setRotation", "(I)V", m_rotation); + applyParameters(); +} + +QList<QSize> AndroidCameraPrivate::getSupportedPictureSizes() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + QList<QSize> list; + + if (m_parameters.isValid()) { + QJniObject sizeList = m_parameters.callObjectMethod("getSupportedPictureSizes", + "()Ljava/util/List;"); + int count = sizeList.callMethod<jint>("size"); + for (int i = 0; i < count; ++i) { + QJniObject size = sizeList.callObjectMethod("get", + "(I)Ljava/lang/Object;", + i); + list.append(QSize(size.getField<jint>("width"), size.getField<jint>("height"))); + } + + std::sort(list.begin(), list.end(), qt_sizeLessThan); + } + + return list; +} + +QList<QSize> AndroidCameraPrivate::getSupportedVideoSizes() +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + QList<QSize> list; + + if (m_parameters.isValid()) { + QJniObject sizeList = m_parameters.callObjectMethod("getSupportedVideoSizes", + "()Ljava/util/List;"); + if (!sizeList.isValid()) + return list; + + int count = sizeList.callMethod<jint>("size"); + for (int i = 0; i < count; ++i) { + const QJniObject size = sizeList.callObjectMethod("get", "(I)Ljava/lang/Object;", i); + if (size.isValid()) + list.append(QSize(size.getField<jint>("width"), size.getField<jint>("height"))); + } + std::sort(list.begin(), list.end(), qt_sizeLessThan); + } + + return list; +} + +void AndroidCameraPrivate::setPictureSize(const QSize &size) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setPictureSize", "(II)V", size.width(), size.height()); + applyParameters(); +} + +void AndroidCameraPrivate::setJpegQuality(int quality) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + if (!m_parameters.isValid()) + return; + + m_parameters.callMethod<void>("setJpegQuality", "(I)V", quality); + applyParameters(); +} + +void AndroidCameraPrivate::startPreview() +{ + setupPreviewFrameCallback(); + + QJniEnvironment env; + auto methodId = env->GetMethodID(m_camera.objectClass(), "startPreview", "()V"); + env->CallVoidMethod(m_camera.object(), methodId); + + if (env.checkAndClearExceptions()) + emit previewFailedToStart(); + else + emit previewStarted(); +} + +void AndroidCameraPrivate::stopPreview() +{ + // cancel any pending new frame notification + m_cameraListener.callMethod<void>("notifyWhenFrameAvailable", "(Z)V", false); + m_camera.callMethod<void>("stopPreview"); + emit previewStopped(); +} + +void AndroidCameraPrivate::takePicture() +{ + // We must clear the preview callback before calling takePicture(), otherwise the call will + // block and the camera server will be frozen until the next device restart... + // That problem only happens on some devices and on the emulator + m_cameraListener.callMethod<void>("clearPreviewCallback", "(Landroid/hardware/Camera;)V", m_camera.object()); + + QJniEnvironment env; + auto methodId = env->GetMethodID(m_camera.objectClass(), "takePicture", + "(Landroid/hardware/Camera$ShutterCallback;" + "Landroid/hardware/Camera$PictureCallback;" + "Landroid/hardware/Camera$PictureCallback;)V"); + env->CallVoidMethod(m_camera.object(), methodId, m_cameraListener.object(), + jobject(0), m_cameraListener.object()); + + if (env.checkAndClearExceptions()) + emit takePictureFailed(); +} + +void AndroidCameraPrivate::setupPreviewFrameCallback() +{ + m_cameraListener.callMethod<void>("setupPreviewCallback", "(Landroid/hardware/Camera;)V", m_camera.object()); +} + +void AndroidCameraPrivate::notifyNewFrames(bool notify) +{ + m_cameraListener.callMethod<void>("notifyNewFrames", "(Z)V", notify); +} + +void AndroidCameraPrivate::fetchLastPreviewFrame() +{ + QJniEnvironment env; + QJniObject data = m_cameraListener.callObjectMethod("lastPreviewBuffer", "()[B"); + + if (!data.isValid()) { + // If there's no buffer received yet, retry when the next one arrives + m_cameraListener.callMethod<void>("notifyWhenFrameAvailable", "(Z)V", true); + return; + } + + const int arrayLength = env->GetArrayLength(static_cast<jbyteArray>(data.object())); + if (arrayLength == 0) + return; + + QByteArray bytes(arrayLength, Qt::Uninitialized); + env->GetByteArrayRegion(static_cast<jbyteArray>(data.object()), + 0, + arrayLength, + reinterpret_cast<jbyte *>(bytes.data())); + + const int width = m_cameraListener.callMethod<jint>("previewWidth"); + const int height = m_cameraListener.callMethod<jint>("previewHeight"); + const int format = m_cameraListener.callMethod<jint>("previewFormat"); + const int bpl = m_cameraListener.callMethod<jint>("previewBytesPerLine"); + + QVideoFrameFormat frameFormat( + QSize(width, height), + qt_pixelFormatFromAndroidImageFormat(AndroidCamera::ImageFormat(format))); + + QVideoFrame frame = QVideoFramePrivate::createFrame( + std::make_unique<QMemoryVideoBuffer>(std::move(bytes), bpl), std::move(frameFormat)); + + emit lastPreviewFrameFetched(frame); +} + +void AndroidCameraPrivate::applyParameters() +{ + QJniEnvironment env; + m_camera.callMethod<void>("setParameters", + "(Landroid/hardware/Camera$Parameters;)V", + m_parameters.object()); +} + +QStringList AndroidCameraPrivate::callParametersStringListMethod(const QByteArray &methodName) +{ + const std::lock_guard<QRecursiveMutex> locker(m_parametersMutex); + + QStringList stringList; + + if (m_parameters.isValid()) { + QJniObject list = m_parameters.callObjectMethod(methodName.constData(), + "()Ljava/util/List;"); + + if (list.isValid()) { + int count = list.callMethod<jint>("size"); + for (int i = 0; i < count; ++i) { + QJniObject string = list.callObjectMethod("get", + "(I)Ljava/lang/Object;", + i); + stringList.append(string.toString()); + } + } + } + + return stringList; +} + +bool AndroidCamera::registerNativeMethods() +{ + static const JNINativeMethod methods[] = { + {"notifyAutoFocusComplete", "(IZ)V", (void *)notifyAutoFocusComplete}, + {"notifyPictureExposed", "(I)V", (void *)notifyPictureExposed}, + {"notifyPictureCaptured", "(I[B)V", (void *)notifyPictureCaptured}, + {"notifyNewPreviewFrame", "(I[BIIII)V", (void *)notifyNewPreviewFrame}, + {"notifyFrameAvailable", "(I)V", (void *)notifyFrameAvailable} + }; + + const int size = std::size(methods); + return QJniEnvironment().registerNativeMethods(QtCameraListenerClassName, methods, size); +} + +QT_END_NAMESPACE + +#include "androidcamera.moc" +#include "moc_androidcamera_p.cpp" diff --git a/src/plugins/multimedia/android/wrappers/jni/androidcamera_p.h b/src/plugins/multimedia/android/wrappers/jni/androidcamera_p.h new file mode 100644 index 000000000..8375cf3b1 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidcamera_p.h @@ -0,0 +1,208 @@ +// 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 + +#ifndef ANDROIDCAMERA_H +#define ANDROIDCAMERA_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 <qsize.h> +#include <qrect.h> +#include <QtMultimedia/qcamera.h> +#include <QtCore/qjniobject.h> +#include <private/qcameradevice_p.h> + +QT_BEGIN_NAMESPACE + +class QThread; + +class AndroidCameraPrivate; +class AndroidSurfaceTexture; +class AndroidSurfaceHolder; + +class AndroidCamera : public QObject +{ + Q_OBJECT +public: + enum CameraFacing { + CameraFacingBack = 0, + CameraFacingFront = 1 + }; + Q_ENUM(CameraFacing) + + enum ImageFormat { // same values as in android.graphics.ImageFormat Java class + UnknownImageFormat = 0, + RGB565 = 4, + NV16 = 16, + NV21 = 17, + YUY2 = 20, + JPEG = 256, + YV12 = 842094169 + }; + Q_ENUM(ImageFormat) + + // http://developer.android.com/reference/android/hardware/Camera.Parameters.html#getSupportedPreviewFpsRange%28%29 + // "The values are multiplied by 1000 and represented in integers" + struct FpsRange { + int min; + int max; + + FpsRange(): min(0), max(0) {} + + qreal getMinReal() const { return min / 1000.0; } + qreal getMaxReal() const { return max / 1000.0; } + + static FpsRange makeFromQReal(qreal min, qreal max) + { + FpsRange range; + range.min = static_cast<int>(min * 1000.0); + range.max = static_cast<int>(max * 1000.0); + return range; + } + }; + + ~AndroidCamera(); + + static AndroidCamera *open(int cameraId); + + int cameraId() const; + + bool lock(); + bool unlock(); + bool reconnect(); + void release(); + + CameraFacing getFacing(); + int getNativeOrientation(); + + QSize getPreferredPreviewSizeForVideo(); + QList<QSize> getSupportedPreviewSizes(); + + QList<FpsRange> getSupportedPreviewFpsRange(); + + FpsRange getPreviewFpsRange(); + void setPreviewFpsRange(FpsRange); + + ImageFormat getPreviewFormat(); + void setPreviewFormat(ImageFormat fmt); + QList<ImageFormat> getSupportedPreviewFormats(); + + QSize previewSize() const; + QSize actualPreviewSize(); + void setPreviewSize(const QSize &size); + bool setPreviewTexture(AndroidSurfaceTexture *surfaceTexture); + bool setPreviewDisplay(AndroidSurfaceHolder *surfaceHolder); + void setDisplayOrientation(int degrees); + + bool isZoomSupported(); + int getMaxZoom(); + QList<int> getZoomRatios(); + int getZoom(); + void setZoom(int value); + + QStringList getSupportedFlashModes(); + QString getFlashMode(); + void setFlashMode(const QString &value); + + QStringList getSupportedFocusModes(); + QString getFocusMode(); + void setFocusMode(const QString &value); + + int getMaxNumFocusAreas(); + QList<QRect> getFocusAreas(); + void setFocusAreas(const QList<QRect> &areas); + + void autoFocus(); + void cancelAutoFocus(); + + bool isAutoExposureLockSupported(); + bool getAutoExposureLock(); + void setAutoExposureLock(bool toggle); + + bool isAutoWhiteBalanceLockSupported(); + bool getAutoWhiteBalanceLock(); + void setAutoWhiteBalanceLock(bool toggle); + + int getExposureCompensation(); + void setExposureCompensation(int value); + float getExposureCompensationStep(); + int getMinExposureCompensation(); + int getMaxExposureCompensation(); + + QStringList getSupportedSceneModes(); + QString getSceneMode(); + void setSceneMode(const QString &value); + + QStringList getSupportedWhiteBalance(); + QString getWhiteBalance(); + void setWhiteBalance(const QString &value); + + void setRotation(int rotation); + int getRotation() const; + + QList<QCameraFormat> getSupportedFormats(); + QList<QSize> getSupportedPictureSizes(); + QList<QSize> getSupportedVideoSizes(); + void setPictureSize(const QSize &size); + void setJpegQuality(int quality); + + void startPreview(); + void stopPreview(); + void stopPreviewSynchronous(); + + void takePicture(); + + void setupPreviewFrameCallback(); + void notifyNewFrames(bool notify); + void fetchLastPreviewFrame(); + QJniObject getCameraObject(); + QJniObject getParametersObject(); + + static int getNumberOfCameras(); + static void getCameraInfo(int id, QCameraDevicePrivate *info); + static QVideoFrameFormat::PixelFormat QtPixelFormatFromAndroidImageFormat(AndroidCamera::ImageFormat); + static AndroidCamera::ImageFormat AndroidImageFormatFromQtPixelFormat(QVideoFrameFormat::PixelFormat); + static bool requestCameraPermission(); + + static bool registerNativeMethods(); +Q_SIGNALS: + void previewSizeChanged(); + void previewStarted(); + void previewFailedToStart(); + void previewStopped(); + + void autoFocusStarted(); + void autoFocusComplete(bool success); + + void whiteBalanceChanged(); + + void takePictureFailed(); + void pictureExposed(); + void pictureCaptured(const QByteArray &frame, QVideoFrameFormat::PixelFormat format, QSize size, int bytesPerLine); + void lastPreviewFrameFetched(const QVideoFrame &frame); + void newPreviewFrame(const QVideoFrame &frame); + +private: + AndroidCamera(AndroidCameraPrivate *d, QThread *worker); + + Q_DECLARE_PRIVATE(AndroidCamera) + AndroidCameraPrivate *d_ptr; + QScopedPointer<QThread> m_worker; +}; + +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(AndroidCamera::ImageFormat) + +#endif // ANDROIDCAMERA_H diff --git a/src/plugins/multimedia/android/wrappers/jni/androidmediametadataretriever.cpp b/src/plugins/multimedia/android/wrappers/jni/androidmediametadataretriever.cpp new file mode 100644 index 000000000..25e1efdb0 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidmediametadataretriever.cpp @@ -0,0 +1,136 @@ +// 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 "androidmediametadataretriever_p.h" + +#include <QtCore/QUrl> +#include <qdebug.h> +#include <QtCore/qcoreapplication.h> + +QT_BEGIN_NAMESPACE + +AndroidMediaMetadataRetriever::AndroidMediaMetadataRetriever() +{ + m_metadataRetriever = QJniObject("android/media/MediaMetadataRetriever"); +} + +AndroidMediaMetadataRetriever::~AndroidMediaMetadataRetriever() +{ + release(); +} + +QString AndroidMediaMetadataRetriever::extractMetadata(MetadataKey key) +{ + QString value; + + QJniObject metadata = m_metadataRetriever.callObjectMethod("extractMetadata", + "(I)Ljava/lang/String;", + jint(key)); + if (metadata.isValid()) + value = metadata.toString(); + + return value; +} + +void AndroidMediaMetadataRetriever::release() +{ + if (!m_metadataRetriever.isValid()) + return; + + m_metadataRetriever.callMethod<void>("release"); +} + +bool AndroidMediaMetadataRetriever::setDataSource(const QUrl &url) +{ + if (!m_metadataRetriever.isValid()) + return false; + + QJniEnvironment env; + if (url.isLocalFile()) { // also includes qrc files (copied to a temp file by QMediaPlayer) + QJniObject string = QJniObject::fromString(url.path()); + QJniObject fileInputStream("java/io/FileInputStream", + "(Ljava/lang/String;)V", + string.object()); + + if (!fileInputStream.isValid()) + return false; + + QJniObject fd = fileInputStream.callObjectMethod("getFD", + "()Ljava/io/FileDescriptor;"); + if (!fd.isValid()) { + fileInputStream.callMethod<void>("close"); + return false; + } + + auto methodId = env->GetMethodID(m_metadataRetriever.objectClass(), "setDataSource", + "(Ljava/io/FileDescriptor;)V"); + env->CallVoidMethod(m_metadataRetriever.object(), methodId, fd.object()); + bool ok = !env.checkAndClearExceptions(); + fileInputStream.callMethod<void>("close"); + if (!ok) + return false; + } else if (url.scheme() == QLatin1String("assets")) { + QJniObject string = QJniObject::fromString(url.path().mid(1)); // remove first '/' + QJniObject activity(QNativeInterface::QAndroidApplication::context()); + QJniObject assetManager = activity.callObjectMethod("getAssets", + "()Landroid/content/res/AssetManager;"); + QJniObject assetFd = assetManager.callObjectMethod("openFd", + "(Ljava/lang/String;)Landroid/content/res/AssetFileDescriptor;", + string.object()); + if (!assetFd.isValid()) + return false; + + QJniObject fd = assetFd.callObjectMethod("getFileDescriptor", + "()Ljava/io/FileDescriptor;"); + if (!fd.isValid()) { + assetFd.callMethod<void>("close"); + return false; + } + + auto methodId = env->GetMethodID(m_metadataRetriever.objectClass(), "setDataSource", + "(Ljava/io/FileDescriptor;JJ)V"); + env->CallVoidMethod(m_metadataRetriever.object(), methodId, + fd.object(), + assetFd.callMethod<jlong>("getStartOffset"), + assetFd.callMethod<jlong>("getLength")); + bool ok = !env.checkAndClearExceptions(); + assetFd.callMethod<void>("close"); + + if (!ok) + return false; + } else if (url.scheme() != QLatin1String("content")) { + // On API levels >= 14, only setDataSource(String, Map<String, String>) accepts remote media + QJniObject string = QJniObject::fromString(url.toString(QUrl::FullyEncoded)); + QJniObject hash("java/util/HashMap"); + + auto methodId = env->GetMethodID(m_metadataRetriever.objectClass(), "setDataSource", + "(Ljava/lang/String;Ljava/util/Map;)V"); + env->CallVoidMethod(m_metadataRetriever.object(), methodId, + string.object(), hash.object()); + if (env.checkAndClearExceptions()) + return false; + } else { + // While on API levels < 14, only setDataSource(Context, Uri) is available and works for + // remote media... + QJniObject string = QJniObject::fromString(url.toString(QUrl::FullyEncoded)); + QJniObject uri = m_metadataRetriever.callStaticObjectMethod( + "android/net/Uri", + "parse", + "(Ljava/lang/String;)Landroid/net/Uri;", + string.object()); + if (!uri.isValid()) + return false; + + auto methodId = env->GetMethodID(m_metadataRetriever.objectClass(), "setDataSource", + "(Landroid/content/Context;Landroid/net/Uri;)V"); + env->CallVoidMethod(m_metadataRetriever.object(), methodId, + QNativeInterface::QAndroidApplication::context().object(), + uri.object()); + if (env.checkAndClearExceptions()) + return false; + } + + return true; +} + +QT_END_NAMESPACE diff --git a/src/plugins/multimedia/android/wrappers/jni/androidmediametadataretriever_p.h b/src/plugins/multimedia/android/wrappers/jni/androidmediametadataretriever_p.h new file mode 100644 index 000000000..68e346336 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidmediametadataretriever_p.h @@ -0,0 +1,66 @@ +// 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 + +#ifndef ANDROIDMEDIAMETADATARETRIEVER_H +#define ANDROIDMEDIAMETADATARETRIEVER_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 <QtCore/private/qglobal_p.h> +#include <QtCore/qurl.h> +#include <QtCore/qjniobject.h> + +QT_BEGIN_NAMESPACE + +class AndroidMediaMetadataRetriever +{ +public: + enum MetadataKey { + Album = 1, + AlbumArtist = 13, + Artist = 2, + Author = 3, + Bitrate = 20, + CDTrackNumber = 0, + Compilation = 15, + Composer = 4, + Date = 5, + DiscNumber = 14, + Duration = 9, + Genre = 6, + HasAudio = 16, + HasVideo = 17, + Location = 23, + MimeType = 12, + NumTracks = 10, + Title = 7, + VideoHeight = 19, + VideoWidth = 18, + VideoRotation = 24, + Writer = 11, + Year = 8 + }; + + AndroidMediaMetadataRetriever(); + ~AndroidMediaMetadataRetriever(); + + QString extractMetadata(MetadataKey key); + bool setDataSource(const QUrl &url); + +private: + void release(); + QJniObject m_metadataRetriever; +}; + +QT_END_NAMESPACE + +#endif // ANDROIDMEDIAMETADATARETRIEVER_H diff --git a/src/plugins/multimedia/android/wrappers/jni/androidmediaplayer.cpp b/src/plugins/multimedia/android/wrappers/jni/androidmediaplayer.cpp new file mode 100644 index 000000000..58a54f01b --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidmediaplayer.cpp @@ -0,0 +1,535 @@ +// 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 "androidmediaplayer_p.h" +#include "androidsurfacetexture_p.h" + +#include <QList> +#include <QReadWriteLock> +#include <QString> +#include <QtCore/qcoreapplication.h> +#include <qloggingcategory.h> + +static const char QtAndroidMediaPlayerClassName[] = "org/qtproject/qt/android/multimedia/QtAndroidMediaPlayer"; +typedef QList<AndroidMediaPlayer *> MediaPlayerList; +Q_GLOBAL_STATIC(MediaPlayerList, mediaPlayers) +Q_GLOBAL_STATIC(QReadWriteLock, rwLock) + +QT_BEGIN_NAMESPACE + +Q_STATIC_LOGGING_CATEGORY(lcAudio, "qt.multimedia.audio"); + +AndroidMediaPlayer::AndroidMediaPlayer() + : QObject() +{ + QWriteLocker locker(rwLock); + auto context = QNativeInterface::QAndroidApplication::context(); + const jlong id = reinterpret_cast<jlong>(this); + mMediaPlayer = QJniObject(QtAndroidMediaPlayerClassName, + "(Landroid/content/Context;J)V", + context.object(), + id); + mediaPlayers->append(this); +} + +AndroidMediaPlayer::~AndroidMediaPlayer() +{ + QWriteLocker locker(rwLock); + const int i = mediaPlayers->indexOf(this); + Q_ASSERT(i != -1); + mediaPlayers->remove(i); +} + +void AndroidMediaPlayer::release() +{ + mMediaPlayer.callMethod<void>("release"); +} + +void AndroidMediaPlayer::reset() +{ + mMediaPlayer.callMethod<void>("reset"); +} + +int AndroidMediaPlayer::getCurrentPosition() +{ + return mMediaPlayer.callMethod<jint>("getCurrentPosition"); +} + +int AndroidMediaPlayer::getDuration() +{ + return mMediaPlayer.callMethod<jint>("getDuration"); +} + +bool AndroidMediaPlayer::isPlaying() +{ + return mMediaPlayer.callMethod<jboolean>("isPlaying"); +} + +int AndroidMediaPlayer::volume() +{ + return mMediaPlayer.callMethod<jint>("getVolume"); +} + +bool AndroidMediaPlayer::isMuted() +{ + return mMediaPlayer.callMethod<jboolean>("isMuted"); +} + +qreal AndroidMediaPlayer::playbackRate() +{ + qreal rate(1.0); + + if (QNativeInterface::QAndroidApplication::sdkVersion() < 23) + return rate; + + QJniObject player = mMediaPlayer.callObjectMethod("getMediaPlayerHandle", + "()Landroid/media/MediaPlayer;"); + if (player.isValid()) { + QJniObject playbackParams = player.callObjectMethod("getPlaybackParams", + "()Landroid/media/PlaybackParams;"); + if (playbackParams.isValid()) { + QJniEnvironment env; + auto methodId = env->GetMethodID(playbackParams.objectClass(), "getSpeed", "()F"); + const qreal speed = env->CallFloatMethod(playbackParams.object(), methodId); + if (!env.checkAndClearExceptions()) + rate = speed; + } + } + + return rate; +} + +jobject AndroidMediaPlayer::display() +{ + return mMediaPlayer.callObjectMethod("display", "()Landroid/view/SurfaceHolder;").object(); +} + +AndroidMediaPlayer::TrackInfo convertTrackInfo(int streamNumber, QJniObject androidTrackInfo) +{ + const QLatin1String unknownMimeType("application/octet-stream"); + const QLatin1String undefinedLanguage("und"); + + if (!androidTrackInfo.isValid()) + return { streamNumber, AndroidMediaPlayer::TrackType::Unknown, undefinedLanguage, + unknownMimeType }; + + QJniEnvironment env; + auto methodId = env->GetMethodID(androidTrackInfo.objectClass(), "getType", "()I"); + const jint type = env->CallIntMethod(androidTrackInfo.object(), methodId); + if (env.checkAndClearExceptions()) + return { streamNumber, AndroidMediaPlayer::TrackType::Unknown, undefinedLanguage, + unknownMimeType }; + + if (type < 0 || type > 5) { + return { streamNumber, AndroidMediaPlayer::TrackType::Unknown, undefinedLanguage, + unknownMimeType }; + } + + AndroidMediaPlayer::TrackType trackType = static_cast<AndroidMediaPlayer::TrackType>(type); + + auto languageObject = androidTrackInfo.callObjectMethod("getLanguage", "()Ljava/lang/String;"); + QString language = languageObject.isValid() ? languageObject.toString() : undefinedLanguage; + + auto mimeTypeObject = androidTrackInfo.callObjectMethod("getMime", "()Ljava/lang/String;"); + QString mimeType = mimeTypeObject.isValid() ? mimeTypeObject.toString() : unknownMimeType; + + return { streamNumber, trackType, language, mimeType }; +} + +QList<AndroidMediaPlayer::TrackInfo> AndroidMediaPlayer::tracksInfo() +{ + auto androidTracksInfoObject = mMediaPlayer.callObjectMethod( + "getAllTrackInfo", + "()[Lorg/qtproject/qt/android/multimedia/QtAndroidMediaPlayer$TrackInfo;"); + + if (!androidTracksInfoObject.isValid()) + return QList<AndroidMediaPlayer::TrackInfo>(); + + auto androidTracksInfo = androidTracksInfoObject.object<jobjectArray>(); + if (!androidTracksInfo) + return QList<AndroidMediaPlayer::TrackInfo>(); + + QJniEnvironment environment; + auto numberofTracks = environment->GetArrayLength(androidTracksInfo); + + QList<AndroidMediaPlayer::TrackInfo> tracksInformation; + + for (int index = 0; index < numberofTracks; index++) { + auto androidTrackInformation = environment->GetObjectArrayElement(androidTracksInfo, index); + + if (environment.checkAndClearExceptions()) { + continue; + } + + auto trackInfo = convertTrackInfo(index, androidTrackInformation); + tracksInformation.insert(index, trackInfo); + + environment->DeleteLocalRef(androidTrackInformation); + } + + return tracksInformation; +} + +int AndroidMediaPlayer::activeTrack(TrackType androidTrackType) +{ + int type = static_cast<int>(androidTrackType); + return mMediaPlayer.callMethod<jint>("getSelectedTrack", "(I)I", type); +} + +void AndroidMediaPlayer::deselectTrack(int trackNumber) +{ + mMediaPlayer.callMethod<void>("deselectTrack", "(I)V", trackNumber); +} + +void AndroidMediaPlayer::selectTrack(int trackNumber) +{ + mMediaPlayer.callMethod<void>("selectTrack", "(I)V", trackNumber); +} + +void AndroidMediaPlayer::play() +{ + mMediaPlayer.callMethod<void>("start"); +} + +void AndroidMediaPlayer::pause() +{ + mMediaPlayer.callMethod<void>("pause"); +} + +void AndroidMediaPlayer::stop() +{ + mMediaPlayer.callMethod<void>("stop"); +} + +void AndroidMediaPlayer::seekTo(qint32 msec) +{ + mMediaPlayer.callMethod<void>("seekTo", "(I)V", jint(msec)); +} + +void AndroidMediaPlayer::setMuted(bool mute) +{ + if (mAudioBlocked) + return; + + mMediaPlayer.callMethod<void>("mute", "(Z)V", jboolean(mute)); +} + +void AndroidMediaPlayer::setDataSource(const QNetworkRequest &request) +{ + QJniObject string = QJniObject::fromString(request.url().toString(QUrl::FullyEncoded)); + + mMediaPlayer.callMethod<void>("initHeaders", "()V"); + for (auto &header : request.rawHeaderList()) { + auto value = request.rawHeader(header); + mMediaPlayer.callMethod<void>("setHeader", "(Ljava/lang/String;Ljava/lang/String;)V", + QJniObject::fromString(QLatin1String(header)).object(), + QJniObject::fromString(QLatin1String(value)).object()); + } + + mMediaPlayer.callMethod<void>("setDataSource", "(Ljava/lang/String;)V", string.object()); +} + +void AndroidMediaPlayer::prepareAsync() +{ + mMediaPlayer.callMethod<void>("prepareAsync"); +} + +void AndroidMediaPlayer::setVolume(int volume) +{ + if (mAudioBlocked) + return; + + mMediaPlayer.callMethod<void>("setVolume", "(I)V", jint(volume)); +} + +void AndroidMediaPlayer::blockAudio() +{ + mAudioBlocked = true; +} + +void AndroidMediaPlayer::unblockAudio() +{ + mAudioBlocked = false; +} + +void AndroidMediaPlayer::startSoundStreaming(const int inputId, const int outputId) +{ + QJniObject::callStaticMethod<void>("org/qtproject/qt/android/multimedia/QtAudioDeviceManager", + "startSoundStreaming", + inputId, + outputId); +} + +void AndroidMediaPlayer::stopSoundStreaming() +{ + QJniObject::callStaticMethod<void>( + "org/qtproject/qt/android/multimedia/QtAudioDeviceManager", "stopSoundStreaming"); +} + +bool AndroidMediaPlayer::setPlaybackRate(qreal rate) +{ + if (QNativeInterface::QAndroidApplication::sdkVersion() < 23) { + qWarning() << "Setting the playback rate on a media player requires" + << "Android 6.0 (API level 23) or later"; + return false; + } + + return mMediaPlayer.callMethod<jboolean>("setPlaybackRate", jfloat(rate)); +} + +void AndroidMediaPlayer::setDisplay(AndroidSurfaceTexture *surfaceTexture) +{ + mMediaPlayer.callMethod<void>("setDisplay", + "(Landroid/view/SurfaceHolder;)V", + surfaceTexture ? surfaceTexture->surfaceHolder() : 0); +} + +bool AndroidMediaPlayer::setAudioOutput(const QByteArray &deviceId) +{ + const bool ret = QJniObject::callStaticMethod<jboolean>( + "org/qtproject/qt/android/multimedia/QtAudioDeviceManager", + "setAudioOutput", + "(I)Z", + deviceId.toInt()); + + if (!ret) + qCWarning(lcAudio) << "Output device not set"; + + return ret; +} + +#if 0 +void AndroidMediaPlayer::setAudioRole(QAudio::Role role) +{ + QString r; + switch (role) { + case QAudio::MusicRole: + r = QLatin1String("CONTENT_TYPE_MUSIC"); + break; + case QAudio::VideoRole: + r = QLatin1String("CONTENT_TYPE_MOVIE"); + break; + case QAudio::VoiceCommunicationRole: + r = QLatin1String("USAGE_VOICE_COMMUNICATION"); + break; + case QAudio::AlarmRole: + r = QLatin1String("USAGE_ALARM"); + break; + case QAudio::NotificationRole: + r = QLatin1String("USAGE_NOTIFICATION"); + break; + case QAudio::RingtoneRole: + r = QLatin1String("USAGE_NOTIFICATION_RINGTONE"); + break; + case QAudio::AccessibilityRole: + r = QLatin1String("USAGE_ASSISTANCE_ACCESSIBILITY"); + break; + case QAudio::SonificationRole: + r = QLatin1String("CONTENT_TYPE_SONIFICATION"); + break; + case QAudio::GameRole: + r = QLatin1String("USAGE_GAME"); + break; + default: + return; + } + + int type = 0; // CONTENT_TYPE_UNKNOWN + int usage = 0; // USAGE_UNKNOWN + + if (r == QLatin1String("CONTENT_TYPE_MOVIE")) + type = 3; + else if (r == QLatin1String("CONTENT_TYPE_MUSIC")) + type = 2; + else if (r == QLatin1String("CONTENT_TYPE_SONIFICATION")) + type = 4; + else if (r == QLatin1String("CONTENT_TYPE_SPEECH")) + type = 1; + else if (r == QLatin1String("USAGE_ALARM")) + usage = 4; + else if (r == QLatin1String("USAGE_ASSISTANCE_ACCESSIBILITY")) + usage = 11; + else if (r == QLatin1String("USAGE_ASSISTANCE_NAVIGATION_GUIDANCE")) + usage = 12; + else if (r == QLatin1String("USAGE_ASSISTANCE_SONIFICATION")) + usage = 13; + else if (r == QLatin1String("USAGE_ASSISTANT")) + usage = 16; + else if (r == QLatin1String("USAGE_GAME")) + usage = 14; + else if (r == QLatin1String("USAGE_MEDIA")) + usage = 1; + else if (r == QLatin1String("USAGE_NOTIFICATION")) + usage = 5; + else if (r == QLatin1String("USAGE_NOTIFICATION_COMMUNICATION_DELAYED")) + usage = 9; + else if (r == QLatin1String("USAGE_NOTIFICATION_COMMUNICATION_INSTANT")) + usage = 8; + else if (r == QLatin1String("USAGE_NOTIFICATION_COMMUNICATION_REQUEST")) + usage = 7; + else if (r == QLatin1String("USAGE_NOTIFICATION_EVENT")) + usage = 10; + else if (r == QLatin1String("USAGE_NOTIFICATION_RINGTONE")) + usage = 6; + else if (r == QLatin1String("USAGE_VOICE_COMMUNICATION")) + usage = 2; + else if (r == QLatin1String("USAGE_VOICE_COMMUNICATION_SIGNALLING")) + usage = 3; + + mMediaPlayer.callMethod<void>("setAudioAttributes", "(II)V", jint(type), jint(usage)); +} +#endif + +static void onErrorNative(JNIEnv *env, jobject thiz, jint what, jint extra, jlong id) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + QReadLocker locker(rwLock); + const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id)); + if (Q_UNLIKELY(i == -1)) + return; + + Q_EMIT (*mediaPlayers)[i]->error(what, extra); +} + +static void onBufferingUpdateNative(JNIEnv *env, jobject thiz, jint percent, jlong id) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + QReadLocker locker(rwLock); + const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id)); + if (Q_UNLIKELY(i == -1)) + return; + + Q_EMIT (*mediaPlayers)[i]->bufferingChanged(percent); +} + +static void onProgressUpdateNative(JNIEnv *env, jobject thiz, jint progress, jlong id) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + QReadLocker locker(rwLock); + const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id)); + if (Q_UNLIKELY(i == -1)) + return; + + Q_EMIT (*mediaPlayers)[i]->progressChanged(progress); +} + +static void onDurationChangedNative(JNIEnv *env, jobject thiz, jint duration, jlong id) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + QReadLocker locker(rwLock); + const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id)); + if (Q_UNLIKELY(i == -1)) + return; + + Q_EMIT (*mediaPlayers)[i]->durationChanged(duration); +} + +static void onInfoNative(JNIEnv *env, jobject thiz, jint what, jint extra, jlong id) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + QReadLocker locker(rwLock); + const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id)); + if (Q_UNLIKELY(i == -1)) + return; + + Q_EMIT (*mediaPlayers)[i]->info(what, extra); +} + +static void onStateChangedNative(JNIEnv *env, jobject thiz, jint state, jlong id) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + QReadLocker locker(rwLock); + const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id)); + if (Q_UNLIKELY(i == -1)) + return; + + Q_EMIT (*mediaPlayers)[i]->stateChanged(state); +} + +static void onVideoSizeChangedNative(JNIEnv *env, + jobject thiz, + jint width, + jint height, + jlong id) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + QReadLocker locker(rwLock); + const int i = mediaPlayers->indexOf(reinterpret_cast<AndroidMediaPlayer *>(id)); + if (Q_UNLIKELY(i == -1)) + return; + + Q_EMIT (*mediaPlayers)[i]->videoSizeChanged(width, height); +} + +static AndroidMediaPlayer *getMediaPlayer(jlong ptr) +{ + auto mediaplayer = reinterpret_cast<AndroidMediaPlayer *>(ptr); + if (!mediaplayer || !mediaPlayers->contains(mediaplayer)) + return nullptr; + + return mediaplayer; +} + +static void onTrackInfoChangedNative(JNIEnv *env, jobject thiz, jlong ptr) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + + QReadLocker locker(rwLock); + auto mediaplayer = getMediaPlayer(ptr); + if (!mediaplayer) + return; + + emit mediaplayer->tracksInfoChanged(); +} + +static void onTimedTextChangedNative(JNIEnv *env, jobject thiz, jstring timedText, jint time, + jlong ptr) +{ + Q_UNUSED(env); + Q_UNUSED(thiz); + Q_UNUSED(time); + + QReadLocker locker(rwLock); + + auto mediaplayer = getMediaPlayer(ptr); + if (!mediaplayer) + return; + + QString subtitleText; + if (timedText != nullptr) + subtitleText = QString::fromUtf8(env->GetStringUTFChars(timedText, 0)); + + emit mediaplayer->timedTextChanged(subtitleText); +} + +bool AndroidMediaPlayer::registerNativeMethods() +{ + static const JNINativeMethod methods[] = { + { "onErrorNative", "(IIJ)V", reinterpret_cast<void *>(onErrorNative) }, + { "onBufferingUpdateNative", "(IJ)V", reinterpret_cast<void *>(onBufferingUpdateNative) }, + { "onProgressUpdateNative", "(IJ)V", reinterpret_cast<void *>(onProgressUpdateNative) }, + { "onDurationChangedNative", "(IJ)V", reinterpret_cast<void *>(onDurationChangedNative) }, + { "onInfoNative", "(IIJ)V", reinterpret_cast<void *>(onInfoNative) }, + { "onVideoSizeChangedNative", "(IIJ)V", + reinterpret_cast<void *>(onVideoSizeChangedNative) }, + { "onStateChangedNative", "(IJ)V", reinterpret_cast<void *>(onStateChangedNative) }, + { "onTrackInfoChangedNative", "(J)V", reinterpret_cast<void *>(onTrackInfoChangedNative) }, + { "onTimedTextChangedNative", "(Ljava/lang/String;IJ)V", + reinterpret_cast<void *>(onTimedTextChangedNative) } + }; + + const int size = std::size(methods); + return QJniEnvironment().registerNativeMethods(QtAndroidMediaPlayerClassName, methods, size); +} + +QT_END_NAMESPACE + +#include "moc_androidmediaplayer_p.cpp" diff --git a/src/plugins/multimedia/android/wrappers/jni/androidmediaplayer_p.h b/src/plugins/multimedia/android/wrappers/jni/androidmediaplayer_p.h new file mode 100644 index 000000000..66095b114 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidmediaplayer_p.h @@ -0,0 +1,135 @@ +// 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 + +#ifndef ANDROIDMEDIAPLAYER_H +#define ANDROIDMEDIAPLAYER_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> +#include <QNetworkRequest> +#include <QtCore/qjniobject.h> +#include <QAudio> + +QT_BEGIN_NAMESPACE + +class AndroidSurfaceTexture; + +class AndroidMediaPlayer : public QObject +{ + Q_OBJECT +public: + AndroidMediaPlayer(); + ~AndroidMediaPlayer(); + + enum MediaError + { + // What + MEDIA_ERROR_UNKNOWN = 1, + MEDIA_ERROR_SERVER_DIED = 100, + MEDIA_ERROR_INVALID_STATE = -38, // Undocumented + // Extra + MEDIA_ERROR_IO = -1004, + MEDIA_ERROR_MALFORMED = -1007, + MEDIA_ERROR_UNSUPPORTED = -1010, + MEDIA_ERROR_TIMED_OUT = -110, + MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200, + MEDIA_ERROR_BAD_THINGS_ARE_GOING_TO_HAPPEN = -2147483648 // Undocumented + }; + + enum MediaInfo + { + MEDIA_INFO_UNKNOWN = 1, + MEDIA_INFO_VIDEO_TRACK_LAGGING = 700, + MEDIA_INFO_VIDEO_RENDERING_START = 3, + MEDIA_INFO_BUFFERING_START = 701, + MEDIA_INFO_BUFFERING_END = 702, + MEDIA_INFO_BAD_INTERLEAVING = 800, + MEDIA_INFO_NOT_SEEKABLE = 801, + MEDIA_INFO_METADATA_UPDATE = 802 + }; + + enum MediaPlayerState { + Uninitialized = 0x1, /* End */ + Idle = 0x2, + Preparing = 0x4, + Prepared = 0x8, + Initialized = 0x10, + Started = 0x20, + Stopped = 0x40, + Paused = 0x80, + PlaybackCompleted = 0x100, + Error = 0x200 + }; + + enum TrackType { Unknown = 0, Video, Audio, TimedText, Subtitle, Metadata }; + + struct TrackInfo + { + int trackNumber; + TrackType trackType; + QString language; + QString mimeType; + }; + + void release(); + void reset(); + + int getCurrentPosition(); + int getDuration(); + bool isPlaying(); + int volume(); + bool isMuted(); + qreal playbackRate(); + jobject display(); + + void play(); + void pause(); + void stop(); + void seekTo(qint32 msec); + void setMuted(bool mute); + void setDataSource(const QNetworkRequest &request); + void prepareAsync(); + void setVolume(int volume); + static void startSoundStreaming(const int inputId, const int outputId); + static void stopSoundStreaming(); + bool setPlaybackRate(qreal rate); + void setDisplay(AndroidSurfaceTexture *surfaceTexture); + static bool setAudioOutput(const QByteArray &deviceId); + QList<TrackInfo> tracksInfo(); + int activeTrack(TrackType trackType); + void deselectTrack(int trackNumber); + void selectTrack(int trackNumber); + + static bool registerNativeMethods(); + + void blockAudio(); + void unblockAudio(); +Q_SIGNALS: + void error(qint32 what, qint32 extra); + void bufferingChanged(qint32 percent); + void durationChanged(qint64 duration); + void progressChanged(qint64 progress); + void stateChanged(qint32 state); + void info(qint32 what, qint32 extra); + void videoSizeChanged(qint32 width, qint32 height); + void timedTextChanged(QString text); + void tracksInfoChanged(); + +private: + QJniObject mMediaPlayer; + bool mAudioBlocked = false; +}; + +QT_END_NAMESPACE + +#endif // ANDROIDMEDIAPLAYER_H diff --git a/src/plugins/multimedia/android/wrappers/jni/androidmediarecorder.cpp b/src/plugins/multimedia/android/wrappers/jni/androidmediarecorder.cpp new file mode 100644 index 000000000..48da7ac01 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidmediarecorder.cpp @@ -0,0 +1,337 @@ +// 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 "androidmediarecorder_p.h" +#include "androidcamera_p.h" +#include "androidsurfacetexture_p.h" +#include "androidsurfaceview_p.h" +#include "qandroidglobal_p.h" +#include "qandroidmultimediautils_p.h" + +#include <qmap.h> +#include <QtCore/qcoreapplication.h> +#include <QtCore/qlogging.h> +#include <QtCore/qurl.h> + +QT_BEGIN_NAMESPACE + +Q_STATIC_LOGGING_CATEGORY(lcMediaRecorder, "qt.multimedia.mediarecorder.android"); + +typedef QMap<QString, QJniObject> CamcorderProfiles; +Q_GLOBAL_STATIC(CamcorderProfiles, g_camcorderProfiles) + +static QString profileKey() +{ + return QStringLiteral("%1-%2"); +} + +bool AndroidCamcorderProfile::hasProfile(jint cameraId, Quality quality) +{ + if (g_camcorderProfiles->contains(profileKey().arg(cameraId).arg(quality))) + return true; + + return QJniObject::callStaticMethod<jboolean>("android/media/CamcorderProfile", + "hasProfile", + "(II)Z", + cameraId, + quality); +} + +AndroidCamcorderProfile AndroidCamcorderProfile::get(jint cameraId, Quality quality) +{ + const QString key = profileKey().arg(cameraId).arg(quality); + QMap<QString, QJniObject>::const_iterator it = g_camcorderProfiles->constFind(key); + + if (it != g_camcorderProfiles->constEnd()) + return AndroidCamcorderProfile(*it); + + QJniObject camProfile = QJniObject::callStaticObjectMethod("android/media/CamcorderProfile", + "get", + "(II)Landroid/media/CamcorderProfile;", + cameraId, + quality); + + return AndroidCamcorderProfile((*g_camcorderProfiles)[key] = camProfile); +} + +int AndroidCamcorderProfile::getValue(AndroidCamcorderProfile::Field field) const +{ + switch (field) { + case audioBitRate: + return m_camcorderProfile.getField<jint>("audioBitRate"); + case audioChannels: + return m_camcorderProfile.getField<jint>("audioChannels"); + case audioCodec: + return m_camcorderProfile.getField<jint>("audioCodec"); + case audioSampleRate: + return m_camcorderProfile.getField<jint>("audioSampleRate"); + case duration: + return m_camcorderProfile.getField<jint>("duration"); + case fileFormat: + return m_camcorderProfile.getField<jint>("fileFormat"); + case quality: + return m_camcorderProfile.getField<jint>("quality"); + case videoBitRate: + return m_camcorderProfile.getField<jint>("videoBitRate"); + case videoCodec: + return m_camcorderProfile.getField<jint>("videoCodec"); + case videoFrameHeight: + return m_camcorderProfile.getField<jint>("videoFrameHeight"); + case videoFrameRate: + return m_camcorderProfile.getField<jint>("videoFrameRate"); + case videoFrameWidth: + return m_camcorderProfile.getField<jint>("videoFrameWidth"); + } + + return 0; +} + +AndroidCamcorderProfile::AndroidCamcorderProfile(const QJniObject &camcorderProfile) +{ + m_camcorderProfile = camcorderProfile; +} + +static const char QtMediaRecorderListenerClassName[] = + "org/qtproject/qt/android/multimedia/QtMediaRecorderListener"; +typedef QMap<jlong, AndroidMediaRecorder*> MediaRecorderMap; +Q_GLOBAL_STATIC(MediaRecorderMap, mediaRecorders) + +static void notifyError(JNIEnv* , jobject, jlong id, jint what, jint extra) +{ + AndroidMediaRecorder *obj = mediaRecorders->value(id, 0); + if (obj) + emit obj->error(what, extra); +} + +static void notifyInfo(JNIEnv* , jobject, jlong id, jint what, jint extra) +{ + AndroidMediaRecorder *obj = mediaRecorders->value(id, 0); + if (obj) + emit obj->info(what, extra); +} + +AndroidMediaRecorder::AndroidMediaRecorder() + : QObject() + , m_id(reinterpret_cast<jlong>(this)) +{ + m_mediaRecorder = QJniObject("android/media/MediaRecorder"); + if (m_mediaRecorder.isValid()) { + QJniObject listener(QtMediaRecorderListenerClassName, "(J)V", m_id); + m_mediaRecorder.callMethod<void>("setOnErrorListener", + "(Landroid/media/MediaRecorder$OnErrorListener;)V", + listener.object()); + m_mediaRecorder.callMethod<void>("setOnInfoListener", + "(Landroid/media/MediaRecorder$OnInfoListener;)V", + listener.object()); + mediaRecorders->insert(m_id, this); + } +} + +AndroidMediaRecorder::~AndroidMediaRecorder() +{ + if (m_isVideoSourceSet || m_isAudioSourceSet) + reset(); + + release(); + mediaRecorders->remove(m_id); +} + +void AndroidMediaRecorder::release() +{ + m_mediaRecorder.callMethod<void>("release"); +} + +bool AndroidMediaRecorder::prepare() +{ + QJniEnvironment env; + auto methodId = env->GetMethodID(m_mediaRecorder.objectClass(), "prepare", "()V"); + env->CallVoidMethod(m_mediaRecorder.object(), methodId); + + if (env.checkAndClearExceptions()) + return false; + return true; +} + +void AndroidMediaRecorder::reset() +{ + m_mediaRecorder.callMethod<void>("reset"); + m_isAudioSourceSet = false; // Now setAudioSource can be used again. + m_isVideoSourceSet = false; +} + +bool AndroidMediaRecorder::start() +{ + QJniEnvironment env; + auto methodId = env->GetMethodID(m_mediaRecorder.objectClass(), "start", "()V"); + env->CallVoidMethod(m_mediaRecorder.object(), methodId); + + if (env.checkAndClearExceptions()) + return false; + return true; +} + +void AndroidMediaRecorder::stop() +{ + m_mediaRecorder.callMethod<void>("stop"); +} + +void AndroidMediaRecorder::setAudioChannels(int numChannels) +{ + m_mediaRecorder.callMethod<void>("setAudioChannels", "(I)V", numChannels); +} + +void AndroidMediaRecorder::setAudioEncoder(AudioEncoder encoder) +{ + QJniEnvironment env; + m_mediaRecorder.callMethod<void>("setAudioEncoder", "(I)V", int(encoder)); +} + +void AndroidMediaRecorder::setAudioEncodingBitRate(int bitRate) +{ + m_mediaRecorder.callMethod<void>("setAudioEncodingBitRate", "(I)V", bitRate); +} + +void AndroidMediaRecorder::setAudioSamplingRate(int samplingRate) +{ + m_mediaRecorder.callMethod<void>("setAudioSamplingRate", "(I)V", samplingRate); +} + +void AndroidMediaRecorder::setAudioSource(AudioSource source) +{ + if (!m_isAudioSourceSet) { + QJniEnvironment env; + auto methodId = env->GetMethodID(m_mediaRecorder.objectClass(), "setAudioSource", "(I)V"); + env->CallVoidMethod(m_mediaRecorder.object(), methodId, source); + if (!env.checkAndClearExceptions()) + m_isAudioSourceSet = true; + } else { + qCWarning(lcMediaRecorder) << "Audio source already set. Not setting a new source."; + } +} + +bool AndroidMediaRecorder::isAudioSourceSet() const +{ + return m_isAudioSourceSet; +} + +bool AndroidMediaRecorder::setAudioInput(const QByteArray &id) +{ + const bool ret = QJniObject::callStaticMethod<jboolean>( + "org/qtproject/qt/android/multimedia/QtAudioDeviceManager", + "setAudioInput", + "(Landroid/media/MediaRecorder;I)Z", + m_mediaRecorder.object(), + id.toInt()); + if (!ret) + qCWarning(lcMediaRecorder) << "No default input device was set."; + + return ret; +} + +void AndroidMediaRecorder::setCamera(AndroidCamera *camera) +{ + QJniObject cam = camera->getCameraObject(); + m_mediaRecorder.callMethod<void>("setCamera", "(Landroid/hardware/Camera;)V", cam.object()); +} + +void AndroidMediaRecorder::setVideoEncoder(VideoEncoder encoder) +{ + m_mediaRecorder.callMethod<void>("setVideoEncoder", "(I)V", int(encoder)); +} + +void AndroidMediaRecorder::setVideoEncodingBitRate(int bitRate) +{ + m_mediaRecorder.callMethod<void>("setVideoEncodingBitRate", "(I)V", bitRate); +} + +void AndroidMediaRecorder::setVideoFrameRate(int rate) +{ + m_mediaRecorder.callMethod<void>("setVideoFrameRate", "(I)V", rate); +} + +void AndroidMediaRecorder::setVideoSize(const QSize &size) +{ + m_mediaRecorder.callMethod<void>("setVideoSize", "(II)V", size.width(), size.height()); +} + +void AndroidMediaRecorder::setVideoSource(VideoSource source) +{ + QJniEnvironment env; + + auto methodId = env->GetMethodID(m_mediaRecorder.objectClass(), "setVideoSource", "(I)V"); + env->CallVoidMethod(m_mediaRecorder.object(), methodId, source); + + if (!env.checkAndClearExceptions()) + m_isVideoSourceSet = true; +} + +void AndroidMediaRecorder::setOrientationHint(int degrees) +{ + m_mediaRecorder.callMethod<void>("setOrientationHint", "(I)V", degrees); +} + +void AndroidMediaRecorder::setOutputFormat(OutputFormat format) +{ + QJniEnvironment env; + auto methodId = env->GetMethodID(m_mediaRecorder.objectClass(), "setOutputFormat", "(I)V"); + env->CallVoidMethod(m_mediaRecorder.object(), methodId, format); + // setAudioSource cannot be set after outputFormat is set. + if (!env.checkAndClearExceptions()) + m_isAudioSourceSet = true; +} + +void AndroidMediaRecorder::setOutputFile(const QString &path) +{ + if (QUrl(path).scheme() == QLatin1String("content")) { + const QJniObject fileDescriptor = QJniObject::callStaticObjectMethod( + "org/qtproject/qt/android/QtNative", + "openFdObjectForContentUrl", + "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Ljava/io/FileDescriptor;", + QNativeInterface::QAndroidApplication::context().object(), + QJniObject::fromString(path).object(), + QJniObject::fromString(QLatin1String("rw")).object()); + + m_mediaRecorder.callMethod<void>("setOutputFile", + "(Ljava/io/FileDescriptor;)V", + fileDescriptor.object()); + } else { + m_mediaRecorder.callMethod<void>("setOutputFile", + "(Ljava/lang/String;)V", + QJniObject::fromString(path).object()); + } +} + +void AndroidMediaRecorder::setSurfaceTexture(AndroidSurfaceTexture *texture) +{ + m_mediaRecorder.callMethod<void>("setPreviewDisplay", + "(Landroid/view/Surface;)V", + texture->surface()); +} + +void AndroidMediaRecorder::setSurfaceHolder(AndroidSurfaceHolder *holder) +{ + QJniObject surfaceHolder(holder->surfaceHolder()); + QJniObject surface = surfaceHolder.callObjectMethod("getSurface", + "()Landroid/view/Surface;"); + if (!surface.isValid()) + return; + + m_mediaRecorder.callMethod<void>("setPreviewDisplay", + "(Landroid/view/Surface;)V", + surface.object()); +} + +bool AndroidMediaRecorder::registerNativeMethods() +{ + static const JNINativeMethod methods[] = { + {"notifyError", "(JII)V", (void *)notifyError}, + {"notifyInfo", "(JII)V", (void *)notifyInfo} + }; + + const int size = std::size(methods); + return QJniEnvironment().registerNativeMethods(QtMediaRecorderListenerClassName, methods, size); +} + +QT_END_NAMESPACE + +#include "moc_androidmediarecorder_p.cpp" diff --git a/src/plugins/multimedia/android/wrappers/jni/androidmediarecorder_p.h b/src/plugins/multimedia/android/wrappers/jni/androidmediarecorder_p.h new file mode 100644 index 000000000..ffdbcc149 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidmediarecorder_p.h @@ -0,0 +1,161 @@ +// 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 + +#ifndef ANDROIDMEDIARECORDER_H +#define ANDROIDMEDIARECORDER_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 <QtCore/qjniobject.h> +#include <qsize.h> + +QT_BEGIN_NAMESPACE + +class AndroidCamera; +class AndroidSurfaceTexture; +class AndroidSurfaceHolder; + +class AndroidCamcorderProfile +{ +public: + enum Quality { // Needs to match CamcorderProfile + QUALITY_LOW, + QUALITY_HIGH, + QUALITY_QCIF, + QUALITY_CIF, + QUALITY_480P, + QUALITY_720P, + QUALITY_1080P, + QUALITY_QVGA + }; + + enum Field { + audioBitRate, + audioChannels, + audioCodec, + audioSampleRate, + duration, + fileFormat, + quality, + videoBitRate, + videoCodec, + videoFrameHeight, + videoFrameRate, + videoFrameWidth + }; + + static bool hasProfile(jint cameraId, Quality quality); + static AndroidCamcorderProfile get(jint cameraId, Quality quality); + int getValue(Field field) const; + +private: + AndroidCamcorderProfile(const QJniObject &camcorderProfile); + QJniObject m_camcorderProfile; +}; + +class AndroidMediaRecorder : public QObject +{ + Q_OBJECT +public: + enum AudioEncoder { + DefaultAudioEncoder = 0, + AMR_NB_Encoder = 1, + AMR_WB_Encoder = 2, + AAC = 3, + OPUS = 7, + VORBIS = 6 + }; + + enum AudioSource { + DefaultAudioSource = 0, + Mic = 1, + VoiceUplink = 2, + VoiceDownlink = 3, + VoiceCall = 4, + Camcorder = 5, + VoiceRecognition = 6 + }; + + enum VideoEncoder { + DefaultVideoEncoder = 0, + H263 = 1, + H264 = 2, + MPEG_4_SP = 3, + HEVC = 5 + }; + + enum VideoSource { + DefaultVideoSource = 0, + Camera = 1 + }; + + enum OutputFormat { + DefaultOutputFormat = 0, + THREE_GPP = 1, + MPEG_4 = 2, + AMR_NB_Format = 3, + AMR_WB_Format = 4, + AAC_ADTS = 6, + OGG = 11, + WEBM = 9 + }; + + AndroidMediaRecorder(); + ~AndroidMediaRecorder(); + + void release(); + bool prepare(); + void reset(); + + bool start(); + void stop(); + + void setAudioChannels(int numChannels); + void setAudioEncoder(AudioEncoder encoder); + void setAudioEncodingBitRate(int bitRate); + void setAudioSamplingRate(int samplingRate); + void setAudioSource(AudioSource source); + bool isAudioSourceSet() const; + bool setAudioInput(const QByteArray &id); + + void setCamera(AndroidCamera *camera); + void setVideoEncoder(VideoEncoder encoder); + void setVideoEncodingBitRate(int bitRate); + void setVideoFrameRate(int rate); + void setVideoSize(const QSize &size); + void setVideoSource(VideoSource source); + + void setOrientationHint(int degrees); + + void setOutputFormat(OutputFormat format); + void setOutputFile(const QString &path); + + void setSurfaceTexture(AndroidSurfaceTexture *texture); + void setSurfaceHolder(AndroidSurfaceHolder *holder); + + static bool registerNativeMethods(); + +Q_SIGNALS: + void error(int what, int extra); + void info(int what, int extra); + +private: + jlong m_id; + QJniObject m_mediaRecorder; + bool m_isAudioSourceSet = false; + bool m_isVideoSourceSet = false; +}; + +QT_END_NAMESPACE + +#endif // ANDROIDMEDIARECORDER_H diff --git a/src/plugins/multimedia/android/wrappers/jni/androidmultimediautils.cpp b/src/plugins/multimedia/android/wrappers/jni/androidmultimediautils.cpp new file mode 100644 index 000000000..9606bd6bb --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidmultimediautils.cpp @@ -0,0 +1,43 @@ +// 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 "androidmultimediautils_p.h" + +#include <QtCore/qjniobject.h> + +QT_BEGIN_NAMESPACE + + +void AndroidMultimediaUtils::enableOrientationListener(bool enable) +{ + QJniObject::callStaticMethod<void>("org/qtproject/qt/android/multimedia/QtMultimediaUtils", + "enableOrientationListener", + "(Z)V", + enable); +} + +int AndroidMultimediaUtils::getDeviceOrientation() +{ + return QJniObject::callStaticMethod<jint>("org/qtproject/qt/android/multimedia/QtMultimediaUtils", + "getDeviceOrientation"); +} + +QString AndroidMultimediaUtils::getDefaultMediaDirectory(MediaType type) +{ + QJniObject path = QJniObject::callStaticObjectMethod( + "org/qtproject/qt/android/multimedia/QtMultimediaUtils", + "getDefaultMediaDirectory", + "(I)Ljava/lang/String;", + jint(type)); + return path.toString(); +} + +void AndroidMultimediaUtils::registerMediaFile(const QString &file) +{ + QJniObject::callStaticMethod<void>("org/qtproject/qt/android/multimedia/QtMultimediaUtils", + "registerMediaFile", + "(Ljava/lang/String;)V", + QJniObject::fromString(file).object()); +} + +QT_END_NAMESPACE diff --git a/src/plugins/multimedia/android/wrappers/jni/androidmultimediautils_p.h b/src/plugins/multimedia/android/wrappers/jni/androidmultimediautils_p.h new file mode 100644 index 000000000..ee72c3c61 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidmultimediautils_p.h @@ -0,0 +1,40 @@ +// 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 + +#ifndef ANDROIDMULTIMEDIAUTILS_H +#define ANDROIDMULTIMEDIAUTILS_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> + +QT_BEGIN_NAMESPACE + +class AndroidMultimediaUtils +{ +public: + enum MediaType { + Music = 0, + Movies = 1, + DCIM = 2, + Sounds = 3 + }; + + static void enableOrientationListener(bool enable); + static int getDeviceOrientation(); + static QString getDefaultMediaDirectory(MediaType type); + static void registerMediaFile(const QString &file); +}; + +QT_END_NAMESPACE + +#endif // ANDROIDMULTIMEDIAUTILS_H diff --git a/src/plugins/multimedia/android/wrappers/jni/androidsurfacetexture.cpp b/src/plugins/multimedia/android/wrappers/jni/androidsurfacetexture.cpp new file mode 100644 index 000000000..c5860b265 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidsurfacetexture.cpp @@ -0,0 +1,152 @@ +// 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 "androidsurfacetexture_p.h" +#include <QtCore/qmutex.h> +#include <QtCore/qcoreapplication.h> + +QT_BEGIN_NAMESPACE + +static const char QtSurfaceTextureListenerClassName[] = "org/qtproject/qt/android/multimedia/QtSurfaceTextureListener"; +typedef QList<jlong> SurfaceTextures; +Q_GLOBAL_STATIC(SurfaceTextures, g_surfaceTextures); +Q_GLOBAL_STATIC(QMutex, g_textureMutex); + +static QAtomicInteger<quint64> indexCounter = 0u; + +// native method for QtSurfaceTexture.java +static void notifyFrameAvailable(JNIEnv* , jobject, jlong id) +{ + const QMutexLocker lock(g_textureMutex()); + const int idx = g_surfaceTextures->indexOf(id); + if (idx == -1) + return; + + AndroidSurfaceTexture *obj = reinterpret_cast<AndroidSurfaceTexture *>(g_surfaceTextures->at(idx)); + if (obj) + Q_EMIT obj->frameAvailable(); +} + +AndroidSurfaceTexture::AndroidSurfaceTexture(quint32 texName) + : QObject() + , m_index(indexCounter.fetchAndAddRelaxed(1)) +{ + Q_STATIC_ASSERT(sizeof (jlong) >= sizeof (void *)); + m_surfaceTexture = QJniObject("android/graphics/SurfaceTexture", "(I)V", jint(texName)); + + if (!m_surfaceTexture.isValid()) + return; + + const QMutexLocker lock(g_textureMutex()); + g_surfaceTextures->append(jlong(this)); + QJniObject listener(QtSurfaceTextureListenerClassName, "(J)V", jlong(this)); + setOnFrameAvailableListener(listener); +} + +AndroidSurfaceTexture::~AndroidSurfaceTexture() +{ + if (m_surface.isValid()) + m_surface.callMethod<void>("release"); + + if (m_surfaceTexture.isValid()) { + release(); + const QMutexLocker lock(g_textureMutex()); + const int idx = g_surfaceTextures->indexOf(jlong(this)); + if (idx != -1) + g_surfaceTextures->remove(idx); + } +} + +QMatrix4x4 AndroidSurfaceTexture::getTransformMatrix() +{ + QMatrix4x4 matrix; + if (!m_surfaceTexture.isValid()) + return matrix; + + QJniEnvironment env; + jfloatArray array = env->NewFloatArray(16); + m_surfaceTexture.callMethod<void>("getTransformMatrix", "([F)V", array); + env->GetFloatArrayRegion(array, 0, 16, matrix.data()); + env->DeleteLocalRef(array); + + return matrix; +} + +void AndroidSurfaceTexture::release() +{ + m_surfaceTexture.callMethod<void>("release"); +} + +void AndroidSurfaceTexture::updateTexImage() +{ + if (!m_surfaceTexture.isValid()) + return; + + m_surfaceTexture.callMethod<void>("updateTexImage"); +} + +jobject AndroidSurfaceTexture::surfaceTexture() +{ + return m_surfaceTexture.object(); +} + +jobject AndroidSurfaceTexture::surface() +{ + if (!m_surface.isValid()) { + m_surface = QJniObject("android/view/Surface", + "(Landroid/graphics/SurfaceTexture;)V", + m_surfaceTexture.object()); + } + + return m_surface.object(); +} + +jobject AndroidSurfaceTexture::surfaceHolder() +{ + if (!m_surfaceHolder.isValid()) { + m_surfaceHolder = QJniObject("org/qtproject/qt/android/multimedia/QtSurfaceTextureHolder", + "(Landroid/view/Surface;)V", + surface()); + } + + return m_surfaceHolder.object(); +} + +void AndroidSurfaceTexture::attachToGLContext(quint32 texName) +{ + if (!m_surfaceTexture.isValid()) + return; + + m_surfaceTexture.callMethod<void>("attachToGLContext", "(I)V", texName); +} + +void AndroidSurfaceTexture::detachFromGLContext() +{ + if (!m_surfaceTexture.isValid()) + return; + + m_surfaceTexture.callMethod<void>("detachFromGLContext"); +} + +bool AndroidSurfaceTexture::registerNativeMethods() +{ + static const JNINativeMethod methods[] = { + {"notifyFrameAvailable", "(J)V", (void *)notifyFrameAvailable} + }; + const int size = std::size(methods); + if (QJniEnvironment().registerNativeMethods(QtSurfaceTextureListenerClassName, methods, size)) + return false; + + return true; +} + +void AndroidSurfaceTexture::setOnFrameAvailableListener(const QJniObject &listener) +{ + m_surfaceTexture.callMethod<void>("setOnFrameAvailableListener", + "(Landroid/graphics/SurfaceTexture$OnFrameAvailableListener;)V", + listener.object()); +} + +QT_END_NAMESPACE + +#include "moc_androidsurfacetexture_p.cpp" diff --git a/src/plugins/multimedia/android/wrappers/jni/androidsurfacetexture_p.h b/src/plugins/multimedia/android/wrappers/jni/androidsurfacetexture_p.h new file mode 100644 index 000000000..24581ca8d --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidsurfacetexture_p.h @@ -0,0 +1,61 @@ +// 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 + +#ifndef ANDROIDSURFACETEXTURE_H +#define ANDROIDSURFACETEXTURE_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 <QtCore/qjniobject.h> + +#include <QMatrix4x4> + +QT_BEGIN_NAMESPACE + +class AndroidSurfaceTexture : public QObject +{ + Q_OBJECT +public: + explicit AndroidSurfaceTexture(quint32 texName); + ~AndroidSurfaceTexture(); + + jobject surfaceTexture(); + jobject surface(); + jobject surfaceHolder(); + inline bool isValid() const { return m_surfaceTexture.isValid(); } + + QMatrix4x4 getTransformMatrix(); + void release(); // API level 14 + void updateTexImage(); + + void attachToGLContext(quint32 texName); // API level 16 + void detachFromGLContext(); // API level 16 + + static bool registerNativeMethods(); + + quint64 index() const { return m_index; } +Q_SIGNALS: + void frameAvailable(); + +private: + void setOnFrameAvailableListener(const QJniObject &listener); + + QJniObject m_surfaceTexture; + QJniObject m_surface; + QJniObject m_surfaceHolder; + const quint64 m_index = 0; +}; + +QT_END_NAMESPACE + +#endif // ANDROIDSURFACETEXTURE_H diff --git a/src/plugins/multimedia/android/wrappers/jni/androidsurfaceview.cpp b/src/plugins/multimedia/android/wrappers/jni/androidsurfaceview.cpp new file mode 100644 index 000000000..dae9516c3 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidsurfaceview.cpp @@ -0,0 +1,152 @@ +// 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 "androidsurfaceview_p.h" + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qdebug.h> +#include <QtCore/qlist.h> +#include <QtCore/qmutex.h> +#include <QtGui/qwindow.h> + +QT_BEGIN_NAMESPACE + +static const char QtSurfaceHolderCallbackClassName[] = "org/qtproject/qt/android/multimedia/QtSurfaceHolderCallback"; +typedef QList<AndroidSurfaceHolder *> SurfaceHolders; +Q_GLOBAL_STATIC(SurfaceHolders, surfaceHolders) +Q_GLOBAL_STATIC(QMutex, shLock) + +AndroidSurfaceHolder::AndroidSurfaceHolder(QJniObject object) + : m_surfaceHolder(object) + , m_surfaceCreated(false) +{ + if (!m_surfaceHolder.isValid()) + return; + + { + QMutexLocker locker(shLock()); + surfaceHolders->append(this); + } + + QJniObject callback(QtSurfaceHolderCallbackClassName, "(J)V", reinterpret_cast<jlong>(this)); + m_surfaceHolder.callMethod<void>("addCallback", + "(Landroid/view/SurfaceHolder$Callback;)V", + callback.object()); +} + +AndroidSurfaceHolder::~AndroidSurfaceHolder() +{ + QMutexLocker locker(shLock()); + const int i = surfaceHolders->indexOf(this); + if (Q_UNLIKELY(i == -1)) + return; + + surfaceHolders->remove(i); +} + +jobject AndroidSurfaceHolder::surfaceHolder() const +{ + return m_surfaceHolder.object(); +} + +bool AndroidSurfaceHolder::isSurfaceCreated() const +{ + QMutexLocker locker(shLock()); + return m_surfaceCreated; +} + +void AndroidSurfaceHolder::handleSurfaceCreated(JNIEnv*, jobject, jlong id) +{ + QMutexLocker locker(shLock()); + const int i = surfaceHolders->indexOf(reinterpret_cast<AndroidSurfaceHolder *>(id)); + if (Q_UNLIKELY(i == -1)) + return; + + (*surfaceHolders)[i]->m_surfaceCreated = true; + Q_EMIT (*surfaceHolders)[i]->surfaceCreated(); +} + +void AndroidSurfaceHolder::handleSurfaceDestroyed(JNIEnv*, jobject, jlong id) +{ + QMutexLocker locker(shLock()); + const int i = surfaceHolders->indexOf(reinterpret_cast<AndroidSurfaceHolder *>(id)); + if (Q_UNLIKELY(i == -1)) + return; + + (*surfaceHolders)[i]->m_surfaceCreated = false; +} + +bool AndroidSurfaceHolder::registerNativeMethods() +{ + static const JNINativeMethod methods[] = { + {"notifySurfaceCreated", "(J)V", (void *)AndroidSurfaceHolder::handleSurfaceCreated}, + {"notifySurfaceDestroyed", "(J)V", (void *)AndroidSurfaceHolder::handleSurfaceDestroyed} + }; + + const int size = std::size(methods); + return QJniEnvironment().registerNativeMethods(QtSurfaceHolderCallbackClassName, methods, size); +} + +AndroidSurfaceView::AndroidSurfaceView() + : m_window(0) + , m_surfaceHolder(0) + , m_pendingVisible(-1) +{ + QNativeInterface::QAndroidApplication::runOnAndroidMainThread([this] { + m_surfaceView = QJniObject("android/view/SurfaceView", + "(Landroid/content/Context;)V", + QNativeInterface::QAndroidApplication::context().object()); + }).waitForFinished(); + + Q_ASSERT(m_surfaceView.isValid()); + + QJniObject holder = m_surfaceView.callObjectMethod("getHolder", + "()Landroid/view/SurfaceHolder;"); + if (!holder.isValid()) { + m_surfaceView = QJniObject(); + } else { + m_surfaceHolder = new AndroidSurfaceHolder(holder); + connect(m_surfaceHolder, &AndroidSurfaceHolder::surfaceCreated, + this, &AndroidSurfaceView::surfaceCreated); + { // Lock now to avoid a race with handleSurfaceCreated() + QMutexLocker locker(shLock()); + m_window = QWindow::fromWinId(WId(m_surfaceView.object())); + + if (m_pendingVisible != -1) + m_window->setVisible(m_pendingVisible); + if (m_pendingGeometry.isValid()) + m_window->setGeometry(m_pendingGeometry); + } + } +} + +AndroidSurfaceView::~AndroidSurfaceView() +{ + delete m_surfaceHolder; + delete m_window; +} + +AndroidSurfaceHolder *AndroidSurfaceView::holder() const +{ + return m_surfaceHolder; +} + +void AndroidSurfaceView::setVisible(bool v) +{ + if (m_window) + m_window->setVisible(v); + else + m_pendingVisible = int(v); +} + +void AndroidSurfaceView::setGeometry(int x, int y, int width, int height) +{ + if (m_window) + m_window->setGeometry(x, y, width, height); + else + m_pendingGeometry = QRect(x, y, width, height); +} + +QT_END_NAMESPACE + +#include "moc_androidsurfaceview_p.cpp" diff --git a/src/plugins/multimedia/android/wrappers/jni/androidsurfaceview_p.h b/src/plugins/multimedia/android/wrappers/jni/androidsurfaceview_p.h new file mode 100644 index 000000000..e6be60ef1 --- /dev/null +++ b/src/plugins/multimedia/android/wrappers/jni/androidsurfaceview_p.h @@ -0,0 +1,78 @@ +// 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 + +#ifndef ANDROIDSURFACEVIEW_H +#define ANDROIDSURFACEVIEW_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 <QtCore/qjniobject.h> +#include <qrect.h> +#include <QtCore/qrunnable.h> +#include <QtCore/qobject.h> + +QT_BEGIN_NAMESPACE + +class QWindow; + +class AndroidSurfaceHolder : public QObject +{ + Q_OBJECT +public: + ~AndroidSurfaceHolder(); + + jobject surfaceHolder() const; + bool isSurfaceCreated() const; + + static bool registerNativeMethods(); + +Q_SIGNALS: + void surfaceCreated(); + +private: + AndroidSurfaceHolder(QJniObject object); + + static void handleSurfaceCreated(JNIEnv*, jobject, jlong id); + static void handleSurfaceDestroyed(JNIEnv*, jobject, jlong id); + + QJniObject m_surfaceHolder; + bool m_surfaceCreated; + + friend class AndroidSurfaceView; +}; + +class AndroidSurfaceView : public QObject +{ + Q_OBJECT +public: + AndroidSurfaceView(); + ~AndroidSurfaceView(); + + AndroidSurfaceHolder *holder() const; + + void setVisible(bool v); + void setGeometry(int x, int y, int width, int height); + +Q_SIGNALS: + void surfaceCreated(); + +private: + QJniObject m_surfaceView; + QWindow *m_window; + AndroidSurfaceHolder *m_surfaceHolder; + int m_pendingVisible; + QRect m_pendingGeometry; +}; + +QT_END_NAMESPACE + +#endif // ANDROIDSURFACEVIEW_H |