summaryrefslogtreecommitdiffstats
path: root/src/plugins/multimedia/android/wrappers
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/multimedia/android/wrappers')
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidcamera.cpp1797
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidcamera_p.h208
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidmediametadataretriever.cpp136
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidmediametadataretriever_p.h66
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidmediaplayer.cpp535
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidmediaplayer_p.h135
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidmediarecorder.cpp337
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidmediarecorder_p.h161
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidmultimediautils.cpp43
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidmultimediautils_p.h40
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidsurfacetexture.cpp152
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidsurfacetexture_p.h61
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidsurfaceview.cpp152
-rw-r--r--src/plugins/multimedia/android/wrappers/jni/androidsurfaceview_p.h78
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 &parameters);
+
+ Q_INVOKABLE QList<AndroidCamera::FpsRange> getSupportedPreviewFpsRange();
+
+ Q_INVOKABLE AndroidCamera::FpsRange getPreviewFpsRange();
+ static AndroidCamera::FpsRange getPreviewFpsRange(QJniObject &parameters);
+ 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 &parameters);
+
+ 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 &parameters)
+{
+ 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 &parameters)
+{
+ 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 &parameters)
+{
+ 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