Revision control
Copy as Markdown
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtMultimediaTestLib/private/audiogenerationutils_p.h>
#include <QtMultimediaTestLib/private/capturesessionfixture_p.h>
#include <QtMultimediaTestLib/private/formatutils_p.h>
#include <QtMultimediaTestLib/private/mediabackendutils_p.h>
#include <QtMultimediaTestLib/private/mediainfo_p.h>
#include <QtMultimediaTestLib/private/osdetection_p.h>
#include <QtMultimediaTestLib/private/qcolorutil_p.h>
#include <QtMultimediaTestLib/private/qfileutil_p.h>
#include <QtMultimediaTestLib/private/qintegrationtestbase_p.h>
#include <QtTest/qtest.h>
#include <QtMultimedia/qaudiobufferinput.h>
#include <QtMultimedia/qaudiodevice.h>
#include <QtMultimedia/qmediacapturesession.h>
#include <QtMultimedia/qmediadevices.h>
#include <QtMultimedia/qmediaformat.h>
#include <QtMultimedia/qmediarecorder.h>
#include <QtMultimedia/qwavedecoder.h>
#include <QtMultimedia/private/qplatformmediacapture_p.h>
#include <QtMultimedia/private/qplatformvideosource_p.h>
#include <QtCore/qbuffer.h>
#include <QtCore/qmimetype.h>
#include <QtCore/qtemporarydir.h>
#include <chrono>
using namespace std::chrono_literals;
namespace {
bool isSupportedPixelFormat(QVideoFrameFormat::PixelFormat pixelFormat)
{
// TODO: Enable more pixel formats once support is added
switch (pixelFormat) {
case QVideoFrameFormat::Format_AYUV:
case QVideoFrameFormat::Format_AYUV_Premultiplied:
case QVideoFrameFormat::Format_YV12:
case QVideoFrameFormat::Format_IMC1:
case QVideoFrameFormat::Format_IMC2:
case QVideoFrameFormat::Format_IMC3:
case QVideoFrameFormat::Format_IMC4:
case QVideoFrameFormat::Format_YUV420P10: // TODO: Cpu conversion not implemented, fails in
// CI if RHI is not supported
case QVideoFrameFormat::Format_Y16: // TODO: Fails on Android
case QVideoFrameFormat::Format_P010: // TODO: Fails on Android
case QVideoFrameFormat::Format_P016: // TODO: Fails on Android
case QVideoFrameFormat::Format_SamplerExternalOES:
case QVideoFrameFormat::Format_SamplerRect:
return false;
default:
return true;
}
}
std::set<QMediaFormat::VideoCodec> unsupportedVideoCodecs(QMediaFormat::FileFormat fileFormat)
{
using VideoCodec = QMediaFormat::VideoCodec;
std::set<QMediaFormat::VideoCodec> unsupportedCodecs;
if constexpr (isMacOS) {
if (isArm && isCI()) {
switch (fileFormat) {
case QMediaFormat::FileFormat::WMV:
case QMediaFormat::FileFormat::AVI:
case QMediaFormat::MPEG4:
case QMediaFormat::QuickTime:
case QMediaFormat::Matroska:
// h264_videotoolbox fails to open on some macOS/arm CI machines.
unsupportedCodecs.insert(VideoCodec::H264);
break;
default:
break;
}
}
}
return unsupportedCodecs;
}
void verifyEncodedFrames(const MediaInfo &info, const std::vector<QColor> &expectedColors)
{
QCOMPARE_EQ(size_t(info.m_frameCount), expectedColors.size());
QCOMPARE_EQ(info.m_colors.size(), expectedColors.size());
for (auto i = 0U; i < expectedColors.size(); ++i)
// HACK: Increase tolerance to accommodate for artifacts
QVERIFY2(fuzzyCompare(info.m_colors[i][0], expectedColors.at(i), 0.1f),
qPrintable(QString("Frame %1: expected %2, got %3")
.arg(i)
.arg(expectedColors.at(i).name())
.arg(info.m_colors.at(i).at(0).name())));
}
} // namespace
using namespace Qt::StringLiterals;
using namespace QtMultimediaTest;
class tst_QMediaRecorderBackend : public QIntegrationTestBase
{
Q_OBJECT
public slots:
void cleanupTestCase();
private slots:
void initTestCase()
{
if (!initIntegrationTestCase())
return;
}
void record_createsFileWithExpectedExtension_whenRecordingAudio_data();
void record_createsFileWithExpectedExtension_whenRecordingAudio();
void record_writesVideo_whenInputFrameShrinksOverTime();
void record_writesVideo_whenInputFrameGrowsOverTime();
void record_stopsRecording_whenInputsReportedEndOfStream_data();
void record_stopsRecording_whenInputsReportedEndOfStream();
void record_writesVideo_withoutTransforms_whenPresentationTransformsPresent_data();
void record_writesVideo_withoutTransforms_whenPresentationTransformsPresent();
void record_writesVideo_withCorrectColors_data();
void record_writesVideo_withCorrectColors();
void actualLocation_returnsNonEmptyLocation_whenRecorderEntersRecordingState();
void record_writesToOutputDevice_whenWritableOutputDeviceAndLocationAreSet();
void record_toQIODevice();
void record_writesToOutputLocation_whenNotWritableOutputDeviceAndLocationAreSet();
void record_writesVideo_withAllSupportedVideoFormats_data();
void record_writesVideo_withAllSupportedVideoFormats();
void record_writesAudio_withAllSupportedAudioFormats_data();
void record_writesAudio_withAllSupportedAudioFormats();
void record_emits_mediaformatChanged_whenFormatChanged();
void stop_stopsRecording_whenInvokedUponRecordingStart();
void record_reflectsAudioEncoderSetting();
void record_adjustsFrameRate_whenSourceFrameRateDifferentFromEncoderFrameRate_data();
void record_adjustsFrameRate_whenSourceFrameRateDifferentFromEncoderFrameRate();
void record_writesLastFrame_whenAdapterHadBufferedItBeforeEndOfStream();
void record_encodesFrames_whenSourceTimestampsAreIrregular_data();
void record_encodesFrames_whenSourceTimestampsAreIrregular();
void record_dropsPausedFramesAndPauseGap_whenRecordingIsResumed_data();
void record_dropsPausedFramesAndPauseGap_whenRecordingIsResumed();
private:
QTemporaryDir m_tempDir;
};
void tst_QMediaRecorderBackend::cleanupTestCase()
{
// Copy any stored files in the temporary directory over to COIN result directory
// to allow inspecting image differences.
if (qEnvironmentVariableIsSet("COIN_CTEST_RESULTSDIR")) {
const QDir sourceDir = m_tempDir.path();
const QDir resultsDir{ qEnvironmentVariable("COIN_CTEST_RESULTSDIR") };
if (!copyAllFiles(sourceDir, resultsDir))
qWarning() << "Failed to copy files to COIN_CTEST_RESULTSDIR";
}
}
void tst_QMediaRecorderBackend::record_createsFileWithExpectedExtension_whenRecordingAudio_data()
{
QTest::addColumn<QMediaFormat::FileFormat>("fileFormat");
QTest::addColumn<QString>("inputFileName");
QTest::addColumn<QString>("expectedFileName");
QMediaFormat format;
for (const QMediaFormat::FileFormat &fileFormat :
format.supportedFileFormats(QMediaFormat::Encode)) {
const QByteArray formatName = QMediaFormat::fileFormatName(fileFormat).toLatin1();
{
// Verify that extension is appended if not already present
QByteArray testName = formatName + " without extension";
QString inputFileName = u"filename"_s;
QString expected = inputFileName;
const QMediaFormat mediaFormat(fileFormat);
const QMimeType mimeType = mediaFormat.mimeType();
const QString preferredExt = mimeType.preferredSuffix();
if (!preferredExt.isEmpty())
expected += "." + preferredExt;
QTest::addRow("%s", testName.data()) << fileFormat << inputFileName << expected;
}
{
// Verify that default extension is not appended when extension is wrong
QByteArray testName = formatName + " with wrong extension";
QString inputFileName = u"filename.mp4"_s;
QString expected = u"filename.mp4"_s;
QTest::addRow("%s", testName.data()) << fileFormat << inputFileName << expected;
}
}
}
void tst_QMediaRecorderBackend::record_createsFileWithExpectedExtension_whenRecordingAudio()
{
QSKIP_IF_NOT_FFMPEG("This test requires APIs that are only implemented with FFmpeg media backend");
QFETCH(const QMediaFormat::FileFormat, fileFormat);
QFETCH(const QString, inputFileName);
QFETCH(const QString, expectedFileName);
QTemporaryDir tempDir;
const QUrl url = QUrl::fromLocalFile(tempDir.filePath(inputFileName));
QMediaCaptureSession session;
QMediaRecorder recorder;
recorder.setOutputLocation(url);
recorder.setMediaFormat({ fileFormat });
QAudioFormat format;
format.setChannelConfig(QAudioFormat::ChannelConfigMono);
format.setChannelCount(1);
format.setSampleFormat(QAudioFormat::Float);
format.setSampleRate(44100);
QAudioBufferInput input{ format };
session.setAudioBufferInput(&input);
session.setRecorder(&recorder);
AudioGenerator generator;
generator.setFormat(format);
generator.setDuration(1s);
generator.emitEmptyBufferOnStop();
bool done = false;
connect(&recorder, &QMediaRecorder::recorderStateChanged, &recorder,
[&](QMediaRecorder::RecorderState state) {
if (state == QMediaRecorder::StoppedState)
done = true;
});
connect(&input, &QAudioBufferInput::readyToSendAudioBuffer, //
&generator, &AudioGenerator::nextBuffer);
connect(&generator, &AudioGenerator::audioBufferCreated, //
&input, &QAudioBufferInput::sendAudioBuffer);
recorder.setAutoStop(true);
recorder.record();
QTRY_VERIFY_WITH_TIMEOUT(done, 60s); // Timeout can be as large as possible
const QUrl loc = recorder.actualLocation();
const bool pass = loc.toString().endsWith(expectedFileName);
if (!pass)
qWarning() << loc << "does not match expected " << expectedFileName;
QVERIFY(pass);
}
void tst_QMediaRecorderBackend::record_writesVideo_whenInputFrameShrinksOverTime()
{
QSKIP_IF_NOT_FFMPEG();
CaptureSessionFixture f{ StreamType::Video };
f.start(RunMode::Push, AutoStop::EmitEmpty);
f.readyToSendVideoFrame.wait();
constexpr int startSize = 38;
int frameCount = 0;
for (int i = 0; i < startSize; ++i) {
++frameCount;
const QSize size{ startSize - i, startSize - i };
f.m_videoGenerator.setSize(size);
QVERIFY(f.m_videoInput.sendVideoFrame(f.m_videoGenerator.createFrame()));
f.readyToSendVideoFrame.wait();
}
f.m_videoInput.sendVideoFrame({});
QVERIFY(f.waitForRecorderStopped(60s));
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().constData());
auto info = MediaInfo::create(f.m_recorder.actualLocation());
QCOMPARE_EQ(info->m_frameCount, frameCount);
// All frames should be resized to the size of the first frame
QCOMPARE_EQ(info->m_size, QSize(startSize, startSize));
}
void tst_QMediaRecorderBackend::record_writesVideo_whenInputFrameGrowsOverTime()
{
QSKIP_IF_NOT_FFMPEG();
CaptureSessionFixture f{ StreamType::Video };
f.start(RunMode::Push, AutoStop::EmitEmpty);
f.readyToSendVideoFrame.wait();
constexpr int startSize = 38;
constexpr int maxSize = 256;
int frameCount = 0;
for (int i = 0; i < maxSize - startSize; ++i) {
++frameCount;
const QSize size{ startSize + i, startSize + i };
f.m_videoGenerator.setPattern(ImagePattern::ColoredSquares);
f.m_videoGenerator.setSize(size);
QVERIFY(f.m_videoInput.sendVideoFrame(f.m_videoGenerator.createFrame()));
f.readyToSendVideoFrame.wait();
}
f.m_videoInput.sendVideoFrame({});
QVERIFY(f.waitForRecorderStopped(60s));
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().constData());
auto info = MediaInfo::create(f.m_recorder.actualLocation());
QCOMPARE_EQ(info->m_frameCount, frameCount);
// All frames should be resized to the size of the first frame
QCOMPARE_EQ(info->m_size, QSize(startSize, startSize));
}
void tst_QMediaRecorderBackend::record_stopsRecording_whenInputsReportedEndOfStream_data()
{
QTest::addColumn<bool>("audioStopsFirst");
QTest::addRow("audio stops first") << true;
QTest::addRow("video stops first") << true;
}
void tst_QMediaRecorderBackend::record_stopsRecording_whenInputsReportedEndOfStream()
{
QSKIP_IF_NOT_FFMPEG();
QFETCH(const bool, audioStopsFirst);
CaptureSessionFixture f{ StreamType::AudioAndVideo };
f.m_recorder.setAutoStop(true);
f.m_audioGenerator.setBufferCount(30);
f.m_videoGenerator.setFrameCount(30);
QSignalSpy audioDone{ &f.m_audioGenerator, &AudioGenerator::done };
QSignalSpy videoDone{ &f.m_videoGenerator, &VideoGenerator::done };
f.start(RunMode::Pull, AutoStop::No);
audioDone.wait();
videoDone.wait();
if (audioStopsFirst) {
f.m_audioInput.sendAudioBuffer({});
QVERIFY(!f.waitForRecorderStopped(300ms)); // Should not stop until both streams stopped
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().constData());
f.m_videoInput.sendVideoFrame({});
} else {
f.m_videoInput.sendVideoFrame({});
QVERIFY(!f.waitForRecorderStopped(300ms)); // Should not stop until both streams stopped
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().constData());
f.m_audioInput.sendAudioBuffer({});
}
QVERIFY(f.waitForRecorderStopped(60s));
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().constData());
// check if the file has been written
const std::optional<MediaInfo> mediaInfo = MediaInfo::create(f.m_recorder.actualLocation());
QVERIFY(mediaInfo);
QVERIFY(mediaInfo->m_hasVideo);
QVERIFY(mediaInfo->m_hasAudio);
}
void tst_QMediaRecorderBackend::record_writesVideo_withoutTransforms_whenPresentationTransformsPresent_data()
{
QTest::addColumn<QVideoFrameFormat::PixelFormat>("pixelFormat");
QTest::addColumn<QtVideo::Rotation>("presentationRotation");
QTest::addColumn<bool>("presentationMirrored");
const std::array pixelFormats = { QVideoFrameFormat::Format_BGRA8888,
QVideoFrameFormat::Format_Jpeg };
for (const QVideoFrameFormat::PixelFormat pixelFormat : pixelFormats) {
const QByteArray formatName =
QVideoFrameFormat::pixelFormatToString(pixelFormat).toLatin1();
auto addRow = [&](const char *description, QtVideo::Rotation rotation, bool mirrored) {
QTest::addRow("%s, %s", formatName.data(), description)
<< pixelFormat << rotation << mirrored;
};
addRow("No rotation, not mirrored", QtVideo::Rotation::None, false);
addRow("90 degrees, not mirrored", QtVideo::Rotation::Clockwise90, false);
addRow("180 degrees, not mirrored", QtVideo::Rotation::Clockwise180, false);
addRow("270 degrees, not mirrored", QtVideo::Rotation::Clockwise270, false);
addRow("No rotation, mirrored", QtVideo::Rotation::None, true);
addRow("90 degrees, mirrored", QtVideo::Rotation::Clockwise90, true);
addRow("180 degrees, mirrored", QtVideo::Rotation::Clockwise180, true);
addRow("270 degrees, mirrored", QtVideo::Rotation::Clockwise270, true);
}
}
void tst_QMediaRecorderBackend::record_writesVideo_withoutTransforms_whenPresentationTransformsPresent()
{
QSKIP_IF_NOT_FFMPEG();
QFETCH(const QVideoFrameFormat::PixelFormat, pixelFormat);
QFETCH(const QtVideo::Rotation, presentationRotation);
QFETCH(const bool, presentationMirrored);
CaptureSessionFixture f{ StreamType::Video };
f.m_videoGenerator.setPixelFormat(pixelFormat);
f.m_videoGenerator.setPattern(ImagePattern::ColoredSquares);
f.m_videoGenerator.setFrameCount(3);
f.m_videoGenerator.setPresentationRotation(presentationRotation);
f.m_videoGenerator.setPresentationMirrored(presentationMirrored);
f.start(RunMode::Pull, AutoStop::EmitEmpty);
QVERIFY(f.waitForRecorderStopped(60s));
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().constData());
const auto info = MediaInfo::create(f.m_recorder.actualLocation());
QCOMPARE_EQ(info->m_colors.size(), 3u);
std::array<QColor, 4> colors = info->m_colors.front();
QVERIFY(fuzzyCompare(colors[0], Qt::red));
QVERIFY(fuzzyCompare(colors[1], Qt::green));
QVERIFY(fuzzyCompare(colors[2], Qt::blue));
QVERIFY(fuzzyCompare(colors[3], Qt::yellow));
}
void tst_QMediaRecorderBackend::record_writesVideo_withCorrectColors_data()
{
QTest::addColumn<QVideoFrameFormat::PixelFormat>("pixelFormat");
for (int i = QVideoFrameFormat::Format_ARGB8888; i < QVideoFrameFormat::NPixelFormats; ++i) {
const auto format = static_cast<QVideoFrameFormat::PixelFormat>(i);
if (!isSupportedPixelFormat(format))
continue;
const QByteArray formatName = QVideoFrameFormat::pixelFormatToString(format).toLatin1();
QTest::addRow("%s", formatName.data()) << format;
}
}
void tst_QMediaRecorderBackend::record_writesVideo_withCorrectColors()
{
QSKIP_IF_NOT_FFMPEG();
QFETCH(const QVideoFrameFormat::PixelFormat, pixelFormat);
// Arrange
CaptureSessionFixture f{ StreamType::Video };
f.m_videoGenerator.setPixelFormat(pixelFormat);
f.m_videoGenerator.setPattern(ImagePattern::ColoredSquares);
f.m_videoGenerator.setFrameCount(1);
f.m_videoGenerator.setSize({ 128, 64 }); // Small frames to speed up test
f.start(RunMode::Push, AutoStop::EmitEmpty);
// Act: Push one frame through and send sentinel stop frame
f.readyToSendVideoFrame.wait();
f.m_videoGenerator.nextFrame();
f.readyToSendVideoFrame.wait();
f.m_videoGenerator.nextFrame();
QVERIFY(f.waitForRecorderStopped(60s));
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
qPrintable(f.m_recorder.errorString()));
const auto info = MediaInfo::create(f.m_recorder.actualLocation(), /*keep frames */ true);
const QImage expectedImage = f.m_videoGenerator.createFrame().toImage();
QCOMPARE_EQ(info->m_frames.size(), 2u); // Front has content, back is empty
const QImage actualImage = info->m_frames.front().toImage();
// Store images to simplify debugging/verifying output
const QString path = m_tempDir.filePath(QVideoFrameFormat::pixelFormatToString(pixelFormat));
QVERIFY(expectedImage.save(path + "_expected.png"));
QVERIFY(actualImage.save(path + "_actual.png"));
// Extract center of each quadrant, because recorder compression introduces artifacts
// in color boundaries.
const std::array<QColor, 4> expectedColors = MediaInfo::sampleQuadrants(expectedImage);
const std::array<QColor, 4> actualColors = MediaInfo::sampleQuadrants(actualImage);
// Assert that colors are similar (not exactly the same because compression introduces minor
// differences)
#ifdef Q_OS_MACOS
// workaround for hevc_videotoolbox
// YUV420P may encode to 10-bit yuv420p10le, which has more quantization artifacts when
// converting from 8-bit source frames. Use higher tolerance for all quadrants in this case.
const float tolerance = (pixelFormat == QVideoFrameFormat::Format_YUV420P) ? 0.05f : 0.01f;
#else
const float tolerance = 0.01f;
#endif
auto verifyQuadrantColor = [&](int quadrant) {
QVERIFY2(fuzzyCompare(expectedColors[quadrant], actualColors[quadrant], tolerance),
qPrintable(QString("Quadrant %1 color mismatch for %2. "
"Expected RGBA(%3,%4,%5,%6), got RGBA(%7,%8,%9,%10)")
.arg(quadrant)
.arg(QVideoFrameFormat::pixelFormatToString(pixelFormat))
.arg(expectedColors[quadrant].red())
.arg(expectedColors[quadrant].green())
.arg(expectedColors[quadrant].blue())
.arg(expectedColors[quadrant].alpha())
.arg(actualColors[quadrant].red())
.arg(actualColors[quadrant].green())
.arg(actualColors[quadrant].blue())
.arg(actualColors[quadrant].alpha())));
};
verifyQuadrantColor(0);
verifyQuadrantColor(1);
verifyQuadrantColor(2);
verifyQuadrantColor(3);
}
void tst_QMediaRecorderBackend::actualLocation_returnsNonEmptyLocation_whenRecorderEntersRecordingState()
{
QSKIP_IF_NOT_FFMPEG();
const QUrl url = QUrl::fromLocalFile(m_tempDir.filePath("any_file_name"));
CaptureSessionFixture f{ StreamType::AudioAndVideo };
f.m_recorder.setOutputLocation(url);
auto onStateChanged = [&f](QMediaRecorder::RecorderState state) {
QCOMPARE(state, QMediaRecorder::RecordingState);
QCOMPARE_NE(f.m_recorder.actualLocation(), QUrl());
};
connect(&f.m_recorder, &QMediaRecorder::recorderStateChanged, this, onStateChanged,
Qt::SingleShotConnection);
QCOMPARE(f.m_recorder.actualLocation(), QUrl());
f.start(RunMode::Pull, AutoStop::EmitEmpty);
QTRY_COMPARE(f.m_recorder.recorderState(), QMediaRecorder::RecordingState);
f.m_recorder.stop();
}
void tst_QMediaRecorderBackend::record_writesToOutputDevice_whenWritableOutputDeviceAndLocationAreSet()
{
QSKIP_IF_NOT_FFMPEG();
// Arrange
const QUrl url = QUrl::fromLocalFile(m_tempDir.filePath("file_to_be_not_created.mp4"));
CaptureSessionFixture f{ StreamType::Audio };
f.m_recorder.setOutputLocation(url);
QTemporaryFile tempFile;
QTEST_ASSERT(tempFile.open());
f.m_recorder.setOutputDevice(&tempFile);
// Act
f.start(RunMode::Pull, AutoStop::EmitEmpty);
QVERIFY(f.waitForRecorderStopped(60s));
tempFile.close();
// Assert
QVERIFY(!QFileInfo::exists(url.toLocalFile()));
QCOMPARE(f.m_recorder.actualLocation(), QUrl());
QCOMPARE_GT(tempFile.size(), 0);
}
void tst_QMediaRecorderBackend::record_toQIODevice()
{
QSKIP_IF_NOT_FFMPEG();
CaptureSessionFixture f{ StreamType::Audio };
f.m_recorder.setMediaFormat({ QMediaFormat::Wave });
f.m_audioGenerator.setBufferCount(30);
QBuffer buffer;
QVERIFY(buffer.open(QIODevice::WriteOnly));
f.start(RunMode::Pull, AutoStop::EmitEmpty, &buffer);
QVERIFY(f.waitForRecorderStopped(60s));
buffer.close();
QCOMPARE(f.m_recorder.error(), QMediaRecorder::NoError);
QCOMPARE(f.m_recorder.actualLocation(), QUrl());
QCOMPARE_GT(buffer.size(), 0);
QVERIFY(buffer.data().startsWith("RIFF"));
}
void tst_QMediaRecorderBackend::record_writesToOutputLocation_whenNotWritableOutputDeviceAndLocationAreSet()
{
QSKIP_IF_NOT_FFMPEG();
// Arrange
CaptureSessionFixture f{ StreamType::Audio };
const QUrl url = QUrl::fromLocalFile(m_tempDir.filePath("file_to_be_not_created.mp4"));
QTemporaryFile tempFile;
f.m_recorder.setOutputDevice(&tempFile);
f.m_recorder.setOutputLocation(url);
// Act
f.start(RunMode::Pull, AutoStop::EmitEmpty);
QVERIFY(f.waitForRecorderStopped(60s));
tempFile.close();
// Assert
const QString actualLocation = f.m_recorder.actualLocation().toLocalFile();
QVERIFY(QFileInfo::exists(actualLocation));
QCOMPARE_GT(QFileInfo(actualLocation).size(), 0);
QCOMPARE_NE(f.m_recorder.actualLocation(), QUrl());
QCOMPARE(tempFile.size(), 0);
}
void tst_QMediaRecorderBackend::record_writesVideo_withAllSupportedVideoFormats_data()
{
QTest::addColumn<QMediaFormat>("format");
for (QMediaFormat f : allFileFormats(/*with unspecified*/ true)) {
for (const QMediaFormat::VideoCodec &c : allVideoCodecs(/*with unspecified*/ true)) {
f.setVideoCodec(c);
if (!f.isSupported(QMediaFormat::Encode))
continue;
const auto formatName = QMediaFormat::fileFormatName(f.fileFormat()).toLatin1();
const auto videoCodecName = QMediaFormat::videoCodecName(f.videoCodec()).toLatin1();
QTest::addRow("%s,%s", formatName.data(), videoCodecName.data()) << f;
}
}
}
void tst_QMediaRecorderBackend::record_writesVideo_withAllSupportedVideoFormats()
{
if (!isFFMPEGPlatform())
QSKIP("Tested only with FFmpeg backend because other backends don't have the same "
"format support");
QFETCH(const QMediaFormat, format);
CaptureSessionFixture f{ StreamType::Video };
f.m_recorder.setMediaFormat(format);
f.m_videoGenerator.setPattern(ImagePattern::ColoredSquares);
f.m_videoGenerator.setFrameCount(3);
f.m_videoGenerator.setFrameRate(24);
f.m_videoGenerator.setSize({ 128, 64 });
f.start(RunMode::Pull, AutoStop::EmitEmpty);
const auto actualFormat = f.m_recorder.mediaFormat();
qDebug() << "Actual format used: " << QMediaFormat::fileFormatName(actualFormat.fileFormat())
<< "/" << QMediaFormat::videoCodecName(actualFormat.videoCodec());
QVERIFY(f.waitForRecorderStopped(60s));
if (f.m_recorder.error() != QMediaRecorder::NoError
&& unsupportedVideoCodecs(actualFormat.fileFormat()).count(actualFormat.videoCodec()))
QEXPECT_FAIL("", "h264_videotoolbox fails to open on some CI machines", Abort);
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().data());
const std::optional<MediaInfo> info = MediaInfo::create(f.m_recorder.actualLocation());
QVERIFY(info);
QCOMPARE_GE(info->m_colors.size(),
2u); // TODO: We loose one frame with some combinations
QCOMPARE_LE(info->m_colors.size(), 3u);
std::array<QColor, 4> colors = info->m_colors.front();
QVERIFY(fuzzyCompare(colors[0], Qt::red));
QVERIFY(fuzzyCompare(colors[1], Qt::green));
QVERIFY(fuzzyCompare(colors[2], Qt::blue));
QVERIFY(fuzzyCompare(colors[3], Qt::yellow));
}
void tst_QMediaRecorderBackend::record_writesAudio_withAllSupportedAudioFormats_data()
{
QTest::addColumn<QMediaFormat>("format");
for (QMediaFormat f : allFileFormats(/*with unspecified*/ true)) {
for (const QMediaFormat::AudioCodec &c : allAudioCodecs(/*with unspecified*/ true)) {
f.setAudioCodec(c);
if (!f.isSupported(QMediaFormat::Encode))
continue;
#ifdef Q_OS_WIN
// QTBUG-147882: Wave audio codec with container formats other than Wave is broken on Windows
if (c == QMediaFormat::AudioCodec::Wave
&& f.fileFormat() != QMediaFormat::Wave) {
continue;
}
#endif
const auto formatName = QMediaFormat::fileFormatName(f.fileFormat()).toLatin1();
const auto audioCodecName = QMediaFormat::audioCodecName(f.audioCodec()).toLatin1();
QTest::addRow("%s,%s", formatName.data(), audioCodecName.data()) << f;
}
}
}
void tst_QMediaRecorderBackend::record_writesAudio_withAllSupportedAudioFormats()
{
if (!isFFMPEGPlatform())
QSKIP("Tested only with FFmpeg backend because other backends don't have the same "
"format support");
QFETCH(const QMediaFormat, format);
CaptureSessionFixture f{ StreamType::Audio };
f.m_recorder.setMediaFormat(format);
QAudioFormat audioFormat;
audioFormat.setSampleRate(44100); // TODO: Change to 8000 fails some tests
audioFormat.setSampleFormat(QAudioFormat::Float);
audioFormat.setChannelConfig(
QAudioFormat::ChannelConfigStereo); // TODO: Change to Mono fails some tests
f.m_audioGenerator.setFormat(audioFormat);
constexpr seconds expectedDuration = 1s;
f.m_audioGenerator.setDuration(expectedDuration);
f.m_audioGenerator.setFrequency(800);
f.start(RunMode::Pull, AutoStop::EmitEmpty);
const auto actualFormat = f.m_recorder.mediaFormat();
qDebug() << "Actual format: " << QMediaFormat::fileFormatName(actualFormat.fileFormat()) << ","
<< QMediaFormat::audioCodecName(actualFormat.audioCodec());
QVERIFY(f.waitForRecorderStopped(60s));
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().data());
const std::optional<MediaInfo> info = MediaInfo::create(f.m_recorder.actualLocation());
QVERIFY(info);
QCOMPARE_GE(info->m_audioBuffer.duration(),
std::chrono::microseconds(expectedDuration / 5).count()); // TODO: Fix cut audio
QCOMPARE_GE(info->m_audioBuffer.byteCount(), 1u); // TODO: Verify with QSineWaveValidator
}
// TODO: Add test that verifies format support with both audio and video in the same recording
void tst_QMediaRecorderBackend::record_emits_mediaformatChanged_whenFormatChanged()
{
QSKIP_IF_NOT_FFMPEG();
// Arrange
CaptureSessionFixture f{ StreamType::Video };
f.m_videoGenerator.setFrameCount(1);
f.m_videoGenerator.setSize({ 128, 64 }); // Small frames to speed up test
QMediaFormat unspecifiedFormat;
f.m_recorder.setMediaFormat(unspecifiedFormat);
f.start(RunMode::Pull, AutoStop::EmitEmpty);
QVERIFY(f.waitForRecorderStopped(60s));
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().data());
QCOMPARE_EQ(f.mediaFormatChanged.size(), 1);
const QMediaFormat actualFormat = f.m_recorder.mediaFormat();
QCOMPARE_NE(actualFormat.fileFormat(), QMediaFormat::UnspecifiedFormat);
QCOMPARE_NE(actualFormat.videoCodec(), QMediaFormat::VideoCodec::Unspecified);
QCOMPARE_NE(actualFormat.audioCodec(), QMediaFormat::AudioCodec::Unspecified);
}
void tst_QMediaRecorderBackend::stop_stopsRecording_whenInvokedUponRecordingStart()
{
QSKIP_IF_NOT_FFMPEG();
// Arrange
const QUrl url = QUrl::fromLocalFile(m_tempDir.filePath("any_file_name"));
CaptureSessionFixture f{ StreamType::AudioAndVideo };
f.m_recorder.setOutputLocation(url);
auto onStateChanged = [&f](QMediaRecorder::RecorderState state) {
if (state == QMediaRecorder::RecordingState)
f.m_recorder.stop();
};
connect(&f.m_recorder, &QMediaRecorder::recorderStateChanged, this, onStateChanged);
// Act
f.start(RunMode::Pull, AutoStop::No);
// Assert
QTRY_COMPARE(f.m_recorder.recorderState(), QMediaRecorder::StoppedState);
QList< QList<QVariant> > expectedRecorderStateChangedSignals( { { QMediaRecorder::RecordingState }, { QMediaRecorder::StoppedState } });
QCOMPARE(f.recorderStateChanged, expectedRecorderStateChangedSignals);
}
void tst_QMediaRecorderBackend::record_reflectsAudioEncoderSetting()
{
QSKIP_IF_NOT_FFMPEG();
// Arrange
CaptureSessionFixture f{ StreamType::Audio };
QAudioFormat audioFormat;
audioFormat.setSampleFormat(QAudioFormat::Float);
audioFormat.setChannelCount(2);
audioFormat.setSampleRate(44100);
f.m_audioGenerator.setFormat(audioFormat);
QMediaFormat fmt{ QMediaFormat::Wave };
fmt.setAudioCodec(QMediaFormat::AudioCodec::Wave);
f.m_recorder.setMediaFormat(fmt);
f.m_recorder.setAudioSampleRate(24000); // nonstandard sampling rate
f.m_recorder.setAudioChannelCount(1); // mono
// act
f.start(RunMode::Pull, AutoStop::EmitEmpty);
QVERIFY(f.waitForRecorderStopped(60s));
// Assert
auto info = MediaInfo::create(f.m_recorder.actualLocation());
QVERIFY(info);
QCOMPARE_EQ(info->m_audioBuffer.format().sampleRate(), 24000);
QCOMPARE_EQ(info->m_audioBuffer.format().channelCount(), 1);
}
void tst_QMediaRecorderBackend::record_adjustsFrameRate_whenSourceFrameRateDifferentFromEncoderFrameRate_data()
{
QTest::addColumn<double>("sourceFrameRate");
QTest::addColumn<double>("recorderFrameRate");
QTest::addColumn<bool>("reportsVariableRate");
QTest::addColumn<bool>("reportsEndTime");
QTest::addColumn<QMediaFormat::VideoCodec>("videoCodec");
struct TestCase {
const char *name;
double sourceRate;
double recorderRate;
};
const TestCase cases[] = {
{ "the default rate from 10.0", 10.0, 0.0 },
{ "the default rate from 59.94", 59.94, 0.0 },
{ "the default rate from 75.0", 75.0, 0.0 },
{ "the same rate", 10.0, 10.0 },
{ "a half rate", 20.0, 10.0 },
{ "a double rate", 10.0, 20.0 },
{ "a 2/3 rate", 15.0, 10.0 },
{ "a 3/2 rate", 10.0, 15.0 },
};
// TODO: Add cases for different video codecs
for (const auto &c : cases) {
for (bool variableRate : { false, true }) {
for (bool reportsEndTime : { true, false }) {
for (auto codec : QMediaFormat().supportedVideoCodecs(QMediaFormat::Encode)) {
// Skip MPEG1 due to its age and limited support of frame rates
if (codec == QMediaFormat::VideoCodec::MPEG1)
continue;
const char *prefix = variableRate ? "Encode variable stream at " : "Encode at ";
const char *suffix = reportsEndTime ? "" : " (invalid endTime)";
QTest::addRow("%s%s %s%s", prefix, c.name,
qPrintable(QMediaFormat::videoCodecName(codec)), suffix)
<< c.sourceRate << c.recorderRate << variableRate << reportsEndTime
<< codec;
}
}
}
}
}
void tst_QMediaRecorderBackend::record_adjustsFrameRate_whenSourceFrameRateDifferentFromEncoderFrameRate()
{
QSKIP_IF_NOT_FFMPEG();
// Arrange
QFETCH(double, sourceFrameRate);
QFETCH(double, recorderFrameRate);
QFETCH(bool, reportsVariableRate);
QFETCH(bool, reportsEndTime);
QFETCH(QMediaFormat::VideoCodec, videoCodec);
if (sourceFrameRate > 60.0 && recorderFrameRate <= 0.0 && !reportsVariableRate)
// Tries to set up codec with fps=75., which fails on MPEG. Skip for now
// TODO: We should detect this and adjust videoFrameRate
QSKIP("Codec fails when setting too high frame rate");
// Set up for 1s of frames
using namespace std::chrono;
const int sourceFrameCount = qFloor(sourceFrameRate);
const auto periodDuration = microseconds(std::lround(1e6 / sourceFrameRate));
const bool frameRateIsSet = recorderFrameRate > 0.0;
double targetFrameRate = frameRateIsSet ? recorderFrameRate : sourceFrameRate;
// Frame rate adapter active when the target rate is explicitly set or the source rate is known
bool adapterActive = frameRateIsSet || !reportsVariableRate;
// MPEG2 doesn't support variable rate, so falls back to 30fps
if (videoCodec == QMediaFormat::VideoCodec::MPEG2 && recorderFrameRate <= 0.0 && reportsVariableRate) {
targetFrameRate = 30.0;
adapterActive = true;
}
int expectedFrameCount = adapterActive ? qFloor(targetFrameRate) : sourceFrameCount;
// Adapter slot and epsilon mirror the adapter's internal values; used for expected frame count
// and source frame index calculation. Only used when adapterActive == true
const auto slotDuration = microseconds(static_cast<qint64>(1e6 / targetFrameRate));
const auto epsilon = reportsVariableRate
? 1000us
: microseconds(std::lround(periodDuration.count() * 0.05)); // 5% of source period
if (adapterActive && reportsVariableRate && !reportsEndTime) {
// We fill gaps in encoding slots after receiving frames, so expected frame count is different
if (targetFrameRate < sourceFrameRate) {
// Flush produces an extra frame only if the last source frame doesn't land on a slot boundary
const auto lastFrameTime = (sourceFrameCount - 1) * periodDuration;
if (lastFrameTime % slotDuration > epsilon)
expectedFrameCount++;
} else {
int copies = qFloor(targetFrameRate / sourceFrameRate);
if (copies > 1)
expectedFrameCount -= copies - 1; // Flushing ensures only one encoding slot for the last frame
}
}
const std::array<QColor, 5> sourceColors = { Qt::red, Qt::green, Qt::blue, Qt::yellow, Qt::magenta };
std::vector<QColor> expectedColors;
expectedColors.reserve(expectedFrameCount);
for (int i = 0; i < expectedFrameCount; ++i) {
// Source frame index: passthrough = direct; adapter active = slot-aligned, clamped
const int sourceIndex = adapterActive
? qMin(static_cast<int>((i * slotDuration + epsilon) / periodDuration),
sourceFrameCount - 1)
: i;
expectedColors.push_back(sourceColors[sourceIndex % static_cast<int>(sourceColors.size())]);
}
CaptureSessionFixture f{ StreamType::Video };
if (frameRateIsSet)
f.m_recorder.setVideoFrameRate(recorderFrameRate);
QMediaFormat fmt;
fmt.setVideoCodec(videoCodec);
f.m_recorder.setMediaFormat(fmt);
if (!reportsVariableRate)
f.m_videoGenerator.setFrameRate(sourceFrameRate);
f.m_videoGenerator.setPeriod(periodDuration);
f.m_videoGenerator.setEndTimeReporting(reportsEndTime);
f.m_videoGenerator.setFrameCount(sourceFrameCount);
f.m_videoGenerator.setSize({ 128, 64 });
f.m_videoGenerator.setColors(sourceColors);
// Act
f.start(RunMode::Pull, AutoStop::EmitEmpty);
QVERIFY(f.waitForRecorderStopped(60s));
// Assert
auto actualFormat = f.m_recorder.mediaFormat();
if (f.m_recorder.error() != QMediaRecorder::NoError
&& unsupportedVideoCodecs(actualFormat.fileFormat()).count(actualFormat.videoCodec()))
QEXPECT_FAIL("", "QTBUG-126276", Abort);
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().constData());
const auto info = MediaInfo::create(f.m_recorder.actualLocation());
QVERIFY(info);
QCOMPARE_GE(info->m_duration, 900ms);
QCOMPARE_LE(info->m_duration, 1100ms);
verifyEncodedFrames(*info, expectedColors);
}
void tst_QMediaRecorderBackend::record_writesLastFrame_whenAdapterHadBufferedItBeforeEndOfStream()
{
QSKIP_IF_NOT_FFMPEG();
// Arrange
CaptureSessionFixture f{ StreamType::Video };
f.m_recorder.setVideoFrameRate(10.0);
f.m_videoGenerator.setSize({ 64, 64 });
// Act
f.start(RunMode::Push, AutoStop::EmitEmpty);
f.readyToSendVideoFrame.wait();
// Frame 1 at t=0: emitted immediately for slot 0
{
QVideoFrame frame = f.m_videoGenerator.createFrame();
frame.setStartTime(0);
QVERIFY(f.m_videoInput.sendVideoFrame(frame));
f.readyToSendVideoFrame.wait();
}
// Frame 2 at t=60ms: before the 100ms slot, no endTime -> adapter buffers without emitting
{
QVideoFrame frame = f.m_videoGenerator.createFrame();
frame.setStartTime(60'000); // µs
QVERIFY(f.m_videoInput.sendVideoFrame(frame));
f.readyToSendVideoFrame.wait();
}
// EOS: flush emits the buffered frame before the encoder stops
f.m_videoInput.sendVideoFrame({});
// Assert
QVERIFY(f.waitForRecorderStopped(60s));
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().constData());
const auto info = MediaInfo::create(f.m_recorder.actualLocation());
QVERIFY(info);
QCOMPARE_EQ(info->m_frameCount, 2);
}
void tst_QMediaRecorderBackend::record_encodesFrames_whenSourceTimestampsAreIrregular_data()
{
QTest::addColumn<double>("frameRate");
QTest::addColumn<std::vector<QColor>>("expectedColors");
// Source frames cycle through Red, Green, Blue, Yellow, Magenta
// with irregular periods (see test).
QTest::addRow("Default frame rate")
<< 0.0 << std::vector<QColor>{ Qt::red, Qt::green, Qt::blue, Qt::yellow,
Qt::magenta, Qt::red, Qt::green, Qt::blue };
// 100ms period, 10 frames
QTest::addRow("Fixed frame rate (10 fps)")
<< 10.0
<< std::vector<QColor>{ Qt::red, Qt::green, Qt::blue, Qt::yellow, Qt::yellow,
Qt::magenta, Qt::red, Qt::green, Qt::green, Qt::blue };
// 200ms period, 5 frames + flushed last frame
QTest::addRow("Fixed frame rate (5fps)")
<< 5.0
<< std::vector<QColor>{ Qt::red, Qt::blue, Qt::yellow, Qt::red, Qt::green, Qt::blue };
}
void tst_QMediaRecorderBackend::record_encodesFrames_whenSourceTimestampsAreIrregular()
{
QSKIP_IF_NOT_FFMPEG();
// Arrange
QFETCH(double, frameRate);
QFETCH(std::vector<QColor>, expectedColors);
// Irregular start times spanning 1s
// 0 red
// 45 green
// 180 blue
// 225 yellow
// 410 magenta
// 600 red
// 650 green
// 900 blue
using namespace std::chrono;
const std::array<microseconds, 8> startTimes = { 0ms, 45ms, 180ms, 225ms,
410ms, 600ms, 650ms, 900ms };
CaptureSessionFixture f{ StreamType::Video };
f.m_videoGenerator.setSize({ 64, 64 });
f.m_videoGenerator.setColors({ Qt::red, Qt::green, Qt::blue, Qt::yellow, Qt::magenta });
if (frameRate > 0.0)
f.m_recorder.setVideoFrameRate(frameRate);
QSignalSpy frameCreated{ &f.m_videoGenerator, &VideoGenerator::frameCreated };
// Act
f.start(RunMode::Push, AutoStop::EmitEmpty);
// Disconnect frameCreated from video input so the test can override each frame's timestamp
// before pushing it manually.
QObject::disconnect(&f.m_videoGenerator, &VideoGenerator::frameCreated, &f.m_videoInput,
&QVideoFrameInput::sendVideoFrame);
f.readyToSendVideoFrame.wait();
for (microseconds startTime : startTimes) {
f.m_videoGenerator.nextFrame(); // advances the source frame index/color, emits frameCreated
QVideoFrame frame = frameCreated.takeFirst().at(0).value<QVideoFrame>();
frame.setStartTime(startTime.count());
QVERIFY(f.m_videoInput.sendVideoFrame(frame));
f.readyToSendVideoFrame.wait();
}
f.m_videoInput.sendVideoFrame({}); // EOS
// Assert
QVERIFY(f.waitForRecorderStopped(60s));
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().constData());
const auto info = MediaInfo::create(f.m_recorder.actualLocation());
QVERIFY(info);
verifyEncodedFrames(*info, expectedColors);
}
void tst_QMediaRecorderBackend::record_dropsPausedFramesAndPauseGap_whenRecordingIsResumed_data()
{
QTest::addColumn<bool>("bypassFrameInput");
// A source that follows the readyToSendVideoFrame protocol is refused by QVideoFrameInput
// while paused, and the frame never reaches the encoder
QTest::addRow("Frame sent through the video frame input") << false;
// A source that pushes frames regardless of the permission, like a camera or a screen
// capture, reaches the encoder while paused and is dropped there
QTest::addRow("Frame pushed directly to the encoder") << true;
}
void tst_QMediaRecorderBackend::record_dropsPausedFramesAndPauseGap_whenRecordingIsResumed()
{
QSKIP_IF_NOT_FFMPEG();
QFETCH(const bool, bypassFrameInput);
// Arrange
using namespace std::chrono;
CaptureSessionFixture f{ StreamType::Video };
f.m_videoGenerator.setSize({ 256, 256 });
f.m_videoGenerator.setColors({ Qt::red, Qt::green, Qt::blue, Qt::yellow, Qt::magenta });
// Pin the codec to avoid reordering
QMediaFormat format;
format.setVideoCodec(QMediaFormat::VideoCodec::MPEG4);
f.m_recorder.setMediaFormat(format);
QSignalSpy frameCreated{ &f.m_videoGenerator, &VideoGenerator::frameCreated };
// Act
f.start(RunMode::Push, AutoStop::EmitEmpty);
// Disconnect frameCreated from video input so the test can override each frame's timestamp
// before pushing it manually.
QObject::disconnect(&f.m_videoGenerator, &VideoGenerator::frameCreated, &f.m_videoInput,
&QVideoFrameInput::sendVideoFrame);
f.readyToSendVideoFrame.wait();
// Advances the source frame index/color and timestamps the created frame
constexpr microseconds frameDuration = 200ms;
const auto nextFrame = [&](microseconds startTime) {
f.m_videoGenerator.nextFrame();
QVideoFrame frame = frameCreated.takeFirst().at(0).value<QVideoFrame>();
frame.setStartTime(startTime.count());
frame.setEndTime((startTime + frameDuration).count());
return frame;
};
// Red at t=0 and green at t=200ms are recorded
QVERIFY(f.m_videoInput.sendVideoFrame(nextFrame(0ms)));
f.readyToSendVideoFrame.wait();
QVERIFY(f.m_videoInput.sendVideoFrame(nextFrame(200ms)));
f.readyToSendVideoFrame.wait();
f.m_recorder.pause();
QTRY_COMPARE(f.m_recorder.recorderState(), QMediaRecorder::PausedState);
// Blue is not recorded, either because the frame input refuses it or because the encoder
// drops it
if (bypassFrameInput) {
// HACK: Reach the encoder the way a camera or a screen capture does, without asking the
// frame input for permission first
QPlatformMediaCaptureSession *platformSession = f.m_session.platformSession();
QVERIFY(platformSession);
const std::vector<QPlatformVideoSource *> sources = platformSession->activeVideoSources();
if (sources.size() < 1)
QSKIP("No video sources active");
emit sources.front()->newVideoFrame(nextFrame(400ms));
} else {
QVERIFY(!f.m_videoInput.sendVideoFrame(nextFrame(400ms)));
}
f.readyToSendVideoFrame.clear();
f.m_recorder.record(); // Resumes recording
QTRY_COMPARE(f.m_recorder.recorderState(), QMediaRecorder::RecordingState);
QTRY_VERIFY(!f.readyToSendVideoFrame.isEmpty());
// Yellow at t=5000 and magenta at t=5200 are recorded
QVERIFY(f.m_videoInput.sendVideoFrame(nextFrame(5000ms)));
f.readyToSendVideoFrame.wait();
QVERIFY(f.m_videoInput.sendVideoFrame(nextFrame(5200ms)));
f.readyToSendVideoFrame.wait();
f.m_videoInput.sendVideoFrame({}); // EOS
// Assert
QVERIFY(f.waitForRecorderStopped(60s));
QVERIFY2(f.m_recorder.error() == QMediaRecorder::NoError,
f.m_recorder.errorString().toLatin1().constData());
const auto info = MediaInfo::create(f.m_recorder.actualLocation());
QVERIFY(info);
// Blue is missing because it was sent while paused
verifyEncodedFrames(*info, { Qt::red, Qt::green, Qt::yellow, Qt::magenta });
// The 4.6s pause is not part of the recording: the four frames are 200ms apart
QVERIFY2(info->m_duration < 2s,
qPrintable(QString("Recording duration %1ms includes the pause")
.arg(info->m_duration.count())));
}
QTEST_MAIN(tst_QMediaRecorderBackend)
#include "tst_qmediarecorderbackend.moc"