Cover audio tail pause, replay, seeking and queued EOF callbacks
This commit is contained in:
1 parent
ab37a06dad
commit
e371cc4dd9
2 files changed
+117
-1
No files matched your search
@@ -887,7 +887,7 @@ if(FFMPEG_FOUND)
|
||||
${FFMPEG_TARGET} ${FFMPEG_LIBRARIES})
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
target_compile_definitions(test_video_playback PRIVATE TEST_NULL_AUDIO)
|
||||
target_link_options(test_video_playback PRIVATE "-Wl,--wrap=ma_context_init")
|
||||
target_link_options(test_video_playback PRIVATE "-Wl,--wrap=ma_context_init" "-Wl,--wrap=ma_device_init" "-Wl,--wrap=avcodec_open2")
|
||||
endif()
|
||||
add_test(NAME test_video_playback COMMAND test_video_playback)
|
||||
set_tests_properties(test_video_playback PROPERTIES TIMEOUT 45 ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
|
||||
@@ -10,13 +10,44 @@
|
||||
#include <atomic>
|
||||
extern "C" {
|
||||
#include "debug_config.h"
|
||||
#ifdef TEST_NULL_AUDIO
|
||||
#include <libavcodec/avcodec.h>
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef TEST_NULL_AUDIO
|
||||
static std::atomic<ma_device_data_proc> originalAudioCallback{nullptr};
|
||||
static std::atomic<uint64_t> nonzeroSamples{0};
|
||||
static std::atomic<bool> failAudioDecoder{false};
|
||||
static std::atomic<unsigned> rejectedAudioDecoders{0};
|
||||
extern "C" ma_result __real_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context*);
|
||||
extern "C" ma_result __real_ma_device_init(ma_context*, const ma_device_config*, ma_device*);
|
||||
extern "C" int __real_avcodec_open2(AVCodecContext*, const AVCodec*, AVDictionary**);
|
||||
extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context* context) {
|
||||
const ma_backend backend = ma_backend_null; return __real_ma_context_init(&backend, 1, nullptr, context);
|
||||
}
|
||||
static void observeAudio(ma_device* device, void* out, const void* in, ma_uint32 frames) {
|
||||
originalAudioCallback.load()(device, out, in, frames);
|
||||
const auto* pcm = static_cast<const float*>(out);
|
||||
uint64_t nonzero = 0;
|
||||
for (unsigned i = 0; i < frames * 2; ++i) nonzero += pcm[i] != 0;
|
||||
nonzeroSamples.fetch_add(nonzero);
|
||||
}
|
||||
extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_config* config, ma_device* device) {
|
||||
ma_device_config copy = *config;
|
||||
if (copy.deviceType == ma_device_type_playback && copy.playback.format == ma_format_f32 && copy.playback.channels == 2) {
|
||||
originalAudioCallback = copy.dataCallback; copy.dataCallback = observeAudio;
|
||||
}
|
||||
return __real_ma_device_init(context, ©, device);
|
||||
}
|
||||
extern "C" int __wrap_avcodec_open2(AVCodecContext* context, const AVCodec* codec, AVDictionary** options) {
|
||||
if (failAudioDecoder && context->codec_type == AVMEDIA_TYPE_AUDIO) {
|
||||
++rejectedAudioDecoders;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "fixture: refusing audio decoder codec=%d", context->codec_id);
|
||||
return AVERROR_DECODER_NOT_FOUND;
|
||||
}
|
||||
return __real_avcodec_open2(context, codec, options);
|
||||
}
|
||||
#endif
|
||||
|
||||
class VideoPlaybackTest : public QObject {
|
||||
@@ -89,6 +120,13 @@ private slots:
|
||||
void seekAndReopenDiscardQueuedEof() {
|
||||
VideoPlayerEngine engine; QSignalSpy finished(&engine, &VideoPlayerEngine::finished);
|
||||
QSignalSpy errors(&engine, &VideoPlayerEngine::error);
|
||||
bool firstOpen = true;
|
||||
connect(&engine, &VideoPlayerEngine::opened, &engine, [&] {
|
||||
if (!firstOpen) return;
|
||||
firstOpen = false;
|
||||
QTest::qSleep(40); // Короткий файл уже декодирован, но его EOF ещё стоит в GUI-очереди.
|
||||
engine.seekMs(300); engine.seekMs(0);
|
||||
});
|
||||
engine.open(video("short")); QTest::qWait(60);
|
||||
for (qint64 position : {300, 0, 250, 40, 0}) engine.seekMs(position);
|
||||
QCOMPARE(engine.state(), VideoPlayerEngine::Playing);
|
||||
@@ -98,6 +136,84 @@ private slots:
|
||||
engine.seekMs(3800); QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 1500); QCOMPARE(errors.count(), 0);
|
||||
}
|
||||
|
||||
void earlySeekUsesRequestedClock() {
|
||||
VideoPlayerEngine engine; QSignalSpy finished(&engine, &VideoPlayerEngine::finished);
|
||||
QSignalSpy errors(&engine, &VideoPlayerEngine::error), frames(&engine, &VideoPlayerEngine::framePresented);
|
||||
connect(&engine, &VideoPlayerEngine::opened, &engine, [&] { engine.seekMs(3800); });
|
||||
engine.open(video("long")); QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 1500);
|
||||
QCOMPARE(engine.positionMs(), qint64(4000)); QVERIFY(frames.count() > 0); QCOMPARE(errors.count(), 0);
|
||||
}
|
||||
|
||||
void finishedCallbackCanOpenAndClose() {
|
||||
VideoPlayerEngine engine; QSignalSpy finished(&engine, &VideoPlayerEngine::finished);
|
||||
QSignalSpy errors(&engine, &VideoPlayerEngine::error);
|
||||
connect(&engine, &VideoPlayerEngine::finished, &engine, [&] {
|
||||
QCOMPARE(engine.state(), VideoPlayerEngine::Finished); QCOMPARE(engine.positionMs(), qint64(400));
|
||||
if (finished.count() == 1) engine.open(video("short"));
|
||||
else engine.close();
|
||||
});
|
||||
engine.open(video("short")); QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 2, 2500);
|
||||
QCOMPARE(engine.state(), VideoPlayerEngine::Stopped); QCOMPARE(engine.positionMs(), qint64(0));
|
||||
QTest::qWait(100); QCOMPARE(finished.count(), 2); QCOMPARE(errors.count(), 0);
|
||||
}
|
||||
|
||||
#ifdef TEST_NULL_AUDIO
|
||||
void audioPauseAndReplay_data() {
|
||||
QTest::addColumn<QString>("name"); QTest::addColumn<bool>("audioTail");
|
||||
QTest::newRow("audio-longer") << "audio-longer" << true;
|
||||
QTest::newRow("video-longer") << "video-longer" << false;
|
||||
}
|
||||
void audioPauseAndReplay() {
|
||||
QFETCH(QString, name); QFETCH(bool, audioTail);
|
||||
VideoPlayerEngine engine; QSignalSpy finished(&engine, &VideoPlayerEngine::finished);
|
||||
QSignalSpy errors(&engine, &VideoPlayerEngine::error);
|
||||
const uint64_t before = nonzeroSamples;
|
||||
engine.open(video(qPrintable(name))); QTRY_VERIFY_WITH_TIMEOUT(engine.positionMs() >= 650, 2000);
|
||||
QVERIFY(engine.hasAudio()); QVERIFY(nonzeroSamples > before);
|
||||
engine.pause(); QCOMPARE(engine.state(), VideoPlayerEngine::Paused);
|
||||
const qint64 position = engine.positionMs();
|
||||
QTest::qWait(60); const uint64_t silent = nonzeroSamples;
|
||||
QTest::qWait(160); QCOMPARE(nonzeroSamples.load(), silent); QCOMPARE(engine.positionMs(), position);
|
||||
QCOMPARE(finished.count(), 0); engine.play();
|
||||
QTest::qWait(150);
|
||||
if (audioTail) QVERIFY(nonzeroSamples > silent);
|
||||
else QCOMPARE(nonzeroSamples.load(), silent); // Возобновление видео не перезапускает уже окончившееся аудио.
|
||||
QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 1500);
|
||||
const uint64_t ended = nonzeroSamples;
|
||||
engine.togglePlayPause(); QTRY_VERIFY_WITH_TIMEOUT(nonzeroSamples > ended, 1000);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 2, 2000);
|
||||
QCOMPARE(engine.positionMs(), qint64(1200)); QCOMPARE(errors.count(), 0);
|
||||
}
|
||||
|
||||
void seekBackAfterAudioEnded() {
|
||||
VideoPlayerEngine engine; QSignalSpy finished(&engine, &VideoPlayerEngine::finished);
|
||||
QSignalSpy errors(&engine, &VideoPlayerEngine::error);
|
||||
engine.open(video("video-longer")); QTRY_VERIFY_WITH_TIMEOUT(engine.positionMs() >= 650, 2000);
|
||||
const uint64_t ended = nonzeroSamples;
|
||||
engine.seekMs(0); QTRY_VERIFY_WITH_TIMEOUT(nonzeroSamples > ended, 1000);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 2000);
|
||||
QCOMPARE(engine.positionMs(), qint64(1200)); QCOMPARE(errors.count(), 0);
|
||||
}
|
||||
|
||||
void audioDecoderFailureUsesVideoClock() {
|
||||
VideoPlayerEngine engine; QSignalSpy finished(&engine, &VideoPlayerEngine::finished);
|
||||
QSignalSpy errors(&engine, &VideoPlayerEngine::error);
|
||||
rejectedAudioDecoders = 0; failAudioDecoder = true;
|
||||
const auto restore = qScopeGuard([] { failAudioDecoder = false; });
|
||||
engine.open(video("audio-longer")); QTRY_COMPARE_WITH_TIMEOUT(finished.count(), 1, 2000);
|
||||
QCOMPARE(rejectedAudioDecoders.load(), 1u); QVERIFY(!engine.hasAudio());
|
||||
QCOMPARE(engine.positionMs(), qint64(400)); QCOMPARE(errors.count(), 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
void missingFileStopsWithoutFinishing() {
|
||||
VideoPlayerEngine engine; QSignalSpy finished(&engine, &VideoPlayerEngine::finished);
|
||||
QSignalSpy errors(&engine, &VideoPlayerEngine::error);
|
||||
engine.open(video("missing")); QTRY_COMPARE_WITH_TIMEOUT(errors.count(), 1, 1000);
|
||||
QCOMPARE(finished.count(), 0); QCOMPARE(engine.state(), VideoPlayerEngine::Stopped);
|
||||
QVERIFY(engine.currentFrame().isNull()); QVERIFY(!engine.hasAudio());
|
||||
}
|
||||
|
||||
void windowClosesOnlyAfterPlayback() {
|
||||
VideoPlayerWindow window; auto* engine = window.findChild<VideoPlayerEngine*>(); QVERIFY(engine);
|
||||
QSignalSpy frames(engine, &VideoPlayerEngine::framePresented), closed(&window, &VideoPlayerWindow::closed);
|
||||
|
||||
Reference in new issue
Block a user