Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtMultimediaTestLib/private/mediabackendutils_p.h>
#include <QtMultimediaTestLib/private/qintegrationtestbase_p.h>
#include <QtTest/qsignalspy.h>
#include <QtTest/qtest.h>
#include <QtMultimedia/qaudiooutput.h>
#include <QtMultimedia/qcamera.h>
#include <QtMultimedia/qcameradevice.h>
#include <QtMultimedia/qimagecapture.h>
#include <QtMultimedia/qmediacapturesession.h>
#include <QtMultimedia/qmediadevices.h>
#include <QtMultimedia/qmediaplayer.h>
#include <QtMultimedia/qmediarecorder.h>
#include <QtMultimedia/qvideosink.h>
#include <QtMultimedia/private/qplatformcamera_p.h>
#include <QtMultimedia/private/qplatformimagecapture_p.h>
#include <QtMultimedia/private/qplatformmediaintegration_p.h>
#include <QtGui/qimagereader.h>
#include <QtCore/qlocale.h>
#include <QtCore/qobject.h>
#include <QtCore/qscopeguard.h>
#include <QtCore/qurl.h>
#if QT_CONFIG(process)
# include <QtCore/qprocess.h>
#endif
#ifdef Q_OS_DARWIN
# include <QtCore/private/qcore_mac_p.h>
#endif
#include <algorithm>
#include <chrono>
#include <memory>
using namespace std::chrono_literals;
/*
This is the backend conformance test.
Since it relies on platform media framework and sound hardware
it may be less stable.
*/
struct VCamParameters
{
QString name;
QString format = QStringLiteral("nv12");
QSize resolution = QSize(1920, 1080);
float fps = 60.0f;
};
// In CI, the VCamManager.exe can hang for a long time even if it's
// available. It hangs so long that it triggers the test watchdog
// of 300s that a test is timed out and considered a crash.
// We add some strict timeouts to work around.
// TODO: Need to solve hangs and flakiness in VCamManager.exe.
static constexpr std::chrono::milliseconds VCamProcessTimeout = 15s;
static constexpr std::chrono::milliseconds VCamDetectTimeout = 5s;
using namespace QtMultimediaTest;
class tst_QCameraBackend : public QIntegrationTestBase
{
Q_OBJECT
public slots:
void initTestCase();
void cleanupTestCase();
private slots:
void testCameraDevice();
void testCtorWithCameraDevice();
void testCtorWithPosition();
void testVirtualCameraAddition();
void testVirtualCameraRemoval();
void testVirtualCameraFrameChanging();
void testCameraActive();
void testCameraStartParallel();
void testMappedFrameSurvivesCameraStop();
void testCameraFormat();
void testCameraCapture();
void testCaptureToBuffer();
void testCaptureResolutionChangeWhileActive();
void testCaptureResolutionChangeRapid();
void testCaptureResolutionSetBeforeStart();
void captureToFile_createsFileWithExpectedExtension_data();
void captureToFile_createsFileWithExpectedExtension();
void testCameraCaptureMetadata();
void testExposureCompensation();
void testExposureMode();
void testVideoRecording_data();
void testVideoRecording();
void testNativeMetadata();
void multipleCameraSet();
private:
// Returns up to \a count distinct photo resolutions supported by the default camera
static QList<QSize> photoResolutionsToTest(int count);
// Saved images can be rotated according to device orientation
static bool isSameSizeAllowingRotation(const QSize &actual, const QSize &expected);
// Captures to file, returns the saved image size or an empty size on failure
static QSize captureToFileAndGetSize(QImageCapture &imageCapture);
bool callVcam(const QString &command, const VCamParameters ¶meters) const;
bool addVcam(const VCamParameters ¶meters) const;
void removeVcam(const VCamParameters ¶meters) const;
static QCameraDevice findCamera(const QString &name);
QColor dominantRgbColor(const QColor &color, int tolerance) const;
bool noCamera = false;
bool m_vcamAvailable = false;
VCamParameters m_defaultCamera {"VCam"};
QByteArray m_vcamPath;
};
class TestVideoFormat : public QVideoSink
{
Q_OBJECT
public:
explicit TestVideoFormat(const QCameraFormat &format)
: formatMismatch(0),
cameraFormat(format)
{
connect(this, &QVideoSink::videoFrameChanged, this, &TestVideoFormat::checkVideoFrameFormat);
}
void setCameraFormatToTest(const QCameraFormat &format)
{
formatMismatch = -1;
cameraFormat = format;
}
int formatMismatch = -1;
private:
QCameraFormat cameraFormat;
public Q_SLOTS:
void checkVideoFrameFormat(const QVideoFrame &frame)
{
QVideoFrameFormat surfaceFormat = frame.surfaceFormat();
if (surfaceFormat.pixelFormat() == cameraFormat.pixelFormat()
&& surfaceFormat.frameSize() == cameraFormat.resolution()) {
formatMismatch = 0;
#ifdef Q_OS_ANDROID
} else if ((surfaceFormat.pixelFormat() == QVideoFrameFormat::Format_YUV420P
|| surfaceFormat.pixelFormat() == QVideoFrameFormat::Format_NV12
|| surfaceFormat.pixelFormat() == QVideoFrameFormat::Format_NV21)
&& cameraFormat.pixelFormat() == QVideoFrameFormat::Format_YUV420P
&& surfaceFormat.frameSize() == cameraFormat.resolution()) {
formatMismatch = 0;
#endif
#ifdef Q_OS_HARMONY
} else if (surfaceFormat.pixelFormat() == QVideoFrameFormat::Format_RGBA8888
&& (cameraFormat.pixelFormat() == QVideoFrameFormat::Format_NV12
|| cameraFormat.pixelFormat() == QVideoFrameFormat::Format_NV21)
&& surfaceFormat.frameSize() == cameraFormat.resolution()) {
// OHOS samples NV12 native buffers via GL_TEXTURE_EXTERNAL_OES which
// delivers RGBA8888 frames to QVideoSink regardless of the native
// camera format.
formatMismatch = 0;
#endif
} else {
formatMismatch = 1;
}
}
};
void tst_QCameraBackend::initTestCase()
{
if (!initIntegrationTestCase())
return;
#if QT_CONFIG(process)
m_vcamPath = qgetenv("VCAM_PATH");
#endif
// VCAM_PATH being set only means the helper is deployed, not that it can
// actually register a camera on this machine. Test it once and skip
// other tests based on this.
if (!m_vcamPath.isEmpty())
m_vcamAvailable = addVcam(m_defaultCamera);
QCamera camera;
noCamera = !camera.isAvailable();
}
void tst_QCameraBackend::cleanupTestCase()
{
if (m_vcamAvailable)
removeVcam(m_defaultCamera);
}
bool tst_QCameraBackend::callVcam(const QString &command, const VCamParameters ¶meters) const
{
#if QT_CONFIG(process)
QProcess vcamManagerProcess;
QString program = m_vcamPath + "\\VCamManager.exe";
vcamManagerProcess.setWorkingDirectory(m_vcamPath);
QStringList arguments;
arguments << command << parameters.name;
if (command == "--add") {
arguments << "--format" << parameters.format;
arguments << "--resolution" << QString::number(parameters.resolution.width()) + "x"
+ QString::number(parameters.resolution.height());
arguments << "--fps" << QString::number(parameters.fps);
}
vcamManagerProcess.start(program, arguments);
if (!vcamManagerProcess.waitForStarted(static_cast<int>(VCamProcessTimeout.count()))) {
qWarning() << "Failed to start" << program << arguments << ":"
<< vcamManagerProcess.errorString();
return false;
}
if (!vcamManagerProcess.waitForFinished(static_cast<int>(VCamProcessTimeout.count()))) {
qWarning() << program << arguments << "did not finish within"
<< VCamProcessTimeout.count() << "ms; terminating it";
vcamManagerProcess.kill();
vcamManagerProcess.waitForFinished(static_cast<int>(VCamProcessTimeout.count()));
return false;
}
if (vcamManagerProcess.exitStatus() != QProcess::NormalExit
|| vcamManagerProcess.exitCode() != 0) {
qWarning() << program << arguments << "failed with exit code"
<< vcamManagerProcess.exitCode();
return false;
}
return true;
#else
Q_UNUSED(command);
Q_UNUSED(parameters);
return false;
#endif
}
bool tst_QCameraBackend::addVcam(const VCamParameters ¶meters) const
{
if (!callVcam("--add", parameters))
return false;
// The device-added notification is delivered asynchronously, so wait a
// bounded time for the camera to actually show up.
return QTest::qWaitFor(
[&] {
return !findCamera(parameters.name).isNull();
},
VCamDetectTimeout);
}
void tst_QCameraBackend::removeVcam(const VCamParameters ¶meters) const
{
QMediaDevices mediaDevices = QMediaDevices();
QSignalSpy removeSignal(&mediaDevices, &QMediaDevices::videoInputsChanged);
callVcam("--remove", parameters);
QTRY_COMPARE(removeSignal.size(), 1);
}
QCameraDevice tst_QCameraBackend::findCamera(const QString &name)
{
const QList<QCameraDevice> cameras = QMediaDevices::videoInputs();
for (const QCameraDevice &camera : cameras) {
if (camera.description().startsWith(name)) {
return camera;
}
}
return QCameraDevice();
}
QColor tst_QCameraBackend::dominantRgbColor(const QColor &color, int tolerance) const
{
if (color.red() > 255 - tolerance && color.green() < tolerance && color.blue() < tolerance)
return Qt::red;
if (color.green() > 255 - tolerance && color.red() < tolerance && color.blue() < tolerance)
return Qt::green;
if (color.blue() > 255 - tolerance && color.red() < tolerance && color.green() < tolerance)
return Qt::blue;
return QColor();
}
void tst_QCameraBackend::testCameraDevice()
{
const QList<QCameraDevice> cameras = QMediaDevices::videoInputs();
if (cameras.isEmpty()) {
QVERIFY(noCamera);
QVERIFY(QMediaDevices::defaultVideoInput().isNull());
QSKIP("Camera selection is not supported");
}
QVERIFY(!noCamera);
for (const QCameraDevice &info : cameras) {
QVERIFY(!info.id().isEmpty());
QVERIFY(!info.description().isEmpty());
}
}
void tst_QCameraBackend::testCtorWithCameraDevice()
{
if (noCamera) {
// only verify that we get an error trying to create a camera
QCamera camera;
QCOMPARE(camera.error(), QCamera::CameraError);
QVERIFY(camera.cameraDevice().isNull());
QSKIP("No camera available");
}
QCameraDevice defaultCamera = QMediaDevices::defaultVideoInput();
{
// should use default camera
QCamera camera;
QCOMPARE(camera.error(), QCamera::NoError);
QVERIFY(!camera.cameraDevice().isNull());
QCOMPARE(camera.cameraDevice(), defaultCamera);
}
{
// should use default camera
QCamera camera(QCameraDevice{});
QCOMPARE(camera.error(), QCamera::NoError);
QVERIFY(!camera.cameraDevice().isNull());
QCOMPARE(camera.cameraDevice(), defaultCamera);
}
{
QCamera camera(defaultCamera);
QCOMPARE(camera.error(), QCamera::NoError);
QCOMPARE(camera.cameraDevice(), defaultCamera);
}
{
QCameraDevice info = QMediaDevices::videoInputs().first();
QCamera camera(info);
QCOMPARE(camera.error(), QCamera::NoError);
QCOMPARE(camera.cameraDevice(), info);
}
}
void tst_QCameraBackend::testCtorWithPosition()
{
if (noCamera)
QSKIP("No camera available");
{
QCamera camera(QCameraDevice::UnspecifiedPosition);
QCOMPARE(camera.error(), QCamera::NoError);
}
{
QCamera camera(QCameraDevice::FrontFace);
// even if no camera is available at this position, it should not fail
// and load the default camera
QCOMPARE(camera.error(), QCamera::NoError);
}
{
QCamera camera(QCameraDevice::BackFace);
// even if no camera is available at this position, it should not fail
// and load the default camera
QCOMPARE(camera.error(), QCamera::NoError);
}
}
void tst_QCameraBackend::testVirtualCameraAddition()
{
if (!m_vcamAvailable)
QSKIP("Virtual camera is not available. Skipping camera addition test.");
int lengthBeforeAdd = QMediaDevices::videoInputs().length();
VCamParameters cameraParams {"TestVCam", QStringLiteral("nv12"), QSize(1280, 720), 33.0f};
QScopeGuard cleanup([&] { removeVcam(cameraParams); });
QMediaDevices mediaDevices = QMediaDevices();
QSignalSpy changeSignal(&mediaDevices, &QMediaDevices::videoInputsChanged);
QVERIFY(callVcam("--add", cameraParams));
QTRY_COMPARE(changeSignal.size(), 1);
int lengthAfterAdd = QMediaDevices::videoInputs().length();
QCOMPARE(lengthAfterAdd, lengthBeforeAdd + 1);
QCameraDevice addedCamera = findCamera(cameraParams.name);
QVERIFY(!addedCamera.isNull());
QVERIFY(!addedCamera.videoFormats().isEmpty());
const QCameraFormat format = addedCamera.videoFormats().first();
QCOMPARE(format.pixelFormat(), QVideoFrameFormat::Format_NV12);
QCOMPARE(format.resolution(), cameraParams.resolution);
QCOMPARE(format.maxFrameRate(), cameraParams.fps);
QCOMPARE(format.minFrameRate(), 1.0f);
}
void tst_QCameraBackend::testVirtualCameraRemoval()
{
if (!m_vcamAvailable)
QSKIP("Virtual camera is not available. Skipping camera removal test.");
QMediaDevices mediaDevices = QMediaDevices();
QSignalSpy changeSignal(&mediaDevices, &QMediaDevices::videoInputsChanged);
int lengthBeforeAdd = QMediaDevices::videoInputs().length();
VCamParameters cameraParams {"TestVCamToRemove"};
QVERIFY(callVcam("--add", cameraParams));
QTRY_COMPARE(changeSignal.size(), 1);
removeVcam(cameraParams);
int lengthAfterRemove = QMediaDevices::videoInputs().length();
QCOMPARE(lengthAfterRemove, lengthBeforeAdd);
QVERIFY(findCamera(cameraParams.name).isNull());
}
void tst_QCameraBackend::testVirtualCameraFrameChanging()
{
if (!m_vcamAvailable)
QSKIP("Virtual camera is not available. Skipping camera frame test.");
QMediaCaptureSession session;
QCamera camera;
QVideoSink sink;
session.setCamera(&camera);
camera.setCameraDevice(findCamera(m_defaultCamera.name));
session.setVideoOutput(&sink);
std::vector<QColor> colors;
connect(&sink, &QVideoSink::videoFrameChanged, this, [&colors](const QVideoFrame &frame){
QVERIFY(frame.isValid());
QImage image = frame.toImage();
QColor pcolor = image.pixelColor(1,1);
colors.push_back(pcolor);
});
camera.start();
QTRY_VERIFY(colors.size() >= 3);
camera.stop();
int tolerance = 5;
for (int i = 0; i < 3; ++i) {
colors[i] = dominantRgbColor(colors[i], tolerance);
if (!colors[i].isValid())
QFAIL("Captured frame color is not pure red, green or blue");
}
QList<QColor> rgbColors = {Qt::red, Qt::green, Qt::blue};
QCOMPARE((rgbColors.indexOf(colors[0]) + 1) % 3, rgbColors.indexOf(colors[1]));
QCOMPARE((rgbColors.indexOf(colors[1]) + 1) % 3, rgbColors.indexOf(colors[2]));
QCOMPARE((rgbColors.indexOf(colors[2]) + 1) % 3, rgbColors.indexOf(colors[0]));
}
void tst_QCameraBackend::testCameraActive()
{
QMediaCaptureSession session;
QCamera camera;
camera.setCameraDevice(QCameraDevice());
QImageCapture imageCapture;
session.setCamera(&camera);
session.setImageCapture(&imageCapture);
QSignalSpy errorSignal(&camera, &QCamera::errorOccurred);
QSignalSpy activeChangedSignal(&camera, &QCamera::activeChanged);
QCOMPARE(camera.isActive(), false);
if (noCamera)
QSKIP("No camera available");
camera.setCameraDevice(QMediaDevices::defaultVideoInput());
QCOMPARE(camera.error(), QCamera::NoError);
camera.start();
QTRY_COMPARE(camera.isActive(), true);
QTRY_COMPARE(activeChangedSignal.size(), 1);
QCOMPARE(activeChangedSignal.last().first().value<bool>(), true);
camera.stop();
QCOMPARE(camera.isActive(), false);
QCOMPARE(activeChangedSignal.last().first().value<bool>(), false);
QCOMPARE(camera.errorString(), QString());
}
void tst_QCameraBackend::testCameraStartParallel()
{
#ifdef Q_OS_ANDROID
QSKIP("Multi-camera feature is currently not supported on Android. "
"Cannot open same device twice.");
#endif
#ifdef Q_OS_LINUX
QSKIP("Multi-camera feature is currently not supported on Linux. "
"Cannot open same device twice.");
#endif
if (noCamera)
QSKIP("No camera available");
QMediaCaptureSession session1;
QMediaCaptureSession session2;
QCamera camera1(QMediaDevices::defaultVideoInput());
QCamera camera2(QMediaDevices::defaultVideoInput());
session1.setCamera(&camera1);
session2.setCamera(&camera2);
QSignalSpy errorSpy1(&camera1, &QCamera::errorOccurred);
QSignalSpy errorSpy2(&camera2, &QCamera::errorOccurred);
camera1.start();
camera2.start();
QCOMPARE(camera1.isActive(), true);
QCOMPARE(camera1.error(), QCamera::NoError);
QCOMPARE(camera2.isActive(), true);
QCOMPARE(camera2.error(), QCamera::NoError);
QCOMPARE(errorSpy1.size(), 0);
QCOMPARE(errorSpy2.size(), 0);
}
void tst_QCameraBackend::testMappedFrameSurvivesCameraStop()
{
if (noCamera)
QSKIP("No camera available");
QMediaCaptureSession session;
QCamera camera;
QVideoSink sink;
session.setCamera(&camera);
session.setVideoOutput(&sink);
QSignalSpy frameSpy(&sink, &QVideoSink::videoFrameChanged);
QVERIFY(frameSpy.isValid());
camera.start();
QTRY_VERIFY_WITH_TIMEOUT(!frameSpy.isEmpty(), 10s);
QVideoFrame frame = frameSpy.first().first().value<QVideoFrame>();
QVERIFY(frame.map(QVideoFrame::ReadOnly));
const QByteArray before(reinterpret_cast<const char *>(frame.bits(0)), frame.mappedBytes(0));
camera.stop();
// The mapping was taken before the stop, so it must still read the same bytes afterwards.
const QByteArray after(reinterpret_cast<const char *>(frame.bits(0)), frame.mappedBytes(0));
frame.unmap();
QCOMPARE(after, before);
}
void tst_QCameraBackend::testCameraFormat()
{
QCamera camera;
QCameraDevice device = camera.cameraDevice();
auto videoFormats = device.videoFormats();
if (videoFormats.isEmpty())
QSKIP("No Camera available, skipping test.");
QCameraFormat cameraFormat = videoFormats.first();
QSignalSpy spy(&camera, &QCamera::cameraFormatChanged);
QVERIFY(spy.size() == 0);
QMediaCaptureSession session;
session.setCamera(&camera);
QVERIFY(videoFormats.size());
camera.setCameraFormat(cameraFormat);
QCOMPARE(camera.cameraFormat(), cameraFormat);
QVERIFY(spy.size() == 1);
TestVideoFormat videoFormatTester(cameraFormat);
session.setVideoOutput(&videoFormatTester);
camera.start();
QTRY_VERIFY(videoFormatTester.formatMismatch == 0);
spy.clear();
camera.stop();
// Change camera format
// OHOS samples NV12 native buffers via GL_TEXTURE_EXTERNAL_OES which
// always delivers RGBA8888 frames to the QVideoSink. A camera-format
// change that swaps native pixel format but keeps the same resolution is
// therefore invisible to TestVideoFormat, so the "mismatch == 1" check
// does not hold. Skip the multi-format sub-block on OHOS.
if (videoFormats.size() > 1 && !isOhosPlatform()) {
QCameraFormat secondFormat = videoFormats.at(1);
camera.setCameraFormat(secondFormat);
QCOMPARE(camera.cameraFormat(), secondFormat);
QCOMPARE(spy.size(), 1);
QCOMPARE(camera.cameraFormat(), secondFormat);
videoFormatTester.setCameraFormatToTest(secondFormat);
camera.start();
QTRY_VERIFY(videoFormatTester.formatMismatch == 0);
// check that frame format is not same as previous camera format
videoFormatTester.setCameraFormatToTest(cameraFormat);
QTRY_VERIFY(videoFormatTester.formatMismatch == 1);
}
// Set null format
spy.clear();
camera.stop();
camera.setCameraFormat({});
QCOMPARE(spy.size(), 1);
videoFormatTester.setCameraFormatToTest({});
camera.start();
if (isOhosPlatform()) {
// OH_CameraInput_Open does not always recover frames after a stop()
// + setCameraFormat({}) + start() cycle on the device side. The camera
// restarts but the preview surface stays silent until the session is
// recreated.
return;
}
// In case of a null format, the backend should have picked
// a decent format to render frames
QTRY_VERIFY(videoFormatTester.formatMismatch == 1);
camera.stop();
spy.clear();
// Shouldn't change anything as it's the same device
camera.setCameraDevice(device);
QCOMPARE(spy.size(), 0);
}
void tst_QCameraBackend::testCameraCapture()
{
QMediaCaptureSession session;
QCamera camera;
QImageCapture imageCapture;
session.setCamera(&camera);
session.setImageCapture(&imageCapture);
//prevents camera to flash during the test
camera.setFlashMode(QCamera::FlashOff);
QVERIFY(!imageCapture.isReadyForCapture());
QSignalSpy capturedSignal(&imageCapture, &QImageCapture::imageCaptured);
QSignalSpy savedSignal(&imageCapture, &QImageCapture::imageSaved);
QSignalSpy errorSignal(&imageCapture, &QImageCapture::errorOccurred);
imageCapture.captureToFile();
QTRY_COMPARE(errorSignal.size(), 1);
QCOMPARE(imageCapture.error(), QImageCapture::NotReadyError);
QCOMPARE(capturedSignal.size(), 0);
errorSignal.clear();
if (noCamera)
QSKIP("No camera available");
QVideoSink sink;
session.setVideoOutput(&sink);
camera.start();
QTRY_VERIFY(imageCapture.isReadyForCapture());
QVERIFY(camera.isActive());
QCOMPARE(errorSignal.size(), 0);
int id = imageCapture.captureToFile();
QTRY_VERIFY_WITH_TIMEOUT(!savedSignal.isEmpty(), 8s);
QTRY_COMPARE(capturedSignal.size(), 1);
QCOMPARE(capturedSignal.last().first().toInt(), id);
QCOMPARE(errorSignal.size(), 0);
QCOMPARE(imageCapture.error(), QImageCapture::NoError);
QCOMPARE(savedSignal.last().first().toInt(), id);
QString location = savedSignal.last().last().toString();
QVERIFY(!location.isEmpty());
QVERIFY(QFileInfo(location).exists());
QImageReader reader(location);
reader.setScaledSize(QSize(320,240));
QVERIFY(!reader.read().isNull());
QFile(location).remove();
}
void tst_QCameraBackend::testCaptureToBuffer()
{
if (noCamera)
QSKIP("No camera available");
QMediaCaptureSession session;
QCamera camera;
QImageCapture imageCapture;
session.setCamera(&camera);
session.setImageCapture(&imageCapture);
camera.setFlashMode(QCamera::FlashOff);
camera.setActive(true);
QTRY_VERIFY(camera.isActive());
QSignalSpy capturedSignal(&imageCapture, &QImageCapture::imageCaptured);
QSignalSpy imageAvailableSignal(&imageCapture, &QImageCapture::imageAvailable);
QSignalSpy savedSignal(&imageCapture, &QImageCapture::imageSaved);
QSignalSpy errorSignal(&imageCapture, &QImageCapture::errorOccurred);
camera.start();
QTRY_VERIFY(imageCapture.isReadyForCapture());
int id = imageCapture.capture();
QTRY_VERIFY(!imageAvailableSignal.isEmpty());
QVERIFY(errorSignal.isEmpty());
QTRY_VERIFY(!capturedSignal.isEmpty());
QVERIFY(!imageAvailableSignal.isEmpty());
QTest::qWait(2000);
QVERIFY(savedSignal.isEmpty());
QCOMPARE(capturedSignal.first().first().toInt(), id);
QCOMPARE(imageAvailableSignal.first().first().toInt(), id);
QVideoFrame frame = imageAvailableSignal.first().last().value<QVideoFrame>();
QVERIFY(!frame.toImage().isNull());
frame = QVideoFrame();
capturedSignal.clear();
imageAvailableSignal.clear();
savedSignal.clear();
QTRY_VERIFY(imageCapture.isReadyForCapture());
}
QList<QSize> tst_QCameraBackend::photoResolutionsToTest(int count)
{
QList<QSize> sizes = QMediaDevices::defaultVideoInput().photoResolutions();
// Smallest first, so every step in the sequence is a change
std::sort(sizes.begin(), sizes.end(), [](const QSize &a, const QSize &b) {
return a.width() * a.height() < b.width() * b.height();
});
return sizes.mid(0, count);
}
bool tst_QCameraBackend::isSameSizeAllowingRotation(const QSize &actual, const QSize &expected)
{
return actual == expected || actual == expected.transposed();
}
QSize tst_QCameraBackend::captureToFileAndGetSize(QImageCapture &imageCapture)
{
QSignalSpy savedSignal(&imageCapture, &QImageCapture::imageSaved);
QSignalSpy errorSignal(&imageCapture, &QImageCapture::errorOccurred);
if (!QTest::qWaitFor([&] { return imageCapture.isReadyForCapture(); }, 8s))
return {};
imageCapture.captureToFile();
if (!QTest::qWaitFor([&] { return !savedSignal.isEmpty() || !errorSignal.isEmpty(); }, 8s)
|| savedSignal.isEmpty())
return {};
const QString location = savedSignal.last().last().toString();
const QSize size = QImageReader(location).size();
QFile::remove(location);
return size;
}
void tst_QCameraBackend::testCaptureResolutionChangeWhileActive()
{
if (noCamera)
QSKIP("No camera available");
const QList<QSize> sizes = photoResolutionsToTest(3);
if (sizes.size() < 2)
QSKIP("Camera reports fewer than two photo resolutions");
QMediaCaptureSession session;
QCamera camera;
QImageCapture imageCapture;
session.setCamera(&camera);
session.setImageCapture(&imageCapture);
camera.setFlashMode(QCamera::FlashOff);
QVideoSink sink;
session.setVideoOutput(&sink);
camera.start();
QTRY_VERIFY(camera.isActive());
QTRY_VERIFY(imageCapture.isReadyForCapture());
QSignalSpy activeChanged(&camera, &QCamera::activeChanged);
QSignalSpy errorSignal(&camera, &QCamera::errorOccurred);
// Go through the sizes twice so both growing and shrinking are covered
for (const QSize &size : sizes + sizes) {
imageCapture.setResolution(size);
QCOMPARE(imageCapture.resolution(), size);
QTRY_VERIFY_WITH_TIMEOUT(camera.isActive(), 8s);
QTRY_VERIFY_WITH_TIMEOUT(imageCapture.isReadyForCapture(), 8s);
const QSize captured = captureToFileAndGetSize(imageCapture);
QVERIFY2(!captured.isEmpty(), "Capture failed after resolution change");
#ifdef Q_OS_ANDROID
QVERIFY(isSameSizeAllowingRotation(captured, size));
#endif
}
#ifdef Q_OS_ANDROID
// The session is rebuilt in place, the camera is not restarted
QCOMPARE(activeChanged.size(), 0);
#endif
QCOMPARE(errorSignal.size(), 0);
QVERIFY(camera.isActive());
}
void tst_QCameraBackend::testCaptureResolutionChangeRapid()
{
if (noCamera)
QSKIP("No camera available");
const QList<QSize> sizes = photoResolutionsToTest(3);
if (sizes.size() < 2)
QSKIP("Camera reports fewer than two photo resolutions");
QMediaCaptureSession session;
QCamera camera;
QImageCapture imageCapture;
session.setCamera(&camera);
session.setImageCapture(&imageCapture);
camera.setFlashMode(QCamera::FlashOff);
QVideoSink sink;
session.setVideoOutput(&sink);
camera.start();
QTRY_VERIFY(camera.isActive());
QTRY_VERIFY(imageCapture.isReadyForCapture());
QSignalSpy errorSignal(&camera, &QCamera::errorOccurred);
// Changes arrive before earlier ones have been applied
for (int i = 0; i < 12; ++i)
imageCapture.setResolution(sizes.at(i % sizes.size()));
[[maybe_unused]] const QSize last = imageCapture.resolution();
QTRY_VERIFY_WITH_TIMEOUT(camera.isActive(), 8s);
const QSize captured = captureToFileAndGetSize(imageCapture);
QVERIFY2(!captured.isEmpty(), "Capture failed after rapid resolution changes");
#ifdef Q_OS_ANDROID
QVERIFY(isSameSizeAllowingRotation(captured, last));
#endif
QCOMPARE(errorSignal.size(), 0);
}
void tst_QCameraBackend::testCaptureResolutionSetBeforeStart()
{
if (noCamera)
QSKIP("No camera available");
const QList<QSize> sizes = photoResolutionsToTest(2);
if (sizes.isEmpty())
QSKIP("Camera reports no photo resolutions");
QMediaCaptureSession session;
QCamera camera;
QImageCapture imageCapture;
session.setCamera(&camera);
session.setImageCapture(&imageCapture);
camera.setFlashMode(QCamera::FlashOff);
imageCapture.setResolution(sizes.first());
QVideoSink sink;
session.setVideoOutput(&sink);
camera.start();
QTRY_VERIFY(camera.isActive());
const QSize captured = captureToFileAndGetSize(imageCapture);
QVERIFY2(!captured.isEmpty(), "Capture failed");
#ifdef Q_OS_ANDROID
QVERIFY(isSameSizeAllowingRotation(captured, sizes.first()));
#endif
// Stop, change while inactive, then restart
camera.stop();
QTRY_VERIFY(!camera.isActive());
imageCapture.setResolution(sizes.last());
camera.start();
QTRY_VERIFY(camera.isActive());
const QSize capturedAfterRestart = captureToFileAndGetSize(imageCapture);
QVERIFY2(!capturedAfterRestart.isEmpty(), "Capture failed after restart");
#ifdef Q_OS_ANDROID
QVERIFY(isSameSizeAllowingRotation(capturedAfterRestart, sizes.last()));
#endif
}
void tst_QCameraBackend::captureToFile_createsFileWithExpectedExtension_data()
{
QTest::addColumn<QString>("inputFilename");
QTest::addColumn<QImageCapture::FileFormat>("format");
QTest::addColumn<QString>("expectedFilename");
{
QTest::addRow("Add file extension to JPEG file without extension")
<< "file" << QImageCapture::JPEG << "file.jpg";
QTest::addRow("Keep extension of JPEG file with extension")
<< "file.jpeg" << QImageCapture::JPEG << "file.jpeg";
QTest::addRow("Keep extension of JPEG file with wrong extension")
<< "file.png" << QImageCapture::JPEG << "file.png";
}
{
QTest::addRow("Add file extension to PNG file without extension")
<< "file" << QImageCapture::PNG << "file.png";
QTest::addRow("Keep extension of PNG file with extension")
<< "file.apng" << QImageCapture::PNG << "file.apng";
QTest::addRow("Keep extension of PNG file with wrong extension")
<< "file.jpg" << QImageCapture::PNG << "file.jpg";
}
{
QTest::addRow("Add file extension to WebP file without extension")
<< "file" << QImageCapture::WebP << "file.webp";
QTest::addRow("Keep extension of WebP file with extension")
<< "file.web" << QImageCapture::WebP << "file.web";
QTest::addRow("Keep extension of WebP file with wrong extension")
<< "file.png" << QImageCapture::WebP << "file.png";
}
{
QTest::addRow("Add file extension to Tiff file without extension")
<< "file" << QImageCapture::Tiff << "file.tiff";
QTest::addRow("Keep extension of Tiff file with extension")
<< "file.cr2" << QImageCapture::Tiff << "file.cr2";
QTest::addRow("Keep extension of Tiff file with wrong extension")
<< "file.png" << QImageCapture::Tiff << "file.png";
}
}
void tst_QCameraBackend::captureToFile_createsFileWithExpectedExtension()
{
if (noCamera)
QSKIP("No camera available");
if (isAndroidPlatform())
QSKIP("Crashes in QImageCapture::setFileFormat() with the android backend");
QFETCH(const QString, inputFilename);
QFETCH(const QImageCapture::FileFormat, format);
QFETCH(const QString, expectedFilename);
QCamera camera;
camera.setFlashMode(QCamera::FlashOff);
QImageCapture imageCapture;
imageCapture.setFileFormat(format);
QMediaCaptureSession session;
session.setCamera(&camera);
session.setImageCapture(&imageCapture);
QSignalSpy savedSignal(&imageCapture, &QImageCapture::imageSaved);
camera.start();
QTRY_VERIFY(imageCapture.isReadyForCapture());
QTemporaryDir tempDir;
tempDir.setAutoRemove(false);
const QString tempFile = tempDir.filePath(inputFilename);
imageCapture.captureToFile(tempFile);
QTRY_VERIFY(!savedSignal.isEmpty());
const QString imagePath = tempDir.filePath(expectedFilename);
QVERIFY(QFile::exists(imagePath));
QImage image;
QVERIFY(image.load(imagePath));
QVERIFY(!image.isNull());
}
void tst_QCameraBackend::testCameraCaptureMetadata()
{
if (noCamera)
QSKIP("No camera available");
QMediaCaptureSession session;
QCamera camera;
QImageCapture imageCapture;
session.setCamera(&camera);
session.setImageCapture(&imageCapture);
camera.setFlashMode(QCamera::FlashOff);
QMediaMetaData referenceMetaData;
referenceMetaData.insert(QMediaMetaData::Title, QStringLiteral("Title"));
referenceMetaData.insert(QMediaMetaData::Language, QVariant::fromValue(QLocale::German));
referenceMetaData.insert(QMediaMetaData::Description, QStringLiteral("Description"));
imageCapture.setMetaData(referenceMetaData);
QSignalSpy metadataSignal(&imageCapture, &QImageCapture::imageMetadataAvailable);
QSignalSpy savedSignal(&imageCapture, &QImageCapture::imageSaved);
camera.start();
QTRY_VERIFY(imageCapture.isReadyForCapture());
QTemporaryDir dir;
auto tmpFile = dir.filePath("testImage");
int id = imageCapture.captureToFile(tmpFile);
QTRY_VERIFY(!savedSignal.isEmpty());
QVERIFY(!metadataSignal.isEmpty());
QCOMPARE(metadataSignal.first().first().toInt(), id);
QMediaMetaData receivedMetaData = metadataSignal.first()[1].value<QMediaMetaData>();
if (isGStreamerPlatform()) {
for (auto key : {
QMediaMetaData::Title,
QMediaMetaData::Language,
QMediaMetaData::Description,
})
QCOMPARE(receivedMetaData[key], referenceMetaData[key]);
QVERIFY(receivedMetaData[QMediaMetaData::Resolution].isValid());
}
}
void tst_QCameraBackend::testExposureCompensation()
{
if (noCamera)
QSKIP("No camera available");
QMediaCaptureSession session;
QCamera camera;
session.setCamera(&camera);
QSignalSpy exposureCompensationSignal(&camera, &QCamera::exposureCompensationChanged);
// it should be possible to set exposure parameters in Unloaded state
QCOMPARE(camera.exposureCompensation(), 0.);
if (!(camera.supportedFeatures() & QCamera::Feature::ExposureCompensation))
return;
camera.setExposureCompensation(1.0);
QCOMPARE(camera.exposureCompensation(), 1.0);
QTRY_COMPARE(exposureCompensationSignal.size(), 1);
QCOMPARE(exposureCompensationSignal.last().first().toReal(), 1.0);
//exposureCompensationChanged should not be emitted when value is not changed
camera.setExposureCompensation(1.0);
QTest::qWait(50);
QCOMPARE(exposureCompensationSignal.size(), 1);
//exposure compensation should be preserved during start
camera.start();
QTRY_VERIFY(camera.isActive());
QCOMPARE(camera.exposureCompensation(), 1.0);
exposureCompensationSignal.clear();
camera.setExposureCompensation(-1.0);
QCOMPARE(camera.exposureCompensation(), -1.0);
QTRY_COMPARE(exposureCompensationSignal.size(), 1);
QCOMPARE(exposureCompensationSignal.last().first().toReal(), -1.0);
}
void tst_QCameraBackend::testExposureMode()
{
if (noCamera)
QSKIP("No camera available");
QCamera camera;
QCOMPARE(camera.exposureMode(), QCamera::ExposureAuto);
// Night
if (camera.isExposureModeSupported(QCamera::ExposureNight)) {
camera.setExposureMode(QCamera::ExposureNight);
QCOMPARE(camera.exposureMode(), QCamera::ExposureNight);
camera.start();
QVERIFY(camera.isActive());
QCOMPARE(camera.exposureMode(), QCamera::ExposureNight);
}
camera.stop();
QTRY_VERIFY(!camera.isActive());
// Auto
camera.setExposureMode(QCamera::ExposureAuto);
QCOMPARE(camera.exposureMode(), QCamera::ExposureAuto);
camera.start();
QTRY_VERIFY(camera.isActive());
QCOMPARE(camera.exposureMode(), QCamera::ExposureAuto);
// Manual
if (camera.isExposureModeSupported(QCamera::ExposureManual)) {
camera.setExposureMode(QCamera::ExposureManual);
QCOMPARE(camera.exposureMode(), QCamera::ExposureManual);
camera.start();
QVERIFY(camera.isActive());
QCOMPARE(camera.exposureMode(), QCamera::ExposureManual);
camera.setManualExposureTime(.02f); // ~20ms should be supported by most cameras
QVERIFY(camera.manualExposureTime() > .01 && camera.manualExposureTime() < .04);
}
camera.setExposureMode(QCamera::ExposureAuto);
}
void tst_QCameraBackend::testVideoRecording_data()
{
QTest::addColumn<QCameraDevice>("device");
const auto devices = QMediaDevices::videoInputs();
int i = 0;
for (const auto &device : devices)
QTest::addRow("%d - %s", i++, device.description().toUtf8().constData()) << device;
if (devices.isEmpty())
QTest::newRow("Null device") << QCameraDevice();
}
void tst_QCameraBackend::testVideoRecording()
{
QSKIP_OHOS("OH_AVPlayer does not extract MP4 stream metadata (resolution) yet, so player.metaData() is empty");
if (noCamera)
QSKIP("No camera available");
QFETCH(QCameraDevice, device);
QMediaCaptureSession session;
std::unique_ptr<QCamera> camera(new QCamera(device));
session.setCamera(camera.get());
QMediaRecorder recorder;
session.setRecorder(&recorder);
QSignalSpy errorSignal(camera.get(), &QCamera::errorOccurred);
QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred);
QSignalSpy recorderStateChanged(&recorder, &QMediaRecorder::recorderStateChanged);
QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged);
recorder.setVideoResolution(320, 240);
// Insert metadata
QMediaMetaData metaData;
metaData.insert(QMediaMetaData::Author, QStringLiteral("Author"));
metaData.insert(QMediaMetaData::Date, QDateTime::currentDateTime());
recorder.setMetaData(metaData);
camera->start();
if (noCamera || device.isNull()) {
QVERIFY(!camera->isActive());
return;
}
QTRY_VERIFY(camera->isActive());
QTRY_VERIFY(camera->isActive());
recorder.record();
if (!recorderErrorSignal.empty() || recorderErrorSignal.wait(550)) {
QEXPECT_FAIL_GSTREAMER("", "QTBUG-124148: GStreamer might return ResourceError", Continue);
QCOMPARE(recorderErrorSignal.last().first().toInt(), QMediaRecorder::FormatError);
return;
}
QTRY_VERIFY(durationChanged.size());
QCOMPARE(recorder.metaData(), metaData);
recorderStateChanged.clear();
recorder.stop();
QTRY_VERIFY(recorderStateChanged.size() > 0);
QVERIFY(recorder.recorderState() == QMediaRecorder::StoppedState);
QVERIFY(errorSignal.isEmpty());
QVERIFY(recorderErrorSignal.isEmpty());
QString fileName = recorder.actualLocation().toLocalFile();
QVERIFY(!fileName.isEmpty());
QVERIFY(QFileInfo(fileName).size() > 0);
QMediaPlayer player;
// Should this be recorder.actualLocation() instead?
player.setSource(QUrl::fromLocalFile(fileName));
QTRY_COMPARE_WITH_TIMEOUT(player.mediaStatus(), QMediaPlayer::LoadedMedia, 8s);
QCOMPARE_EQ(player.metaData().value(QMediaMetaData::Resolution).toSize(), QSize(320, 240));
QCOMPARE_GT(player.duration(), 350);
QCOMPARE_LT(player.duration(), 650);
// TODO: integrate with a virtual camera and check mediaplayer output
QFile(fileName).remove();
}
void tst_QCameraBackend::testNativeMetadata()
{
QSKIP_OHOS("OH_AVRecorder/OH_AVPlayer do not yet round-trip MP4 user metadata (Title, Language, Description)");
if (noCamera)
QSKIP("No camera available");
QMediaCaptureSession session;
QCameraDevice device = QMediaDevices::defaultVideoInput();
QCamera camera(device);
session.setCamera(&camera);
QMediaRecorder recorder;
session.setRecorder(&recorder);
QSignalSpy errorSignal(&camera, &QCamera::errorOccurred);
QSignalSpy recorderErrorSignal(&recorder, &QMediaRecorder::errorOccurred);
QSignalSpy recorderStateChanged(&recorder, &QMediaRecorder::recorderStateChanged);
QSignalSpy durationChanged(&recorder, &QMediaRecorder::durationChanged);
camera.start();
if (device.isNull()) {
QVERIFY(!camera.isActive());
return;
}
QTRY_VERIFY(camera.isActive());
// Insert common metadata supported on all platforms
// Don't use Date, as some backends set that on their own
QMediaMetaData metaData;
metaData.insert(QMediaMetaData::Title, QStringLiteral("Title"));
metaData.insert(QMediaMetaData::Language, QVariant::fromValue(QLocale::German));
metaData.insert(QMediaMetaData::Description, QStringLiteral("Description"));
recorder.setMetaData(metaData);
recorder.record();
QTRY_VERIFY(durationChanged.size());
QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::RecorderState::RecordingState);
QCOMPARE(recorder.metaData(), metaData);
recorderStateChanged.clear();
recorder.stop();
QTRY_VERIFY(recorderStateChanged.size() > 0);
QTRY_VERIFY(recorder.recorderState() == QMediaRecorder::RecorderState::StoppedState);
QVERIFY(errorSignal.isEmpty());
if (!isGStreamerPlatform()) {
QVERIFY(recorderErrorSignal.isEmpty());
}
QString fileName = recorder.actualLocation().toLocalFile();
QVERIFY(!fileName.isEmpty());
QVERIFY(QFileInfo(fileName).size() > 0);
// QMediaRecorder::metaData() can only test that QMediaMetaData is set properly on the recorder.
// Use QMediaPlayer to test that the native metadata is properly set on the track
QAudioOutput output;
QMediaPlayer player;
player.setAudioOutput(&output);
QSignalSpy metadataChangedSpy(&player, &QMediaPlayer::metaDataChanged);
player.setSource(QUrl::fromLocalFile(fileName));
player.play();
int metadataChangedRequiredCount = isGStreamerPlatform() ? 2 : 1;
QTRY_VERIFY(metadataChangedSpy.size() >= metadataChangedRequiredCount);
QCOMPARE(player.metaData().value(QMediaMetaData::Title).toString(),
metaData.value(QMediaMetaData::Title).toString());
auto lang = player.metaData().value(QMediaMetaData::Language).value<QLocale::Language>();
if (lang != QLocale::AnyLanguage)
QCOMPARE(lang, metaData.value(QMediaMetaData::Language).value<QLocale::Language>());
QCOMPARE(player.metaData().value(QMediaMetaData::Description).toString(), metaData.value(QMediaMetaData::Description).toString());
QVERIFY(player.metaData().value(QMediaMetaData::Resolution).isValid());
if (isGStreamerPlatform())
QVERIFY(player.metaData().value(QMediaMetaData::Date).isValid());
player.stop();
player.setSource({});
QFile(fileName).remove();
}
void tst_QCameraBackend::multipleCameraSet()
{
if (noCamera)
QSKIP("No camera available");
QMediaCaptureSession session;
QCameraDevice device = QMediaDevices::defaultVideoInput();
QMediaRecorder recorder;
session.setRecorder(&recorder);
for (int i = 0; i < 5; ++i) {
#ifdef Q_OS_DARWIN
QMacAutoReleasePool releasePool;
#endif
QCamera camera(device);
session.setCamera(&camera);
camera.start();
QTest::qWait(100);
}
}
QTEST_MAIN(tst_QCameraBackend)
#include "tst_qcamerabackend.moc"