From 74b17685f7570f5c864e20a7928aa1b139e82e74 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 8 Sep 2026 01:00:38 +0300 Subject: [PATCH] =?UTF-8?q?chatgui:=20=D1=84=D0=B8=D0=BA=D1=81=20=D1=81?= =?UTF-8?q?=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BA=D0=B0=D0=B4=D1=80=D0=BE=D0=B2=20(=D1=87?= =?UTF-8?q?=D0=B0=D1=81=D1=8B=20=D0=BD=D0=B5=20=D1=83=D0=B1=D0=B5=D0=B3?= =?UTF-8?q?=D0=B0=D1=8E=D1=82=20=D0=B2=D0=BF=D0=B5=D1=80=D1=91=D0=B4=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=B1=D1=8B=D1=81=D1=82=D1=80=D0=BE=D0=BC?= =?UTF-8?q?=20=D0=B4=D0=B5=D0=BA=D0=BE=D0=B4=D0=B5)=20+=20smoke-=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=20=D0=B4=D0=B2=D0=B8=D0=B6=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - presentTick: кадры всегда по мастер-часам, завершение только когда очередь пуста - clockMs: по окончании аудио часы в конец (хвостовые кадры не теряются) - fallback на системные часы, если аудио-устройство недоступно - tests/test_video_engine: smoke-тест FFmpeg-декодера (opened/frames/seek) --- tools/chatgui/CMakeLists.txt | 19 +++++++++ tools/chatgui/src/videoplayer_engine.cpp | 26 ++++++++----- tools/chatgui/src/videoplayer_window.cpp | 9 +++++ tools/chatgui/src/videoplayer_window.h | 1 + tools/chatgui/tests/test_video_engine.cpp | 47 +++++++++++++++++++++++ 5 files changed, 93 insertions(+), 9 deletions(-) create mode 100644 tools/chatgui/tests/test_video_engine.cpp diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 3265cf5d..937ddbf0 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -181,3 +181,22 @@ add_executable(test_config_updater target_include_directories(test_config_updater PRIVATE ${CMAKE_SOURCE_DIR}) target_link_libraries(test_config_updater PRIVATE ${QT_CORE} pthread) add_test(NAME test_config_updater COMMAND test_config_updater) + +# smoke-тест видео-декодера (FFmpeg). Ручной запуск: test_video_engine +add_executable(test_video_engine + tests/test_video_engine.cpp + src/videoplayer_engine.cpp + src/sound_manager.cpp + transport/miniaudio_impl.c +) +target_include_directories(test_video_engine PRIVATE ${CMAKE_SOURCE_DIR}/../../lib ${CMAKE_SOURCE_DIR}/../../src ${CMAKE_SOURCE_DIR}/db ${CMAKE_SOURCE_DIR}/../../lib/libopus/include ${FFMPEG_INCLUDE_DIRS}) +target_compile_definitions(test_video_engine PRIVATE USE_SQLITE SQLITE_THREADSAFE=1) +if(FFMPEG_FOUND) + target_compile_definitions(test_video_engine PRIVATE HAVE_FFMPEG) +endif() +set_source_files_properties(transport/miniaudio_impl.c PROPERTIES LANGUAGE C) +if(WIN32) + target_link_libraries(test_video_engine PRIVATE ${QT_LIBS} ${QT_CORE} utun pthread ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES}) +else() + target_link_libraries(test_video_engine PRIVATE ${QT_LIBS} ${QT_CORE} utun pthread dl ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES}) +endif() diff --git a/tools/chatgui/src/videoplayer_engine.cpp b/tools/chatgui/src/videoplayer_engine.cpp index 2885d5ff..3ac826f7 100644 --- a/tools/chatgui/src/videoplayer_engine.cpp +++ b/tools/chatgui/src/videoplayer_engine.cpp @@ -357,6 +357,8 @@ void VideoPlayerEngine::close() { qint64 VideoPlayerEngine::clockMs() const { if (m_hasAudio && m_audioSampleRate > 0) { + if (m_state == Finished && !SoundManager::instance()->isCurrentPcmPlaying()) + return m_durationMs; /* аудио закончилось — часы в конец */ unsigned long long cur = SoundManager::instance()->currentPcmCursor(); return (qint64)(cur * 1000 / (unsigned long long)m_audioSampleRate); } @@ -393,7 +395,14 @@ void VideoPlayerEngine::onAudioDecoded(std::shared_ptr> pcm m_audioSampleRate = sampleRate; SoundManager::instance()->playRawPcm(pcm->data(), (int)(pcm->size() / (size_t)channels), sampleRate, channels); - DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio started %dHz %dch", sampleRate, channels); + if (!SoundManager::instance()->isCurrentPcmPlaying()) { + /* аудио-устройство недоступно — переходим на системные часы */ + m_hasAudio = false; + m_audioSampleRate = 0; + DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio not playing, fallback to system clock"); + } else { + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio started %dHz %dch", sampleRate, channels); + } } else { m_hasAudio = false; DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "VideoEngine: no audio data, video only"); @@ -426,25 +435,24 @@ void VideoPlayerEngine::onWorkerFinished() { void VideoPlayerEngine::presentTick() { qint64 clk = clockMs(); + if (m_durationMs > 0 && clk > m_durationMs) clk = m_durationMs; + QImage show; bool have = false; - while (!m_pending.empty()) { - if (m_state != Finished && m_pending.front().second > clk) break; + while (!m_pending.empty() && m_pending.front().second <= clk) { show = m_pending.front().first; m_pending.pop_front(); have = true; } if (have) { m_renderFrame = show; emit framePresented(); } + m_positionMs = clk; + emit positionChanged(m_positionMs); + if (m_state == Finished && m_pending.empty()) { - m_positionMs = m_durationMs > 0 ? m_durationMs : clk; - emit positionChanged(m_positionMs); m_presentTimer.stop(); - return; + if (m_hasAudio) SoundManager::instance()->stopCurrentPcm(); } - m_positionMs = clk; - if (m_durationMs > 0 && clk > m_durationMs) m_positionMs = m_durationMs; - emit positionChanged(m_positionMs); } #include "videoplayer_engine.moc" diff --git a/tools/chatgui/src/videoplayer_window.cpp b/tools/chatgui/src/videoplayer_window.cpp index 20654824..3d85fd7a 100644 --- a/tools/chatgui/src/videoplayer_window.cpp +++ b/tools/chatgui/src/videoplayer_window.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -298,3 +299,11 @@ void VideoPlayerWindow::closeEvent(QCloseEvent* e) { emit closed(); QWidget::closeEvent(e); } + +void VideoPlayerWindow::resizeEvent(QResizeEvent* e) { + QWidget::resizeEvent(e); + m_render->setGeometry(rect()); + int ch = 48; + m_controls->setGeometry(0, height() - ch, width(), ch); + m_controls->raise(); +} diff --git a/tools/chatgui/src/videoplayer_window.h b/tools/chatgui/src/videoplayer_window.h index 8d09d10e..dc35c52f 100644 --- a/tools/chatgui/src/videoplayer_window.h +++ b/tools/chatgui/src/videoplayer_window.h @@ -46,6 +46,7 @@ protected: void mouseReleaseEvent(QMouseEvent* event) override; void mouseDoubleClickEvent(QMouseEvent* event) override; void closeEvent(QCloseEvent* event) override; + void resizeEvent(QResizeEvent* event) override; private slots: void onEngineOpened(int width, int height, qint64 durationMs, bool hasAudio); diff --git a/tools/chatgui/tests/test_video_engine.cpp b/tools/chatgui/tests/test_video_engine.cpp new file mode 100644 index 00000000..1b04418f --- /dev/null +++ b/tools/chatgui/tests/test_video_engine.cpp @@ -0,0 +1,47 @@ +// tests/test_video_engine.cpp — smoke-тест декодера VideoPlayerEngine (FFmpeg) +// Запуск: test_video_engine <путь_к_mp4> +#include "../src/videoplayer_engine.h" +#include +#include +#include +#include +#include + +int main(int argc, char** argv) { + QApplication app(argc, argv); + if (argc < 2) { qCritical("usage: test_video_engine "); return 2; } + QString path = argv[1]; + + VideoPlayerEngine eng; + int frames = 0, opened = 0, errors = 0, finished = 0; + qint64 lastPos = -1; + int width = 0, height = 0; qint64 dur = 0; bool hasAudio = false; + + QObject::connect(&eng, &VideoPlayerEngine::opened, + [&](int w, int h, qint64 d, bool a) { opened++; width = w; height = h; dur = d; hasAudio = a; }); + QObject::connect(&eng, &VideoPlayerEngine::framePresented, [&]() { frames++; }); + QObject::connect(&eng, &VideoPlayerEngine::positionChanged, [&](qint64 p) { lastPos = p; }); + QObject::connect(&eng, &VideoPlayerEngine::error, [&](const QString& m) { errors++; qCritical("engine error: %s", qPrintable(m)); }); + QObject::connect(&eng, &VideoPlayerEngine::finished, [&]() { finished++; }); + + eng.open(path); + + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + timer.start(5000); + loop.exec(); + + eng.close(); + + qInfo("opened=%d (%dx%d dur=%lldms audio=%d) frames=%d finished=%d errors=%d lastPos=%lldms", + opened, width, height, (long long)dur, hasAudio ? 1 : 0, frames, finished, errors, (long long)lastPos); + + int rc = 0; + if (opened != 1) { qCritical("FAIL: opened != 1"); rc = 1; } + if (frames <= 0) { qCritical("FAIL: no frames presented"); rc = 1; } + if (errors > 0) { qCritical("FAIL: errors present"); rc = 1; } + qInfo(rc == 0 ? "PASS" : "FAIL"); + return rc; +}