Follow PCM node completion and keep seeking and replay consistent

This commit is contained in:
evgeny committed 2026-10-28 00:58:59 +01:00
1 parent 7fe90be7b2
commit ab37a06dad
1 file changed
+47 -22
+47 -22
View File
@@ -89,6 +89,7 @@ void Worker::run() {
else if (p->codec_type == AVMEDIA_TYPE_AUDIO && aidx < 0) aidx = (int)i;
}
if (vidx < 0) { worker_fail(this, QCoreApplication::translate("ChatGui", "no video stream: %1").arg(path)); cleanup(); return; }
const bool audioExpected = aidx >= 0;
AVStream* vst = fmt->streams[vidx];
int vw = vst->codecpar->width, vh = vst->codecpar->height;
@@ -170,6 +171,9 @@ void Worker::run() {
/* вернуть чтение в начало для видео */
av_seek_frame(fmt, -1, 0, AVSEEK_FLAG_BACKWARD);
emit audioDecoded(audioPcm, asr, ach, audioDurMs);
} else if (audioExpected) {
// opened сообщил о наличии трека; явный fallback запускает GUI-часы при отказе audio decoder/resampler.
emit audioDecoded({}, 0, 0, 0);
}
/* ── 2. декодируем видео и шлём кадры ── */
@@ -250,6 +254,10 @@ void Worker::run() {
int ret = av_read_frame(fmt, pkt);
if (ret < 0) {
if (ret != AVERROR_EOF) {
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "VideoEngine: read failed ret=%d epoch=%d", ret, gen);
worker_fail(this, QCoreApplication::translate("ChatGui", "Cannot read video")); cleanup(); return;
}
avcodec_send_packet(vdec, nullptr);
while (avcodec_receive_frame(vdec, frame) == 0) if (!emitFrame(frame)) { cleanup(); return; }
eof = true;
@@ -296,7 +304,7 @@ void VideoPlayerEngine::open(const QString& path) {
m_abort.storeRelaxed(0);
m_paused.storeRelaxed(0);
m_epoch.storeRelaxed(0); m_seekTargetMs.storeRelaxed(0);
m_decodeEof = false; m_videoEndMs = 0; m_audioFrames = 0; m_audioClock = false;
m_decodeEof = false; m_videoEndMs = 0; m_audioFrames = 0; m_audioClock = false; m_drainReportAt = 0;
m_buffered.storeRelaxed(0);
clearPending();
m_renderFrame = QImage();
@@ -337,9 +345,9 @@ void VideoPlayerEngine::open(const QString& path) {
}
void VideoPlayerEngine::play() {
if (m_state != Paused && m_state != Playing) return;
if (m_state != Paused) return;
m_paused.storeRelaxed(0);
if (m_hasAudio) SoundManager::instance()->resumePcm(m_audioPlayback);
if (m_audioClock) SoundManager::instance()->resumePcm(m_audioPlayback);
if (!m_audioClock) { m_elapsedBaseMs = m_positionMs; m_elapsed.restart(); }
setState(Playing);
m_presentTimer.start();
@@ -347,8 +355,9 @@ void VideoPlayerEngine::play() {
void VideoPlayerEngine::pause() {
if (m_state != Playing) return;
m_positionMs = clockMs();
m_paused.storeRelaxed(1);
if (m_hasAudio) SoundManager::instance()->pausePcm(m_audioPlayback);
if (m_audioClock) SoundManager::instance()->pausePcm(m_audioPlayback);
setState(Paused);
m_presentTimer.stop();
}
@@ -360,11 +369,14 @@ void VideoPlayerEngine::togglePlayPause() {
}
void VideoPlayerEngine::seekMs(qint64 ms) {
if (!m_worker || m_abort.loadRelaxed()) {
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "VideoEngine: seek ignored without active worker target=%lldms", (long long)ms); return;
}
if (ms < 0) ms = 0;
if (m_durationMs > 0 && ms > m_durationMs) ms = m_durationMs;
m_seekTargetMs.storeRelaxed(ms);
m_epoch.fetchAndAddRelease(1);
m_decodeEof = false; m_videoEndMs = 0;
m_decodeEof = false; m_videoEndMs = 0; m_drainReportAt = 0;
clearPending();
if (m_hasAudio) {
int sr = SoundManager::instance()->pcmSampleRate(m_audioPlayback);
@@ -373,6 +385,8 @@ void VideoPlayerEngine::seekMs(qint64 ms) {
onWorkerError(QCoreApplication::translate("ChatGui", "Cannot seek video")); return;
}
m_audioClock = (uint64_t)(ms * sr / 1000) < m_audioFrames;
if (m_state == Playing && m_audioClock && !SoundManager::instance()->isPcmPlaying(m_audioPlayback))
SoundManager::instance()->resumePcm(m_audioPlayback);
}
}
m_elapsedBaseMs = ms; m_elapsed.restart();
@@ -384,14 +398,15 @@ void VideoPlayerEngine::seekMs(qint64 ms) {
void VideoPlayerEngine::stop() {
++m_openGeneration;
m_abort.storeRelaxed(1);
m_decodeEof = false; m_videoEndMs = 0; m_audioClock = false; m_audioFrames = 0;
m_decodeEof = false; m_videoEndMs = 0; m_audioClock = false; m_audioFrames = 0; m_drainReportAt = 0;
m_presentTimer.stop();
clearPending();
m_renderFrame = QImage();
if (m_hasAudio) SoundManager::instance()->stopPcm(m_audioPlayback);
setState(Stopped);
if (m_audioPlayback) SoundManager::instance()->stopPcm(m_audioPlayback);
m_audioPlayback = 0; m_audioSampleRate = 0; m_hasAudio = false;
m_positionMs = 0;
m_durationMs = 0;
setState(Stopped);
}
void VideoPlayerEngine::close() {
@@ -411,12 +426,14 @@ qint64 VideoPlayerEngine::clockMs() {
if (m_audioClock) {
auto* sound = SoundManager::instance();
const uint64_t cursor = sound->pcmCursor(m_audioPlayback);
if (cursor < m_audioFrames || sound->isPcmPlaying(m_audioPlayback))
if (sound->isPcmPlaying(m_audioPlayback))
return (qint64)(cursor * 1000 / (uint64_t)m_audioSampleRate);
// Cursor измеряет время узла, а не точную длину PCM: при естественном EOF может остаться последний квант.
m_audioClock = false;
m_elapsedBaseMs = std::max(m_positionMs, (qint64)((m_audioFrames * 1000 + m_audioSampleRate - 1) / m_audioSampleRate));
m_elapsed.restart();
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio ended, timer continues from=%lldms", (long long)m_elapsedBaseMs);
m_elapsed.restart(); m_drainReportAt = 0;
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio ended cursor=%llu/%llu timer_from=%lldms",
(unsigned long long)cursor, (unsigned long long)m_audioFrames, (long long)m_elapsedBaseMs);
}
return m_elapsedBaseMs + m_elapsed.elapsed();
}
@@ -448,7 +465,8 @@ void VideoPlayerEngine::onWorkerOpened(int width, int height, qint64 durationMs,
void VideoPlayerEngine::onAudioDecoded(std::shared_ptr<std::vector<int16_t>> pcm,
int sampleRate, int channels, qint64 durationMs) {
if (pcm && !pcm->empty() && channels > 0) {
const bool paused = m_state == Paused;
if (pcm && !pcm->empty() && channels > 0 && sampleRate > 0) {
m_audioSampleRate = sampleRate;
m_audioFrames = pcm->size() / (size_t)channels;
SoundManager::instance()->stopPcm(m_audioPlayback);
@@ -461,7 +479,13 @@ void VideoPlayerEngine::onAudioDecoded(std::shared_ptr<std::vector<int16_t>> pcm
m_audioFrames = 0; m_audioClock = false;
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio not playing, fallback to system clock");
} else {
m_audioClock = true;
const uint64_t target = (uint64_t)(m_positionMs * sampleRate / 1000);
if (target && !SoundManager::instance()->seekPcm(m_audioPlayback, target)) {
onWorkerError(QCoreApplication::translate("ChatGui", "Cannot seek video")); return;
}
m_audioClock = target < m_audioFrames;
if (paused || !m_audioClock) SoundManager::instance()->pausePcm(m_audioPlayback);
else if (!SoundManager::instance()->isPcmPlaying(m_audioPlayback)) SoundManager::instance()->resumePcm(m_audioPlayback);
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: audio started rate=%d channels=%d frames=%llu",
sampleRate, channels, (unsigned long long)m_audioFrames);
}
@@ -471,8 +495,7 @@ void VideoPlayerEngine::onAudioDecoded(std::shared_ptr<std::vector<int16_t>> pcm
}
m_elapsedBaseMs = m_positionMs;
m_elapsed.restart();
setState(Playing);
m_presentTimer.start();
if (!paused) { setState(Playing); m_presentTimer.start(); }
}
void VideoPlayerEngine::onFrameDecoded(const QImage& image, qint64 ptsMs, qint64 endMs, int epoch) {
@@ -486,9 +509,9 @@ void VideoPlayerEngine::onFrameDecoded(const QImage& image, qint64 ptsMs, qint64
void VideoPlayerEngine::onWorkerError(const QString& msg) {
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "VideoEngine: %s", qPrintable(msg));
m_presentTimer.stop();
setState(Stopped);
emit error(msg);
const uint64_t generation = m_openGeneration; QPointer<VideoPlayerEngine> guard(this);
stop();
if (guard && m_openGeneration == generation + 1) emit error(msg);
}
void VideoPlayerEngine::onDecodeFinished(int epoch) {
@@ -514,7 +537,11 @@ void VideoPlayerEngine::presentTick() {
m_buffered.fetchAndSubRelaxed(1);
have = true;
}
m_positionMs = clk;
const bool audioDone = !m_hasAudio || !SoundManager::instance()->isPcmPlaying(m_audioPlayback);
const qint64 audioEnd = m_audioSampleRate > 0 ? (qint64)((m_audioFrames * 1000 + m_audioSampleRate - 1) / m_audioSampleRate) : 0;
const qint64 end = std::max(m_videoEndMs, audioEnd);
const bool complete = m_decodeEof && m_pending.empty() && clk >= end && audioDone;
m_positionMs = complete ? end : clk;
if (have) {
m_renderFrame = show; emit framePresented();
if (!guard || generation != m_openGeneration || epoch != m_epoch.loadRelaxed() || m_state != Playing) return;
@@ -522,8 +549,6 @@ void VideoPlayerEngine::presentTick() {
emit positionChanged(m_positionMs);
if (!guard || generation != m_openGeneration || epoch != m_epoch.loadRelaxed() || m_state != Playing) return;
const bool audioDone = !m_hasAudio || (!SoundManager::instance()->isPcmPlaying(m_audioPlayback) &&
SoundManager::instance()->pcmCursor(m_audioPlayback) >= m_audioFrames);
if (m_decodeEof && m_elapsed.elapsed() - m_drainReportAt >= 1000) {
m_drainReportAt = m_elapsed.elapsed();
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO,
@@ -532,7 +557,7 @@ void VideoPlayerEngine::presentTick() {
(unsigned long long)SoundManager::instance()->pcmCursor(m_audioPlayback), (unsigned long long)m_audioFrames,
SoundManager::instance()->isPcmPlaying(m_audioPlayback), m_audioClock);
}
if (m_decodeEof && m_pending.empty() && clk >= m_videoEndMs && audioDone) {
if (complete) {
m_presentTimer.stop();
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: playback finished epoch=%d position=%lldms video_end=%lldms audio_done=%d",
epoch, (long long)m_positionMs, (long long)m_videoEndMs, audioDone);