Browse Source

chatgui: фикс синхронизации кадров (часы не убегают вперёд при быстром декоде) + smoke-тест движка

- presentTick: кадры всегда по мастер-часам, завершение только когда очередь пуста
- clockMs: по окончании аудио часы в конец (хвостовые кадры не теряются)
- fallback на системные часы, если аудио-устройство недоступно
- tests/test_video_engine: smoke-тест FFmpeg-декодера (opened/frames/seek)
v2
evgeny 3 weeks ago
parent
commit
74b17685f7
  1. 19
      tools/chatgui/CMakeLists.txt
  2. 26
      tools/chatgui/src/videoplayer_engine.cpp
  3. 9
      tools/chatgui/src/videoplayer_window.cpp
  4. 1
      tools/chatgui/src/videoplayer_window.h
  5. 47
      tools/chatgui/tests/test_video_engine.cpp

19
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 <mp4>
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()

26
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<std::vector<int16_t>> 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"

9
tools/chatgui/src/videoplayer_window.cpp

@ -11,6 +11,7 @@
#include <QKeyEvent>
#include <QMouseEvent>
#include <QCloseEvent>
#include <QResizeEvent>
#include <QApplication>
#include <QScreen>
#include <QStyle>
@ -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();
}

1
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);

47
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 <QApplication>
#include <QTimer>
#include <QEventLoop>
#include <QDebug>
#include <cstdio>
int main(int argc, char** argv) {
QApplication app(argc, argv);
if (argc < 2) { qCritical("usage: test_video_engine <video.mp4>"); 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;
}
Loading…
Cancel
Save