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; +}