Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qandroidcamera.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include <QtFFmpegMediaPluginImpl/private/qandroidcamera_p.h>
5
6#include <jni.h>
7
8#include <limits>
9#include <optional>
10
11#include <QtCore/qdebug.h>
12#include <QtCore/qloggingcategory.h>
13#include <QtCore/qmetaobject.h>
14#include <QtCore/qpermissions.h>
15#include <QtCore/private/qandroidextras_p.h>
16
17#include <QtGui/qguiapplication.h>
18#include <QtGui/qscreen.h>
19
20#include <QtFFmpegMediaPluginImpl/private/qandroidvideoframefactory_p.h>
21#include <QtFFmpegMediaPluginImpl/private/qffmpegvideobuffer_p.h>
22
23#include <QtMultimedia/private/qcameradevice_p.h>
24#include <QtMultimedia/private/qmultimediautils_p.h>
25
26extern "C" {
27#include "libavutil/hwcontext.h"
28}
29
31Q_STATIC_LOGGING_CATEGORY(qLCAndroidCamera, "qt.multimedia.ffmpeg.androidCamera");
32
33namespace {
34
35// TODO: Unify format selection with cross-platform layer
36QCameraFormat getDefaultCameraFormat(const QCameraDevice & cameraDevice)
37{
38 // default settings
39 const auto defaultFrameFormat = QVideoFrameFormat::Format_YUV420P;
40 const auto defaultResolution = QSize(1920, 1080);
41 QCameraFormatPrivate *defaultFormat = new QCameraFormatPrivate{
42 .pixelFormat = defaultFrameFormat,
43 .resolution = defaultResolution,
44 .minFrameRate = 12,
45 .maxFrameRate = 30,
46 };
47
48 QCameraFormat resultFormat = defaultFormat->create();
49 const auto &supportedFormats = cameraDevice.videoFormats();
50
51 if (supportedFormats.empty() || supportedFormats.contains(resultFormat))
52 return resultFormat;
53
54 auto pixelCount = [](const QSize& resolution) {
55 Q_ASSERT(resolution.isValid());
56 return resolution.width() * resolution.height();
57 };
58
59 const int defaultPixelCount = pixelCount(defaultResolution);
60
61 // The lower the score, the better the format suits
62 int differenceScore = std::numeric_limits<int>::max();
63
64 auto calcDifferenceScore = [defaultPixelCount, pixelCount](const QCameraFormat& format) {
65 const int pixelDifference = pixelCount(format.resolution()) - defaultPixelCount;
66 // prefer:
67 // 1. 'pixels count >= default' over 'pixels count < default'
68 // 2. lower abs(pixelDifference)
69 return pixelDifference < 0
70 ? -pixelDifference
71 : std::numeric_limits<int>::min() + pixelDifference;
72 };
73
74 for (const auto &supportedFormat : supportedFormats) {
75 if (supportedFormat.pixelFormat() == defaultFrameFormat) {
76 if (supportedFormat.resolution() == defaultResolution)
77 return supportedFormat;
78
79 const int currentDifferenceScore = calcDifferenceScore(supportedFormat);
80
81 if (currentDifferenceScore < differenceScore) {
82 differenceScore = currentDifferenceScore;
83 resultFormat = supportedFormat;
84 }
85 }
86 }
87
88 return resultFormat;
89}
90
91bool checkCameraPermission()
92{
93 QCameraPermission permission;
94
95 const bool granted = qApp->checkPermission(permission) == Qt::PermissionStatus::Granted;
96 if (!granted)
97 qCWarning(qLCAndroidCamera) << "Access to camera not granted!";
98
99 return granted;
100}
101
102int sensorOrientation(QString cameraId)
103{
104 QJniObject deviceManager(QtJniTypes::Traits<QtJniTypes::QtVideoDeviceManager>::className(),
105 QNativeInterface::QAndroidApplication::context());
106
107 if (!deviceManager.isValid()) {
108 qCWarning(qLCAndroidCamera) << "Failed to connect to Qt Video Device Manager.";
109 return 0;
110 }
111
112 return deviceManager.callMethod<jint>("getSensorOrientation",
113 QJniObject::fromString(cameraId).object<jstring>());
114}
115
116// Returns the FocusModes that are available on the physical device, for which we also have
117// an implementation.
118[[nodiscard]] static QList<QCamera::FocusMode> qGetSupportedFocusModesFromAndroidCamera(
119 const QJniObject &deviceManager,
120 const QCameraDevice &cameraDevice)
121{
122 QList<QCamera::FocusMode> returnValue;
123
124 const QStringList focusModeStrings = deviceManager.callMethod<QStringList>(
125 "getSupportedQCameraFocusModesAsStrings",
126 QJniObject::fromString(QString::fromUtf8(cameraDevice.id())).object<jstring>());
127
128 // Translate the strings into enums if possible.
129 for (const QString &focusModeString : focusModeStrings) {
130 bool ok = false;
131 const auto focusMode = static_cast<QCamera::FocusMode>(
132 QMetaEnum::fromType<QCamera::FocusMode>()
133 .keyToValue(
134 focusModeString.toLatin1().data(),
135 &ok));
136 if (ok)
137 returnValue.push_back(focusMode);
138 else
139 qCDebug(qLCAndroidCamera) <<
140 "received a QCamera::FocusMode string from Android "
141 "QtVideoDeviceManager.java that was not recognized.";
142 }
143
144 return returnValue;
145}
146
147} // namespace
148
149namespace QFFmpeg {
150
151// QAndroidCamera
152
173
175{
176 // Flush queued events / frames and clear the reference back to this
177 // this instance. Blocking call.
178 m_jniCamera.callMethod<void>("destroy");
180}
181
183{
184 const bool oldActive = isActive();
185 if (oldActive)
186 setActive(false);
187
188 // Reset all our control-members on the Java-side to default
189 // values. Then populate them again during updateCameraCharacteristics()
190 m_jniCamera.callMethod<void>("resetControlProperties");
191
195
196 if (oldActive)
197 setActive(true);
198}
199
201{
202 // TODO: m_androidFramePixelFormat is continuously being written to by
203 // the Java-side capture-processing background thread when receiving frame, while this
204 // function is commonly called by the media recording engine on other threads.
205 // A potential solution might include a mutex-lock and/or determining
206 // the pixelFormat ahead of time by checking what format we request
207 // when starting the Android camera capture session.
209}
210
212{
214 // Apply rotation for surface only
216 return result;
217}
218
219// Called by the Java-side processing background thread.
221{
223 || m_frameFactory == nullptr) {
224 qCWarning(qLCAndroidCamera) << "Received frame when not active... ignoring";
225 qCWarning(qLCAndroidCamera) << "state:" << m_state;
226 image.callMethod<void>("close");
227 return;
228 }
229
231 if (!videoFrame.isValid())
232 return;
233
234 // TODO: m_androidFramePixelFormat is written by the Java-side processing
235 // background thread, but read by the QCamera thread during QAndroid::ffmpegHWPixelFormat().
236 // This causes a race condition (not severe). We should eventually implement some
237 // synchronization strategy.
238 m_androidFramePixelFormat = videoFrame.pixelFormat(); // NOTE: Should this be updated on stills?
242 // Apply a still resolution changed during startup
245 }
246
248
249 if (!takePhoto)
251 else
253}
254
256{
257 // If the camera is not attached to the main display, we don't want to apply rotation
258 // based on primary screen. We assume we are an external camera.
262 return QtVideo::Rotation::None;
263
268
269 // Display rotation is the opposite direction of the physical device rotation. We need the
270 // device rotation, that's why Landscape is 270 and InvertedLandscape is 90
271 int deviceOrientation = 0;
272 switch (screenOrientation) {
275 break;
277 deviceOrientation = 270;
278 break;
280 deviceOrientation = 180;
281 break;
284 break;
285 }
286
287 const int sign = (cameraPosition == QCameraDevice::Position::FrontFace) ? 1 : -1;
289 - deviceOrientation * sign + 360) % 360;
290
291 return QtVideo::Rotation(rotation);
292}
293
295{
297 if (isActive() == active)
298 return;
299
300 if (!m_jniCamera.isValid()) {
301 updateError(QCamera::CameraError, QStringLiteral("No connection to Android Camera2 API"));
302 return;
303 }
304
305 if (!active || !checkCameraPermission()) {
306 m_jniCamera.callMethod<void>("stopAndClose");
307 m_jniCamera.callMethod<void>("clearSurfaces");
309 return;
310 }
311
315
316 if (width < 0 || height < 0) {
320 }
321
322 m_videoResolution = { FFALIGN(width, 16), FFALIGN(height, 16) };
324
326
327 // Create frameFactory when ImageReader is created;
329
330 // Most supported format combination for ImageReaders in a session is YUV_420_888 for
331 // preview/video, and JPEG for stills
332 // TODO: Set session combinations based on device capabilities:
333 // https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture
334 m_jniCamera.callMethod<void>("prepareCamera", static_cast<jint>(m_videoResolution.width()),
335 static_cast<jint>(m_videoResolution.height()),
336 static_cast<jint>(m_cameraFormat.minFrameRate()),
337 static_cast<jint>(m_cameraFormat.maxFrameRate()),
338 static_cast<jint>(m_stillReaderSize.width()),
339 static_cast<jint>(m_stillReaderSize.height()));
340
342
343 if (!canOpen) {
346 QString(u"Failed to start camera: ").append(m_cameraDevice.description()));
347 }
348}
349
350// TODO: setState is currently being used by the C++ thread owning
351// the QCamera object, and the Java-side capture-processing background thread.
352// This can lead to race conditions and the m_state ending up in inconsistent states.
353// We should have a synchronization strategy in the future.
355{
356 if (newState == m_state)
357 return;
358
359 bool wasActive = isActive();
360
361 if (newState == State::Started)
363
364 if (m_state == State::Started && newState == State::Closed)
366
368 && newState == State::Closed) {
369
371
373 QString(u"Failed to start Camera %1").arg(m_cameraDevice.description()));
374 }
375
378
381
382 if (wasActive != isActive())
384}
385
387{
389
391 return true;
393 return false;
394
396
398
399 return true;
400}
401
403{
404 if (m_cameraDevice.id().isEmpty()) {
406 return;
407 }
408
411
412 if (!deviceManager.isValid()) {
413 qCWarning(qLCAndroidCamera) << "Failed to connect to Qt Video Device Manager.";
415 return;
416 }
417
418
419 // Gather capabilities.
421
422 float newMaxZoom = 1.f;
423 float newMinZoom = 1.f;
425 "getZoomRange",
427 if (newZoomRange.isValid() && newZoomRange.size() == 2) {
430 } else {
432 "received invalid float array when querying zoomRange from Android Camera2. "
433 "Likely Qt developer bug";
434 }
435
439 "getSupportedFlashModes",
441 for (const auto &flashMode : flashModes) {
442 if (flashMode == QLatin1String("auto"))
444 else if (flashMode == QLatin1String("on"))
446 }
447
449 "isTorchModeSupported",
451
455
458
459
460 // Signal capability changes
464
465
466 // Apply properties
467 if (minZoomFactor() < maxZoomFactor()) {
468 // New device supports zooming. Clamp it and apply it to new camera device.
471 }
472
475
478
481
482
483 // Reset properties if needed.
484 if (minZoomFactor() >= maxZoomFactor())
486
489
492
495}
496
497// Should only be called when the camera device is set to null.
499{
501 if (zoomFactor() != 1.0) {
502 zoomTo(1.0, -1.0);
503 }
504 if (torchMode() != QCamera::TorchOff) {
506 }
507 m_TorchModeSupported = false;
508
509 if (flashMode() != QCamera::FlashOff) {
511 }
514
515
516 // Reset focus mode.
520
522}
523
525{
527 return m_videoResolution; // Not set, will use video resolution
528
530
531 // Use as-is with empty list, might fail
534
536}
537
538// Restart camera now, must be called from owning thread
540{
541 if (!isActive())
542 return;
543
544 setActive(false);
545 setActive(true);
546}
547
548// Queue a camera restart, can be called from a background Java thread
550{
551 if (m_restartPending.exchange(true))
552 return;
553
554 // NOTE: Queued on event loop to avoid re-entering onVideoSourceChanged
556 this,
557 [this]() {
558 m_restartPending = false;
559 restartCamera(); // No-op if inactive
560 },
562}
563
565{
567 return;
568
571 Q_FUNC_INFO <<
572 "attmpted to set focus-distance on camera without support for FocusDistance feature";
573 return;
574 }
575
576 if (distance < 0 || distance > 1) {
578 Q_FUNC_INFO <<
579 "attempted to set camera focus-distance with out-of-bounds value";
580 return;
581 }
582
583 // focusDistance should only be applied if the focusMode is currently set to Manual.
584 // The Java function will handle this behavior. Either way, the value should be accepted so
585 // we can apply it if FocusModeManual is activated
586 m_jniCamera.callMethod<void>("setFocusDistance", distance);
587
589}
590
592{
593 if (focusMode() == mode)
594 return;
595
598 Q_FUNC_INFO <<
599 QLatin1String("attempted to set focus-mode '%1' on camera where it is unsupported.")
601 return;
602 }
603
604 // If the new focus-mode is Manual, then the focusDistance
605 // needs to be applied as well. The Java function handles this.
607 "setFocusMode",
608 static_cast<jint>(mode));
609
611}
612
614{
616 return;
617
619 switch (mode) {
620 case QCamera::FlashAuto:
621 flashMode = QLatin1String("auto");
622 break;
623 case QCamera::FlashOn:
624 flashMode = QLatin1String("on");
625 break;
626 case QCamera::FlashOff:
627 default:
628 flashMode = QLatin1String("off");
629 break;
630 }
631
634}
635
640
642{
643 // Android doesn't have an API for that.
644 // Only check if device supports more flash modes than just FlashOff.
645 return m_supportedFlashModes.size() > 1;
646}
647
652
654{
655 if (mode == QCamera::TorchOff)
656 return true;
657 else if (mode == QCamera::TorchOn)
659
660 return false;
661}
662
664{
665 bool torchMode;
666 if (mode == QCamera::TorchOff) {
667 torchMode = false;
668 } else if (mode == QCamera::TorchOn) {
669 torchMode = true;
670 } else {
671 qWarning() << "Unknown Torch mode";
672 return;
673 }
674 m_jniCamera.callMethod<void>("setTorchMode", jboolean(torchMode));
676}
677
678void QAndroidCamera::zoomTo(float factor, float rate)
679{
680 Q_UNUSED(rate);
681
682 if (!m_cameraDevice.id().isEmpty()) {
683 m_jniCamera.callMethod<void>("zoomTo", factor);
684 }
686}
687
689{
692 if (isActive()) {
693 setActive(false);
694 m_wasActive = true;
695 }
696 break;
697 case Qt::ApplicationActive:
698 if (m_wasActive) {
699 setActive(true);
700 m_wasActive = false;
701 }
702 break;
703 default:
704 break;
705 }
706}
707
708// Called by Java-side processing background thread.
714
715// Called by Java-side processing background thread.
720
721// Called by Java-side processing background thread.
722// The session rebuilt for a new still reader size failed. Fall back to a full restart.
727
728// Called by Java-side processing background thread.
734
735// Called by Java-side processing background thread.
740
741// Called by Java-side processing background thread.
743{
745 QString(u"Capture error with Camera %1. Camera2 Api error code: %2")
747 .arg(reason));
748}
749
750// Called by Java-side processing background thread.
755
756// Called by Java-side processing background thread.
762
764{
765 m_jniCamera.callMethod<void>("beginStillPhotoCapture");
766}
767
769{
770 m_jniCamera.callMethod<void>("saveExifToFile", QJniObject::fromString(filename).object<jstring>());
771}
772
774{
776
779 qCWarning(qLCAndroidCamera) << "Image capture resolution set to" << resolution
780 << "while list of valid photo resolutions is empty. Image "
781 "capture can fail if resolution is not supported by device.";
782 }
785}
786
788{
790
791 const QSize newSize = stillResolution();
792 // Not started: the next start uses the new size or re-runs this, see frameAvailable()
794 return;
795
796 if (m_jniCamera.callMethod<jboolean>("updateStillImageReader", newSize.width(),
797 newSize.height())) {
799 return;
800 }
801
803}
804
806{
808
810 QStringLiteral("Capture session failure with Camera %1. Camera2 Api error code: %2")
812 .arg(reason));
813}
814
815// Called by Java background thread if the on-going still-photo capture fails.
817{
818 // TODO: Emit a more descriptive error signal. At the time of writing, there is no
819 // suitable QImageCapture::Error enumerator for this scenario.
822 QStringLiteral("Unknown error"));
823}
824
825} // namespace QFFmpeg
826
827// JNI logic
828// The following static functions can only be called by the Java-side processing background
829// thread. They all assume that we are currently holding the mutex protecting the
830// QAndroidCamera reference on the Java-side.
831
832namespace {
833
834[[nodiscard]] QFFmpeg::QAndroidCamera& jLongToQAndroidCamera(jlong input)
835{
836 Q_ASSERT(input != 0);
837 return *reinterpret_cast<QFFmpeg::QAndroidCamera *>(static_cast<uintptr_t>(input));
838}
839
840} // Anonymous namespace
841
842static void onPreviewFrameAvailable(JNIEnv *, jobject, jlong qAndroidCameraPtr,
843 QtJniTypes::Image image)
844{
845 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
846 camera.frameAvailable(QJniObject(image));
847}
848Q_DECLARE_JNI_NATIVE_METHOD(onPreviewFrameAvailable)
849
850static void onStillPhotoAvailable(JNIEnv *, jobject, jlong qAndroidCameraPtr,
851 QtJniTypes::Image image)
852{
853 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
854 camera.frameAvailable(QJniObject(image), true);
855}
856Q_DECLARE_JNI_NATIVE_METHOD(onStillPhotoAvailable)
857
858static void onCameraOpened(JNIEnv *, jobject, jlong qAndroidCameraPtr)
859{
860 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
861 camera.onCameraOpened();
862}
863Q_DECLARE_JNI_NATIVE_METHOD(onCameraOpened)
864
865static void onCameraDisconnect(JNIEnv *, jobject, jlong qAndroidCameraPtr)
866{
867 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
868 camera.onCameraDisconnect();
869}
870Q_DECLARE_JNI_NATIVE_METHOD(onCameraDisconnect)
871
872static void onCameraError(JNIEnv *, jobject, jlong qAndroidCameraPtr, jint error)
873{
874 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
875 camera.onCameraError(error);
876}
877Q_DECLARE_JNI_NATIVE_METHOD(onCameraError)
878
879static void onCaptureSessionConfigured(JNIEnv *, jobject, jlong qAndroidCameraPtr)
880{
881 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
882 camera.onCaptureSessionConfigured();
883}
884Q_DECLARE_JNI_NATIVE_METHOD(onCaptureSessionConfigured)
885
886static void onCaptureSessionConfigureFailed(JNIEnv *, jobject, jlong qAndroidCameraPtr)
887{
888 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
889 camera.onCaptureSessionConfigureFailed();
890}
891Q_DECLARE_JNI_NATIVE_METHOD(onCaptureSessionConfigureFailed)
892
893static void onStillReaderSwapFailed(JNIEnv *, jobject, jlong qAndroidCameraPtr)
894{
895 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
896 camera.onStillReaderSwapFailed();
897}
898Q_DECLARE_JNI_NATIVE_METHOD(onStillReaderSwapFailed)
899
900static void onSessionActive(JNIEnv *, jobject, jlong qAndroidCameraPtr)
901{
902 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
903 camera.onSessionActive();
904}
905Q_DECLARE_JNI_NATIVE_METHOD(onSessionActive)
906
907static void onSessionClosed(JNIEnv *, jobject, jlong qAndroidCameraPtr)
908{
909 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
910 camera.onSessionClosed();
911}
912Q_DECLARE_JNI_NATIVE_METHOD(onSessionClosed)
913
914static void onCaptureSessionFailed(JNIEnv *, jobject, jlong qAndroidCameraPtr, jint reason,
915 jlong framenumber)
916{
917 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
918 camera.onCaptureSessionFailed(reason, framenumber);
919}
920Q_DECLARE_JNI_NATIVE_METHOD(onCaptureSessionFailed)
921
922static void onStillPhotoCaptureFailed(JNIEnv *, jobject, jlong qAndroidCameraPtr)
923{
924 QFFmpeg::QAndroidCamera &camera = jLongToQAndroidCamera(qAndroidCameraPtr);
925 camera.onStillPhotoCaptureFailed();
926}
927Q_DECLARE_JNI_NATIVE_METHOD(onStillPhotoCaptureFailed)
928
929bool QFFmpeg::QAndroidCamera::registerNativeMethods()
930{
931 static const bool registered = []() {
932 return QtJniCamera2::registerNativeMethods({
933 Q_JNI_NATIVE_METHOD(onCameraOpened),
934 Q_JNI_NATIVE_METHOD(onCameraDisconnect),
935 Q_JNI_NATIVE_METHOD(onCameraError),
936 Q_JNI_NATIVE_METHOD(onCaptureSessionConfigured),
937 Q_JNI_NATIVE_METHOD(onCaptureSessionConfigureFailed),
938 Q_JNI_NATIVE_METHOD(onStillReaderSwapFailed),
939 Q_JNI_NATIVE_METHOD(onCaptureSessionFailed),
940 Q_JNI_NATIVE_METHOD(onPreviewFrameAvailable),
941 Q_JNI_NATIVE_METHOD(onStillPhotoAvailable),
942 Q_JNI_NATIVE_METHOD(onSessionActive),
943 Q_JNI_NATIVE_METHOD(onSessionClosed),
944 Q_JNI_NATIVE_METHOD(onStillPhotoCaptureFailed),
945 });
946 }();
947 return registered;
948}
949
950QT_END_NAMESPACE
static void onCaptureSessionFailed(JNIEnv *, jobject, jlong qAndroidCameraPtr, jint reason, jlong framenumber)
static void onCameraOpened(JNIEnv *, jobject, jlong qAndroidCameraPtr)
static void onPreviewFrameAvailable(JNIEnv *, jobject, jlong qAndroidCameraPtr, QtJniTypes::Image image)
static void onStillReaderSwapFailed(JNIEnv *, jobject, jlong qAndroidCameraPtr)
static void onCaptureSessionConfigured(JNIEnv *, jobject, jlong qAndroidCameraPtr)
static void onCaptureSessionConfigureFailed(JNIEnv *, jobject, jlong qAndroidCameraPtr)
static void onStillPhotoCaptureFailed(JNIEnv *, jobject, jlong qAndroidCameraPtr)
static void onStillPhotoAvailable(JNIEnv *, jobject, jlong qAndroidCameraPtr, QtJniTypes::Image image)
static void onSessionClosed(JNIEnv *, jobject, jlong qAndroidCameraPtr)
static void onSessionActive(JNIEnv *, jobject, jlong qAndroidCameraPtr)
static void onCameraError(JNIEnv *, jobject, jlong qAndroidCameraPtr, jint error)
static void onCameraDisconnect(JNIEnv *, jobject, jlong qAndroidCameraPtr)
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)