Browse Source

chatgui: нативный move/resize (Wayland) + фикс замирания видео при перемотке

- окно: startSystemMove/startSystemResize вместо ручного move/setGeometry
  (Wayland игнорирует позицию окна) — драг и resize за левый/верхний край работают
- пропорции: снэп к videoAspect после нативного resize (дебаунс 150мс)
- перемотка: backpressure по m_buffered (декод не убегает вперёд и воркер живёт
  для seek), seek через av_seek_frame(-1, us), пропуск кадров с AV_NOPTS_VALUE
- SeekSlider: клик по шкале = прыжок к позиции
- крестик закрытия справа вверху; курсор контролов — стрелка
- test_video_engine: проверка seek
v2
evgeny 3 weeks ago
parent
commit
645591bb64
  1. 49
      tools/chatgui/src/videoplayer_engine.cpp
  2. 2
      tools/chatgui/src/videoplayer_engine.h
  3. 168
      tools/chatgui/src/videoplayer_window.cpp
  4. 14
      tools/chatgui/src/videoplayer_window.h
  5. 32
      tools/chatgui/tests/test_video_engine.cpp

49
tools/chatgui/src/videoplayer_engine.cpp

@ -18,7 +18,7 @@ extern "C" {
} }
#endif #endif
static const int kMaxInFlight = 24; static const int kMaxBuffered = 30;
/* ── Worker: демюкс + декод в отдельном потоке ── */ /* ── Worker: демюкс + декод в отдельном потоке ── */
@ -30,7 +30,7 @@ public:
QAtomicInt* paused = nullptr; QAtomicInt* paused = nullptr;
QAtomicInt* seekReq = nullptr; QAtomicInt* seekReq = nullptr;
QAtomicInteger<qint64>* seekTargetMs = nullptr; QAtomicInteger<qint64>* seekTargetMs = nullptr;
QAtomicInt* inFlight = nullptr; QAtomicInt* buffered = nullptr;
QAtomicInt* epoch = nullptr; QAtomicInt* epoch = nullptr;
signals: signals:
@ -184,35 +184,45 @@ void Worker::run() {
SWS_BILINEAR, nullptr, nullptr, nullptr); SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!sws) { worker_fail(this, "sws init failed"); cleanup(); return; } if (!sws) { worker_fail(this, "sws init failed"); cleanup(); return; }
int gen = epoch->loadRelaxed();
auto emitFrame = [&](AVFrame* fr) { auto emitFrame = [&](AVFrame* fr) {
if (fr->pts == AV_NOPTS_VALUE) return; /* кадр без pts ломает очередь */
QImage img(vw, vh, QImage::Format_RGB888); QImage img(vw, vh, QImage::Format_RGB888);
uint8_t* dst[1] = { img.bits() }; uint8_t* dst[1] = { img.bits() };
int dstStride[1] = { (int)img.bytesPerLine() }; int dstStride[1] = { (int)img.bytesPerLine() };
sws_scale(sws, fr->data, fr->linesize, 0, vh, dst, dstStride); sws_scale(sws, fr->data, fr->linesize, 0, vh, dst, dstStride);
qint64 ptsMs = av_rescale_q(fr->pts, vst->time_base, AVRational{1, 1000}); qint64 ptsMs = av_rescale_q(fr->pts, vst->time_base, AVRational{1, 1000});
inFlight->fetchAndAddRelaxed(1); buffered->fetchAndAddRelaxed(1);
emit frameDecoded(img, ptsMs, epoch->loadRelaxed()); emit frameDecoded(img, ptsMs, gen);
}; };
bool eof = false; bool eof = false;
while (!abort->loadRelaxed() && !eof) { while (!abort->loadRelaxed()) {
if (seekReq->loadRelaxed()) { if (seekReq->loadRelaxed()) {
qint64 targetMs = seekTargetMs->loadRelaxed(); qint64 targetMs = seekTargetMs->loadRelaxed();
qint64 ts = av_rescale_q(targetMs, AVRational{1, 1000}, vst->time_base); int sret = av_seek_frame(fmt, -1, targetMs * 1000, AVSEEK_FLAG_BACKWARD);
av_seek_frame(fmt, vidx, ts, AVSEEK_FLAG_BACKWARD); if (sret < 0)
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "VideoEngine: seek to %lldms failed ret=%d",
(long long)targetMs, sret);
else
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: seek to %lldms", (long long)targetMs);
avcodec_flush_buffers(vdec); avcodec_flush_buffers(vdec);
gen = epoch->loadRelaxed();
eof = false;
seekReq->storeRelaxed(0); seekReq->storeRelaxed(0);
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: seek to %lldms", (long long)targetMs);
} }
if (paused->loadRelaxed()) { QThread::msleep(10); continue; } if (paused->loadRelaxed()) { QThread::msleep(10); continue; }
if (inFlight->loadRelaxed() >= kMaxInFlight) { QThread::msleep(3); continue; } if (eof) { QThread::msleep(10); continue; } /* ждём seek или abort */
if (buffered->loadRelaxed() >= kMaxBuffered) { QThread::msleep(3); continue; }
int ret = av_read_frame(fmt, pkt); int ret = av_read_frame(fmt, pkt);
if (ret < 0) { if (ret < 0) {
avcodec_send_packet(vdec, nullptr); avcodec_send_packet(vdec, nullptr);
while (avcodec_receive_frame(vdec, frame) == 0) emitFrame(frame); while (avcodec_receive_frame(vdec, frame) == 0) emitFrame(frame);
eof = true; eof = true;
break; DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: decode finished");
emit finished();
continue;
} }
if (pkt->stream_index == vidx) { if (pkt->stream_index == vidx) {
avcodec_send_packet(vdec, pkt); avcodec_send_packet(vdec, pkt);
@ -220,11 +230,6 @@ void Worker::run() {
} }
av_packet_unref(pkt); av_packet_unref(pkt);
} }
if (!abort->loadRelaxed()) {
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: decode finished");
emit finished();
}
} }
cleanup(); cleanup();
@ -258,7 +263,7 @@ void VideoPlayerEngine::open(const QString& path) {
m_abort.storeRelaxed(0); m_abort.storeRelaxed(0);
m_paused.storeRelaxed(0); m_paused.storeRelaxed(0);
m_seekReq.storeRelaxed(0); m_seekReq.storeRelaxed(0);
m_inFlight.storeRelaxed(0); m_buffered.storeRelaxed(0);
clearPending(); clearPending();
m_renderFrame = QImage(); m_renderFrame = QImage();
m_durationMs = 0; m_positionMs = 0; m_hasAudio = false; m_durationMs = 0; m_positionMs = 0; m_hasAudio = false;
@ -272,7 +277,7 @@ void VideoPlayerEngine::open(const QString& path) {
m_worker->paused = &m_paused; m_worker->paused = &m_paused;
m_worker->seekReq = &m_seekReq; m_worker->seekReq = &m_seekReq;
m_worker->seekTargetMs = &m_seekTargetMs; m_worker->seekTargetMs = &m_seekTargetMs;
m_worker->inFlight = &m_inFlight; m_worker->buffered = &m_buffered;
m_worker->epoch = &m_epoch; m_worker->epoch = &m_epoch;
m_worker->moveToThread(m_thread); m_worker->moveToThread(m_thread);
@ -282,7 +287,6 @@ void VideoPlayerEngine::open(const QString& path) {
connect(m_worker, &Worker::frameDecoded, this, &VideoPlayerEngine::onFrameDecoded); connect(m_worker, &Worker::frameDecoded, this, &VideoPlayerEngine::onFrameDecoded);
connect(m_worker, &Worker::error, this, &VideoPlayerEngine::onWorkerError); connect(m_worker, &Worker::error, this, &VideoPlayerEngine::onWorkerError);
connect(m_worker, &Worker::finished, this, &VideoPlayerEngine::onWorkerFinished); connect(m_worker, &Worker::finished, this, &VideoPlayerEngine::onWorkerFinished);
connect(m_worker, &Worker::finished, m_thread, &QThread::quit);
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: open %s", qPrintable(path)); DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: open %s", qPrintable(path));
m_thread->start(); m_thread->start();
@ -326,7 +330,6 @@ void VideoPlayerEngine::seekMs(qint64 ms) {
m_elapsed.restart(); m_elapsed.restart();
} }
m_positionMs = ms; m_positionMs = ms;
m_inFlight.storeRelaxed(0);
if (m_state == Finished) setState(Paused); if (m_state == Finished) setState(Paused);
emit positionChanged(ms); emit positionChanged(ms);
} }
@ -372,6 +375,7 @@ void VideoPlayerEngine::setState(State s) {
} }
void VideoPlayerEngine::clearPending() { void VideoPlayerEngine::clearPending() {
m_buffered.fetchAndSubRelaxed((int)m_pending.size());
m_pending.clear(); m_pending.clear();
} }
@ -414,8 +418,10 @@ void VideoPlayerEngine::onAudioDecoded(std::shared_ptr<std::vector<int16_t>> pcm
} }
void VideoPlayerEngine::onFrameDecoded(const QImage& image, qint64 ptsMs, int epoch) { void VideoPlayerEngine::onFrameDecoded(const QImage& image, qint64 ptsMs, int epoch) {
m_inFlight.fetchAndSubRelaxed(1); if (epoch != m_epoch.loadRelaxed()) {
if (epoch != m_epoch.loadRelaxed()) return; /* устаревший кадр после seek */ m_buffered.fetchAndSubRelaxed(1); /* устаревший кадр после seek — не будет показан */
return;
}
m_pending.push_back({ image, ptsMs }); m_pending.push_back({ image, ptsMs });
} }
@ -442,6 +448,7 @@ void VideoPlayerEngine::presentTick() {
while (!m_pending.empty() && m_pending.front().second <= clk) { while (!m_pending.empty() && m_pending.front().second <= clk) {
show = m_pending.front().first; show = m_pending.front().first;
m_pending.pop_front(); m_pending.pop_front();
m_buffered.fetchAndSubRelaxed(1);
have = true; have = true;
} }
if (have) { m_renderFrame = show; emit framePresented(); } if (have) { m_renderFrame = show; emit framePresented(); }

2
tools/chatgui/src/videoplayer_engine.h

@ -79,7 +79,7 @@ private:
QAtomicInt m_paused{0}; QAtomicInt m_paused{0};
QAtomicInt m_seekReq{0}; QAtomicInt m_seekReq{0};
QAtomicInteger<qint64> m_seekTargetMs{0}; QAtomicInteger<qint64> m_seekTargetMs{0};
QAtomicInt m_inFlight{0}; QAtomicInt m_buffered{0};
QAtomicInt m_epoch{0}; QAtomicInt m_epoch{0};
/* презентация (только GUI-поток) */ /* презентация (только GUI-поток) */

168
tools/chatgui/src/videoplayer_window.cpp

@ -15,6 +15,7 @@
#include <QApplication> #include <QApplication>
#include <QScreen> #include <QScreen>
#include <QStyle> #include <QStyle>
#include <QWindow>
/* ─── VideoRenderWidget ─── */ /* ─── VideoRenderWidget ─── */
@ -51,13 +52,25 @@ static QString fmtTime(qint64 ms) {
return QString("%1:%2").arg(s / 60).arg(s % 60, 2, 10, QChar('0')); return QString("%1:%2").arg(s / 60).arg(s % 60, 2, 10, QChar('0'));
} }
static QPoint mouseGlobalPos(const QMouseEvent* e) { /* Слайдер перемотки: клик по любому месту шкалы = прыжок к позиции (драг кружка сохраняется). */
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) class SeekSlider : public QSlider {
return e->globalPosition().toPoint(); public:
#else using QSlider::QSlider;
return e->globalPos();
#endif protected:
void mousePressEvent(QMouseEvent* e) override {
if (e->button() == Qt::LeftButton) {
setSliderDown(true);
int v = QStyle::sliderValueFromPosition(minimum(), maximum(),
(int)e->position().x(), width());
setValue(v);
emit sliderPressed();
e->accept();
} else {
QSlider::mousePressEvent(e);
}
} }
};
VideoPlayerWindow::VideoPlayerWindow(QWidget* parent) VideoPlayerWindow::VideoPlayerWindow(QWidget* parent)
: QWidget(parent, Qt::Window | Qt::FramelessWindowHint) : QWidget(parent, Qt::Window | Qt::FramelessWindowHint)
@ -74,6 +87,7 @@ VideoPlayerWindow::VideoPlayerWindow(QWidget* parent)
m_controls = new QWidget(this); m_controls = new QWidget(this);
m_controls->setObjectName("videoControls"); m_controls->setObjectName("videoControls");
m_controls->setCursor(Qt::ArrowCursor);
m_controls->installEventFilter(this); m_controls->installEventFilter(this);
m_controls->setStyleSheet( m_controls->setStyleSheet(
"#videoControls { background-color: rgba(0,0,0,0.65); }" "#videoControls { background-color: rgba(0,0,0,0.65); }"
@ -86,9 +100,7 @@ VideoPlayerWindow::VideoPlayerWindow(QWidget* parent)
m_playBtn = new QPushButton(QStringLiteral("⏸"), m_controls); m_playBtn = new QPushButton(QStringLiteral("⏸"), m_controls);
m_playBtn->setToolTip(QStringLiteral("Пауза")); m_playBtn->setToolTip(QStringLiteral("Пауза"));
m_closeBtn = new QPushButton(QStringLiteral("✕"), m_controls); m_slider = new SeekSlider(Qt::Horizontal, m_controls);
m_closeBtn->setToolTip(QStringLiteral("Закрыть"));
m_slider = new QSlider(Qt::Horizontal, m_controls);
m_slider->setRange(0, 0); m_slider->setRange(0, 0);
m_timeLabel = new QLabel(QStringLiteral("0:00 / 0:00"), m_controls); m_timeLabel = new QLabel(QStringLiteral("0:00 / 0:00"), m_controls);
@ -98,13 +110,26 @@ VideoPlayerWindow::VideoPlayerWindow(QWidget* parent)
lay->addWidget(m_playBtn); lay->addWidget(m_playBtn);
lay->addWidget(m_slider, 1); lay->addWidget(m_slider, 1);
lay->addWidget(m_timeLabel); lay->addWidget(m_timeLabel);
lay->addWidget(m_closeBtn);
/* крестик закрытия — отдельно, справа вверху */
m_closeBtn = new QPushButton(QStringLiteral("✕"), this);
m_closeBtn->setToolTip(QStringLiteral("Закрыть"));
m_closeBtn->setCursor(Qt::ArrowCursor);
m_closeBtn->setStyleSheet(
"QPushButton { background: rgba(0,0,0,0.5); color: white; border: none; "
"border-radius: 4px; font-size: 14px; }"
"QPushButton:hover { background: rgba(255,255,255,0.25); }");
m_hideTimer = new QTimer(this); m_hideTimer = new QTimer(this);
m_hideTimer->setSingleShot(true); m_hideTimer->setSingleShot(true);
m_hideTimer->setInterval(2500); m_hideTimer->setInterval(2500);
connect(m_hideTimer, &QTimer::timeout, this, &VideoPlayerWindow::hideControls); connect(m_hideTimer, &QTimer::timeout, this, &VideoPlayerWindow::hideControls);
m_snapTimer = new QTimer(this);
m_snapTimer->setSingleShot(true);
m_snapTimer->setInterval(150);
connect(m_snapTimer, &QTimer::timeout, this, &VideoPlayerWindow::snapToAspect);
connect(m_playBtn, &QPushButton::clicked, this, &VideoPlayerWindow::onPlayPause); connect(m_playBtn, &QPushButton::clicked, this, &VideoPlayerWindow::onPlayPause);
connect(m_closeBtn, &QPushButton::clicked, this, &VideoPlayerWindow::onClose); connect(m_closeBtn, &QPushButton::clicked, this, &VideoPlayerWindow::onClose);
connect(m_slider, &QSlider::sliderPressed, this, &VideoPlayerWindow::onSliderPressed); connect(m_slider, &QSlider::sliderPressed, this, &VideoPlayerWindow::onSliderPressed);
@ -136,6 +161,7 @@ void VideoPlayerWindow::onEngineOpened(int width, int height, qint64 durationMs,
Q_UNUSED(hasAudio); Q_UNUSED(hasAudio);
m_slider->setRange(0, (int)durationMs); m_slider->setRange(0, (int)durationMs);
updateTimeLabels(0); updateTimeLabels(0);
m_videoAspect = (height > 0) ? (double)width / height : 0.0;
QScreen* scr = QApplication::primaryScreen(); QScreen* scr = QApplication::primaryScreen();
QRect ag = scr ? scr->availableGeometry() : QRect(0, 0, 1280, 720); QRect ag = scr ? scr->availableGeometry() : QRect(0, 0, 1280, 720);
@ -226,6 +252,7 @@ void VideoPlayerWindow::toggleFullscreen() {
void VideoPlayerWindow::setControlsVisible(bool visible) { void VideoPlayerWindow::setControlsVisible(bool visible) {
m_controls->setVisible(visible); m_controls->setVisible(visible);
m_closeBtn->setVisible(visible);
} }
void VideoPlayerWindow::showControls() { void VideoPlayerWindow::showControls() {
@ -285,11 +312,10 @@ void VideoPlayerWindow::mousePressEvent(QMouseEvent* e) {
} }
ResizeMode m = hitTestResize(e->pos()); ResizeMode m = hitTestResize(e->pos());
if (m != ResizeNone) { if (m != ResizeNone) {
beginResize(m, mouseGlobalPos(e)); m_resizeEdges = edgesForMode(m);
if (windowHandle()) windowHandle()->startSystemResize(m_resizeEdges);
} else { } else {
m_dragging = true; if (windowHandle()) windowHandle()->startSystemMove();
m_dragStart = mouseGlobalPos(e);
m_windowStart = frameGeometry().topLeft();
} }
} }
} }
@ -299,51 +325,25 @@ void VideoPlayerWindow::mouseMoveEvent(QMouseEvent* e) {
showControls(); showControls();
return; return;
} }
if (m_resizeMode != ResizeNone && (e->buttons() & Qt::LeftButton)) {
applyResize(mouseGlobalPos(e));
return;
}
if (m_dragging && (e->buttons() & Qt::LeftButton)) {
move(m_windowStart + (mouseGlobalPos(e) - m_dragStart));
return;
}
if (!(e->buttons() & Qt::LeftButton)) if (!(e->buttons() & Qt::LeftButton))
updateResizeCursor(e->pos()); updateResizeCursor(e->pos());
} }
void VideoPlayerWindow::mouseReleaseEvent(QMouseEvent* e) {
if (e->button() == Qt::LeftButton) {
m_dragging = false;
m_resizeMode = ResizeNone;
}
}
void VideoPlayerWindow::mouseDoubleClickEvent(QMouseEvent* e) { void VideoPlayerWindow::mouseDoubleClickEvent(QMouseEvent* e) {
if (e->button() == Qt::LeftButton) toggleFullscreen(); if (e->button() == Qt::LeftButton) toggleFullscreen();
} }
bool VideoPlayerWindow::eventFilter(QObject* obj, QEvent* ev) { bool VideoPlayerWindow::eventFilter(QObject* obj, QEvent* ev) {
/* Резрез за нижний край: панель контролов перекрывает нижнюю кромку окна, /* Резрез за нижний край: панель контролов перекрывает нижнюю кромку окна,
* поэтому маршрутизируем события из её пустых полей в логику resize. */ * поэтому маршрутизируем press из её пустых полей в нативный resize. */
if (obj == m_controls && !m_fullscreen) { if (obj == m_controls && !m_fullscreen && ev->type() == QEvent::MouseButtonPress) {
QEvent::Type t = ev->type();
if (t == QEvent::MouseButtonPress || t == QEvent::MouseMove || t == QEvent::MouseButtonRelease) {
QMouseEvent* me = static_cast<QMouseEvent*>(ev); QMouseEvent* me = static_cast<QMouseEvent*>(ev);
if (me->button() == Qt::LeftButton) {
QPoint winPos = m_controls->mapTo(this, me->pos()); QPoint winPos = m_controls->mapTo(this, me->pos());
ResizeMode m = hitTestResize(winPos); ResizeMode m = hitTestResize(winPos);
if (m == ResizeNone) return QWidget::eventFilter(obj, ev); if (m != ResizeNone) {
m_resizeEdges = edgesForMode(m);
QPoint gp = mouseGlobalPos(me); if (windowHandle()) windowHandle()->startSystemResize(m_resizeEdges);
if (t == QEvent::MouseButtonPress && me->button() == Qt::LeftButton) {
beginResize(m, gp);
return true;
}
if (t == QEvent::MouseMove && m_resizeMode != ResizeNone && (me->buttons() & Qt::LeftButton)) {
applyResize(gp);
return true;
}
if (t == QEvent::MouseButtonRelease && me->button() == Qt::LeftButton) {
m_resizeMode = ResizeNone;
return true; return true;
} }
} }
@ -363,6 +363,10 @@ void VideoPlayerWindow::resizeEvent(QResizeEvent* e) {
int ch = 48; int ch = 48;
m_controls->setGeometry(0, height() - ch, width(), ch); m_controls->setGeometry(0, height() - ch, width(), ch);
m_controls->raise(); m_controls->raise();
m_closeBtn->setGeometry(width() - 40, 8, 32, 32);
m_closeBtn->raise();
/* во время нативного resize снэпаем пропорцию после окончания жеста */
if (m_resizeEdges != 0) m_snapTimer->start();
} }
VideoPlayerWindow::ResizeMode VideoPlayerWindow::hitTestResize(const QPoint& pos) const { VideoPlayerWindow::ResizeMode VideoPlayerWindow::hitTestResize(const QPoint& pos) const {
@ -393,47 +397,33 @@ void VideoPlayerWindow::updateResizeCursor(const QPoint& pos) {
} }
} }
void VideoPlayerWindow::beginResize(ResizeMode m, const QPoint& globalPos) { Qt::Edges VideoPlayerWindow::edgesForMode(ResizeMode m) {
m_resizeMode = m; switch (m) {
m_resizeStartGeom = geometry(); case ResizeLeft: return Qt::LeftEdge;
m_resizeStartGlobal = globalPos; case ResizeRight: return Qt::RightEdge;
} case ResizeTop: return Qt::TopEdge;
case ResizeBottom: return Qt::BottomEdge;
void VideoPlayerWindow::applyResize(const QPoint& globalPos) { case ResizeTopLeft: return Qt::TopEdge | Qt::LeftEdge;
const int kMinW = 160; case ResizeTopRight: return Qt::TopEdge | Qt::RightEdge;
QRect g = m_resizeStartGeom; case ResizeBottomLeft: return Qt::BottomEdge | Qt::LeftEdge;
double aspect = (g.height() > 0) ? (double)g.width() / g.height() : 1.0; case ResizeBottomRight: return Qt::BottomEdge | Qt::RightEdge;
int minH = qMax(1, qRound(kMinW / aspect)); default: return {};
int dx = globalPos.x() - m_resizeStartGlobal.x(); }
int dy = globalPos.y() - m_resizeStartGlobal.y(); }
int newW = g.width(), newH = g.height(); void VideoPlayerWindow::snapToAspect() {
bool widthDriven = true; if (m_videoAspect <= 0.0 || m_resizeEdges == 0) return;
switch (m_resizeMode) { Qt::Edges edges = m_resizeEdges;
case ResizeRight: case ResizeTopRight: case ResizeBottomRight: newW = g.width() + dx; break; m_resizeEdges = {}; /* сброс до resize(), чтобы снэп не ре-триггерил сам себя */
case ResizeLeft: case ResizeTopLeft: case ResizeBottomLeft: newW = g.width() - dx; break; int w = width(), h = height();
case ResizeBottom: newH = g.height() + dy; widthDriven = false; break; if (w <= 0 || h <= 0) return;
case ResizeTop: newH = g.height() - dy; widthDriven = false; break;
default: return; bool verticalOnly = !(edges & (Qt::LeftEdge | Qt::RightEdge));
} if (verticalOnly) {
if (widthDriven) newH = qRound(newW / aspect); int newW = qRound(h * m_videoAspect);
else newW = qRound(newH * aspect); if (qAbs(newW - w) > 1) resize(newW, h);
} else {
if (newW < kMinW) { newW = kMinW; newH = minH; } int newH = qRound(w / m_videoAspect);
if (newH < minH) { newH = minH; newW = kMinW; } if (qAbs(newH - h) > 1) resize(w, newH);
}
int cx = g.center().x(), cy = g.center().y();
int left = g.left(), top = g.top();
switch (m_resizeMode) {
case ResizeRight: left = g.left(); top = cy - newH / 2; break;
case ResizeLeft: left = g.right() - newW + 1; top = cy - newH / 2; break;
case ResizeBottom: left = cx - newW / 2; top = g.top(); break;
case ResizeTop: left = cx - newW / 2; top = g.bottom() - newH + 1; break;
case ResizeTopLeft: left = g.right() - newW + 1; top = g.bottom() - newH + 1; break;
case ResizeTopRight: left = g.left(); top = g.bottom() - newH + 1; break;
case ResizeBottomLeft: left = g.right() - newW + 1; top = g.top(); break;
case ResizeBottomRight: left = g.left(); top = g.top(); break;
default: return;
}
setGeometry(left, top, newW, newH);
} }

14
tools/chatgui/src/videoplayer_window.h

@ -43,7 +43,6 @@ protected:
void keyPressEvent(QKeyEvent* event) override; void keyPressEvent(QKeyEvent* event) override;
void mousePressEvent(QMouseEvent* event) override; void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override; void mouseDoubleClickEvent(QMouseEvent* event) override;
void closeEvent(QCloseEvent* event) override; void closeEvent(QCloseEvent* event) override;
void resizeEvent(QResizeEvent* event) override; void resizeEvent(QResizeEvent* event) override;
@ -75,8 +74,8 @@ private:
QSize fitWindowSize(int vw, int vh) const; QSize fitWindowSize(int vw, int vh) const;
ResizeMode hitTestResize(const QPoint& localPos) const; ResizeMode hitTestResize(const QPoint& localPos) const;
void updateResizeCursor(const QPoint& localPos); void updateResizeCursor(const QPoint& localPos);
void applyResize(const QPoint& globalPos); static Qt::Edges edgesForMode(ResizeMode m);
void beginResize(ResizeMode m, const QPoint& globalPos); void snapToAspect();
VideoRenderWidget* m_render = nullptr; VideoRenderWidget* m_render = nullptr;
QWidget* m_controls = nullptr; QWidget* m_controls = nullptr;
@ -88,11 +87,8 @@ private:
bool m_fullscreen = false; bool m_fullscreen = false;
bool m_sliderDragging = false; bool m_sliderDragging = false;
bool m_dragging = false; double m_videoAspect = 0.0;
QPoint m_dragStart; Qt::Edges m_resizeEdges = {};
QPoint m_windowStart;
ResizeMode m_resizeMode = ResizeNone;
QRect m_resizeStartGeom;
QPoint m_resizeStartGlobal;
QTimer* m_hideTimer = nullptr; QTimer* m_hideTimer = nullptr;
QTimer* m_snapTimer = nullptr;
}; };

32
tools/chatgui/tests/test_video_engine.cpp

@ -7,15 +7,23 @@
#include <QDebug> #include <QDebug>
#include <cstdio> #include <cstdio>
extern "C" {
#include "../../lib/debug_config.h"
}
int main(int argc, char** argv) { int main(int argc, char** argv) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_TRACE);
debug_enable_console(1);
QApplication app(argc, argv); QApplication app(argc, argv);
if (argc < 2) { qCritical("usage: test_video_engine <video.mp4>"); return 2; } if (argc < 2) { qCritical("usage: test_video_engine <video.mp4>"); return 2; }
QString path = argv[1]; QString path = argv[1];
VideoPlayerEngine eng; VideoPlayerEngine eng;
int frames = 0, opened = 0, errors = 0, finished = 0; int frames = 0, opened = 0, errors = 0, finished = 0;
qint64 lastPos = -1; qint64 lastPos = -1, posAfterSeek = -1;
int width = 0, height = 0; qint64 dur = 0; bool hasAudio = false; int width = 0, height = 0; qint64 dur = 0; bool hasAudio = false;
int framesAfterSeek = 0;
QObject::connect(&eng, &VideoPlayerEngine::opened, QObject::connect(&eng, &VideoPlayerEngine::opened,
[&](int w, int h, qint64 d, bool a) { opened++; width = w; height = h; dur = d; hasAudio = a; }); [&](int w, int h, qint64 d, bool a) { opened++; width = w; height = h; dur = d; hasAudio = a; });
@ -26,22 +34,38 @@ int main(int argc, char** argv) {
eng.open(path); eng.open(path);
/* через 1с — seek к середине */
int framesBeforeSeek = 0;
QTimer seekTimer;
seekTimer.setSingleShot(true);
QObject::connect(&seekTimer, &QTimer::timeout, [&]() {
framesBeforeSeek = frames;
eng.seekMs(eng.durationMs() / 2);
QTimer::singleShot(1000, &eng, [&]() {
framesAfterSeek = frames - framesBeforeSeek;
posAfterSeek = eng.positionMs();
});
});
seekTimer.start(1000);
QEventLoop loop; QEventLoop loop;
QTimer timer; QTimer timer;
timer.setSingleShot(true); timer.setSingleShot(true);
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
timer.start(5000); timer.start(6000);
loop.exec(); loop.exec();
eng.close(); eng.close();
qInfo("opened=%d (%dx%d dur=%lldms audio=%d) frames=%d finished=%d errors=%d lastPos=%lldms", qInfo("opened=%d (%dx%d dur=%lldms audio=%d) frames=%d finished=%d errors=%d lastPos=%lldms posAfterSeek=%lldms framesAfterSeek=%d",
opened, width, height, (long long)dur, hasAudio ? 1 : 0, frames, finished, errors, (long long)lastPos); opened, width, height, (long long)dur, hasAudio ? 1 : 0, frames, finished, errors,
(long long)lastPos, (long long)posAfterSeek, framesAfterSeek);
int rc = 0; int rc = 0;
if (opened != 1) { qCritical("FAIL: opened != 1"); rc = 1; } if (opened != 1) { qCritical("FAIL: opened != 1"); rc = 1; }
if (frames <= 0) { qCritical("FAIL: no frames presented"); rc = 1; } if (frames <= 0) { qCritical("FAIL: no frames presented"); rc = 1; }
if (errors > 0) { qCritical("FAIL: errors present"); rc = 1; } if (errors > 0) { qCritical("FAIL: errors present"); rc = 1; }
if (posAfterSeek <= 0 || framesAfterSeek <= 0) { qCritical("FAIL: no frames/position after seek"); rc = 1; }
qInfo(rc == 0 ? "PASS" : "FAIL"); qInfo(rc == 0 ? "PASS" : "FAIL");
return rc; return rc;
} }

Loading…
Cancel
Save