1+ #include " settings_manager.hpp"
12#include " video_decoder.hpp"
23#include " video_encoder.hpp"
34#include " video_frame_store.hpp"
45#include < QElapsedTimer>
56#include < QFile>
7+ #include < QScopeGuard>
68#include < QSignalSpy>
79#include < QTemporaryDir>
810#include < QTest>
@@ -20,6 +22,8 @@ namespace {
2022constexpr int kTestFrameCount = 60 ;
2123constexpr int kTestGopSize = 12 ;
2224constexpr int kColdFrame = 48 ;
25+ constexpr int kEvictionFrameCount = 240 ;
26+ constexpr qsizetype kEvictionCacheBytes = 2 * 1024 * 1024 ;
2327} // namespace
2428
2529class TestVideoDecoder : public QObject {
@@ -28,13 +32,14 @@ class TestVideoDecoder : public QObject {
2832 private slots:
2933 void initialStateIsEmpty ();
3034 void decodesEncodedFramesThroughVideoSink ();
35+ void frameCacheEvictionStaysWithinBudget ();
3136
3237 private:
33- static bool createTestVideo (const QString &path);
38+ static bool createTestVideo (const QString &path, int frameCount = kTestFrameCount );
3439 static QVector3D averageColor (const QImage &image);
3540};
3641
37- bool TestVideoDecoder::createTestVideo (const QString &path) {
42+ bool TestVideoDecoder::createTestVideo (const QString &path, int frameCount ) {
3843 VideoEncoder encoder;
3944 VideoEncoder::Config config;
4045 config.width = 96 ;
@@ -50,7 +55,7 @@ bool TestVideoDecoder::createTestVideo(const QString &path) {
5055 return false ;
5156
5257 const std::array<QColor, 4 > colors = {QColor (Qt::red), QColor (Qt::yellow), QColor (Qt::cyan), QColor (Qt::blue)};
53- for (int frame = 0 ; frame < kTestFrameCount ; ++frame) {
58+ for (int frame = 0 ; frame < frameCount ; ++frame) {
5459 QImage image (config.width , config.height , QImage::Format_RGBA8888);
5560 image.fill (colors[static_cast <std::size_t >(frame) % colors.size ()]);
5661 if (!encoder.pushFrame (image, frame)) {
@@ -214,5 +219,108 @@ void TestVideoDecoder::decodesEncodedFramesThroughVideoSink() {
214219 QCOMPARE (burstFrameSpy.last ().first ().toInt (), 36 );
215220}
216221
222+ void TestVideoDecoder::frameCacheEvictionStaysWithinBudget () {
223+ SettingsManager &settings = SettingsManager::instance ();
224+ const QVariant previousOverride = settings.value (QStringLiteral (" _videoDecoderFrameCacheMaxBytes" ));
225+ settings.setValue (QStringLiteral (" _videoDecoderFrameCacheMaxBytes" ), kEvictionCacheBytes );
226+ const auto restoreCacheOverride = qScopeGuard ([&settings, previousOverride]() -> void {
227+ if (previousOverride.isValid ()) {
228+ settings.setValue (QStringLiteral (" _videoDecoderFrameCacheMaxBytes" ), previousOverride);
229+ } else {
230+ settings.removeValue (QStringLiteral (" _videoDecoderFrameCacheMaxBytes" ));
231+ }
232+ });
233+
234+ QTemporaryDir dir;
235+ QVERIFY (dir.isValid ());
236+ const QString videoPath = dir.filePath (QStringLiteral (" decoder-eviction.mp4" ));
237+ QVERIFY (createTestVideo (videoPath, kEvictionFrameCount ));
238+
239+ constexpr int clipId = 8 ;
240+ VideoFrameStore store;
241+ QVideoSink sink;
242+ store.registerSink (QString::number (clipId), &sink);
243+
244+ VideoDecoder decoder (clipId, QUrl::fromLocalFile (videoPath), &store);
245+ QSignalSpy readySpy (&decoder, &MediaDecoder::ready);
246+ decoder.scheduleStart ();
247+ QTRY_COMPARE_WITH_TIMEOUT (readySpy.count (), 1 , 10'000 );
248+ QVERIFY (decoder.isReady ());
249+ QCOMPARE (decoder.totalFrameCount (), kEvictionFrameCount );
250+
251+ const std::array<QColor, 4 > expectedColors = {QColor (Qt::red), QColor (Qt::yellow), QColor (Qt::cyan), QColor (Qt::blue)};
252+ auto seekAndVerify = [&decoder, &sink, &expectedColors](int frame) -> qint64 {
253+ QSignalSpy frameSpy (&decoder, &MediaDecoder::frameReady);
254+ QElapsedTimer timer;
255+ timer.start ();
256+ decoder.seekToFrame (frame, decoder.sourceFps ());
257+ if (frameSpy.isEmpty () && !frameSpy.wait (10'000 )) {
258+ return -1 ;
259+ }
260+ if (frameSpy.count () != 1 || frameSpy.takeFirst ().at (0 ).toInt () != frame) {
261+ return -1 ;
262+ }
263+ const QImage image = sink.videoFrame ().toImage ();
264+ if (image.isNull ()) {
265+ return -1 ;
266+ }
267+ const QVector3D actualColor = averageColor (image);
268+ const QColor expectedColor = expectedColors[static_cast <std::size_t >(frame) % expectedColors.size ()];
269+ const QVector3D expectedColorVector (static_cast <float >(expectedColor.red ()), static_cast <float >(expectedColor.green ()), static_cast <float >(expectedColor.blue ()));
270+ if ((actualColor - expectedColorVector).length () >= 80 .0F ) {
271+ return -1 ;
272+ }
273+ return timer.elapsed ();
274+ };
275+
276+ QVector<qint64> coldSeekTimes;
277+ constexpr int gopCount = kEvictionFrameCount / kTestGopSize ;
278+ coldSeekTimes.reserve (gopCount);
279+ for (int gop = gopCount - 1 ; gop >= 0 ; --gop) {
280+ const int frame = (gop * kTestGopSize ) + (((gop * 5 ) + 1 ) % kTestGopSize );
281+ const qint64 elapsedMs = seekAndVerify (frame);
282+ QVERIFY2 (elapsedMs >= 0 , qPrintable (QStringLiteral (" cold seek to frame %1 failed" ).arg (frame)));
283+ const int completedSeeks = gopCount - gop;
284+ QTRY_COMPARE_WITH_TIMEOUT (decoder.cacheStats ().decodedFrames , static_cast <quint64>(completedSeeks * kTestGopSize ), 10'000 );
285+ QTRY_COMPARE_WITH_TIMEOUT (decoder.cacheStats ().gopEvictions , static_cast <quint64>(std::max (0 , completedSeeks - 3 )), 10'000 );
286+ coldSeekTimes.append (elapsedMs);
287+ }
288+
289+ const VideoDecoder::CacheStats sweepStats = decoder.cacheStats ();
290+ std::ranges::sort (coldSeekTimes);
291+ const qint64 medianColdSeekMs = coldSeekTimes.at (coldSeekTimes.size () / 2 );
292+ QTextStream (stdout) << " video_decoder eviction_sweep seeks=" << gopCount << " median_ms=" << medianColdSeekMs << " misses=" << sweepStats.misses << " decoded_frames=" << sweepStats.decodedFrames << " gop_blocks=" << sweepStats.gopBlocks
293+ << " gop_evictions=" << sweepStats.gopEvictions << " frame_entries=" << sweepStats.frameEntries << " frame_cost=" << sweepStats.frameCost << " frame_max_cost=" << sweepStats.frameMaxCost << Qt::endl;
294+ QCOMPARE (sweepStats.misses , quint64{gopCount});
295+ QCOMPARE (sweepStats.decodedFrames , quint64{kEvictionFrameCount });
296+ QCOMPARE (sweepStats.gopBlocks , 3 );
297+ QCOMPARE (sweepStats.gopEvictions , quint64{gopCount - 3 });
298+ QCOMPARE (sweepStats.frameMaxCost , kEvictionCacheBytes );
299+ QVERIFY (sweepStats.frameCost <= sweepStats.frameMaxCost );
300+ QVERIFY (sweepStats.frameEntries < kEvictionFrameCount );
301+
302+ constexpr int frameCacheHitTarget = 47 ;
303+ const qint64 frameCacheHitMs = seekAndVerify (frameCacheHitTarget);
304+ QVERIFY (frameCacheHitMs >= 0 );
305+ const VideoDecoder::CacheStats hitStats = decoder.cacheStats ();
306+ QTextStream (stdout) << " video_decoder frame_cache_reseek frame=" << frameCacheHitTarget << " elapsed_ms=" << frameCacheHitMs << " misses=" << hitStats.misses << " gop_hits=" << hitStats.gopHits << " frame_hits=" << hitStats.frameHits << " decoded_frames=" << hitStats.decodedFrames << Qt::endl;
307+ QCOMPARE (hitStats.misses , sweepStats.misses );
308+ QCOMPARE (hitStats.gopHits , sweepStats.gopHits );
309+ QCOMPARE (hitStats.frameHits , sweepStats.frameHits + 1 );
310+ QCOMPARE (hitStats.decodedFrames , sweepStats.decodedFrames );
311+
312+ constexpr int evictedFrameTarget = kEvictionFrameCount - 1 ;
313+ const qint64 evictedReseekMs = seekAndVerify (evictedFrameTarget);
314+ QVERIFY (evictedReseekMs >= 0 );
315+ QTRY_COMPARE_WITH_TIMEOUT (decoder.cacheStats ().decodedFrames , hitStats.decodedFrames + kTestGopSize , 10'000 );
316+ const VideoDecoder::CacheStats evictionStats = decoder.cacheStats ();
317+ QTextStream (stdout) << " video_decoder evicted_reseek frame=" << evictedFrameTarget << " elapsed_ms=" << evictedReseekMs << " misses=" << evictionStats.misses << " frame_hits=" << evictionStats.frameHits << " decoded_frames=" << evictionStats.decodedFrames << " frame_entries=" << evictionStats.frameEntries
318+ << " frame_cost=" << evictionStats.frameCost << Qt::endl;
319+ QCOMPARE (evictionStats.misses , hitStats.misses + 1 );
320+ QCOMPARE (evictionStats.frameHits , hitStats.frameHits );
321+ QCOMPARE (evictionStats.decodedFrames , hitStats.decodedFrames + kTestGopSize );
322+ QVERIFY (evictionStats.frameCost <= evictionStats.frameMaxCost );
323+ }
324+
217325QTEST_MAIN (TestVideoDecoder)
218326#include " test_video_decoder.moc"
0 commit comments