diff --git a/tools/chatgui/src/videoplayer_engine.cpp b/tools/chatgui/src/videoplayer_engine.cpp index 6699f78f1..6f0f08d8c 100644 --- a/tools/chatgui/src/videoplayer_engine.cpp +++ b/tools/chatgui/src/videoplayer_engine.cpp @@ -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> 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> 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> 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 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);