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
This commit is contained in:
evgeny committed 2026-09-08 02:04:18 +03:00
1 parent f2b34459c3
commit 645591bb64
5 files changed
+139 -122

No files matched your search

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