From e371cc4dd9fa2d1000800800efa5dd876aaf1eca Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 20:48:22 +0100 Subject: [PATCH] Cover audio tail pause, replay, seeking and queued EOF callbacks --- tools/chatgui/CMakeLists.txt | 2 +- tools/chatgui/tests/test_video_playback.cpp | 116 ++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 6226ab66..1e9ec195 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -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") diff --git a/tools/chatgui/tests/test_video_playback.cpp b/tools/chatgui/tests/test_video_playback.cpp index 55cf33c4..94005181 100644 --- a/tools/chatgui/tests/test_video_playback.cpp +++ b/tools/chatgui/tests/test_video_playback.cpp @@ -10,13 +10,44 @@ #include extern "C" { #include "debug_config.h" +#ifdef TEST_NULL_AUDIO +#include +#endif } #ifdef TEST_NULL_AUDIO +static std::atomic originalAudioCallback{nullptr}; +static std::atomic nonzeroSamples{0}; +static std::atomic failAudioDecoder{false}; +static std::atomic 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(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("name"); QTest::addColumn("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(); QVERIFY(engine); QSignalSpy frames(engine, &VideoPlayerEngine::framePresented), closed(&window, &VideoPlayerWindow::closed);