diff --git a/tools/chatgui/src/videoplayer_engine.cpp b/tools/chatgui/src/videoplayer_engine.cpp index 6f0f08d8..c061ad39 100644 --- a/tools/chatgui/src/videoplayer_engine.cpp +++ b/tools/chatgui/src/videoplayer_engine.cpp @@ -349,8 +349,10 @@ void VideoPlayerEngine::play() { m_paused.storeRelaxed(0); if (m_audioClock) SoundManager::instance()->resumePcm(m_audioPlayback); if (!m_audioClock) { m_elapsedBaseMs = m_positionMs; m_elapsed.restart(); } - setState(Playing); m_presentTimer.start(); + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: resume epoch=%d position=%lldms audio_clock=%d", + m_epoch.loadRelaxed(), (long long)m_positionMs, m_audioClock); + setState(Playing); } void VideoPlayerEngine::pause() { @@ -358,8 +360,10 @@ void VideoPlayerEngine::pause() { m_positionMs = clockMs(); m_paused.storeRelaxed(1); if (m_audioClock) SoundManager::instance()->pausePcm(m_audioPlayback); - setState(Paused); m_presentTimer.stop(); + DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: pause epoch=%d position=%lldms audio_clock=%d", + m_epoch.loadRelaxed(), (long long)m_positionMs, m_audioClock); + setState(Paused); } void VideoPlayerEngine::togglePlayPause() { @@ -396,6 +400,9 @@ void VideoPlayerEngine::seekMs(qint64 ms) { } void VideoPlayerEngine::stop() { + if (m_worker) DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoEngine: stop generation=%llu state=%d position=%lldms pending=%zu PCM=%llu", + (unsigned long long)m_openGeneration, (int)m_state, (long long)m_positionMs, m_pending.size(), + (unsigned long long)m_audioPlayback); ++m_openGeneration; m_abort.storeRelaxed(1); m_decodeEof = false; m_videoEndMs = 0; m_audioClock = false; m_audioFrames = 0; m_drainReportAt = 0; @@ -454,12 +461,14 @@ void VideoPlayerEngine::onWorkerOpened(int width, int height, qint64 durationMs, m_videoHeight = height; m_durationMs = durationMs; m_hasAudio = hasAudio; + const uint64_t generation = m_openGeneration; QPointer guard(this); emit opened(width, height, durationMs, hasAudio); + if (!guard || generation != m_openGeneration) return; if (!hasAudio) { - m_elapsedBaseMs = 0; + m_elapsedBaseMs = m_positionMs; // Обработчик opened может уже запросить seek. m_elapsed.restart(); - setState(Playing); m_presentTimer.start(); + setState(Playing); } } @@ -495,7 +504,7 @@ void VideoPlayerEngine::onAudioDecoded(std::shared_ptr> pcm } m_elapsedBaseMs = m_positionMs; m_elapsed.restart(); - if (!paused) { setState(Playing); m_presentTimer.start(); } + if (!paused) { m_presentTimer.start(); setState(Playing); } } void VideoPlayerEngine::onFrameDecoded(const QImage& image, qint64 ptsMs, qint64 endMs, int epoch) { diff --git a/tools/chatgui/src/videoplayer_engine.h b/tools/chatgui/src/videoplayer_engine.h index 90555e5e..661b777f 100644 --- a/tools/chatgui/src/videoplayer_engine.h +++ b/tools/chatgui/src/videoplayer_engine.h @@ -25,35 +25,35 @@ Q_DECLARE_METATYPE(std::shared_ptr>) class VideoPlayerEngine : public QObject { Q_OBJECT public: - enum State { Stopped = 0, Playing = 1, Paused = 2, Finished = 3 }; + enum State { Stopped = 0, Playing = 1, Paused = 2, Finished = 3 }; // Загрузка также Stopped; Finished только после доигрывания. explicit VideoPlayerEngine(QObject* parent = nullptr); ~VideoPlayerEngine() override; - void open(const QString& path); - void play(); - void pause(); - void togglePlayPause(); - void seekMs(qint64 ms); - void stop(); - void close(); + void open(const QString& path); // Закрывает прежний файл, запускает worker; opened/error сообщают результат асинхронно. + void play(); // Возобновляет Paused; в остальных состояниях ничего не делает. + void pause(); // Приостанавливает Playing, включая доигрывание после EOF; сохраняет кадр и позицию. + void togglePlayPause(); // Playing ↔ Paused; Finished запускает заново через seek(0). + void seekMs(qint64 ms); // Асинхронный seek, мс от начала; ограничивается [0, duration], если длительность известна. + void stop(); // Отменяет worker, удаляет очередь и PCM, сбрасывает позицию/длительность; поток освобождает close. + void close(); // stop, quit и ожидание worker до 2с, затем освобождение worker/QThread; также вызывается деструктором. State state() const { return m_state; } - qint64 durationMs() const { return m_durationMs; } - qint64 positionMs() const { return m_positionMs; } - bool hasAudio() const { return m_hasAudio; } + qint64 durationMs() const { return m_durationMs; } // Длительность контейнера/видеопотока, 0 — неизвестна либо stop. + qint64 positionMs() const { return m_positionMs; } // Последняя позиция часов/seek; при Finished — конец принятого медиа. + bool hasAudio() const { return m_hasAudio; } // Найден аудиотрек; становится false при отказе декодирования/PCM или stop. int videoWidth() const { return m_videoWidth; } int videoHeight() const { return m_videoHeight; } QString filePath() const { return m_path; } - QImage currentFrame() const { return m_renderFrame; } + QImage currentFrame() const { return m_renderFrame; } // Неизменяемый снимок показанного RGB-кадра, пустой до первого показа/после stop. signals: - void opened(int width, int height, qint64 durationMs, bool hasAudio); - void positionChanged(qint64 ms); - void stateChanged(int state); + void opened(int width, int height, qint64 durationMs, bool hasAudio); // Метаданные найдены; декодирование/PCM ещё могут завершиться ошибкой. + void positionChanged(qint64 ms); // Текущая позиция при показе либо seek; при pause таймер остановлен. + void stateChanged(int state); // Новое значение State уже записано; одинаковое состояние повторно не публикуется. void finished(); // Текущее воспроизведение завершено; все поля обновлены до сигнала, можно close/open/seek. - void error(const QString& msg); - void framePresented(); + void error(const QString& msg); // Ошибка текущего open; воспроизведение остановлено до сигнала. + void framePresented(); // currentFrame уже обновлён; запоздалые промежуточные кадры могут пропускаться. private slots: void onWorkerOpened(int width, int height, qint64 durationMs, bool hasAudio); @@ -68,36 +68,36 @@ private: qint64 clockMs(); // PCM, затем непрерывный монотонный таймер после окончания аудио. void clearPending(); - uint64_t m_openGeneration = 0; - QString m_path; - QThread* m_thread = nullptr; - class Worker* m_worker = nullptr; + uint64_t m_openGeneration = 0; // Меняется при open/stop; отбрасывает queued-сигналы прежнего файла. + QString m_path; // Последний запрошенный путь, сохраняется при stop/close. + QThread* m_thread = nullptr; // Владеет engine; worker исполняет run до abort. + class Worker* m_worker = nullptr; // Создаётся на open, удаляется на close. - State m_state = Stopped; - qint64 m_durationMs = 0; - qint64 m_positionMs = 0; - bool m_hasAudio = false; - int m_videoWidth = 0; - int m_videoHeight = 0; + State m_state = Stopped; // Публичное состояние показа, независимо от опережающего декодирования. + qint64 m_durationMs = 0; // Из метаданных, используется для ограничения seek/слайдера. + qint64 m_positionMs = 0; // Мс; при pause удерживается, при stop сбрасывается. + bool m_hasAudio = false; // Есть аудиотрек, для которого ещё не потребовался fallback на системные часы. + int m_videoWidth = 0; // Исходные размеры видеопотока из opened, сохраняются при stop. + int m_videoHeight = 0; // Высота в пикселях. /* управление worker'ом (атомики, доступны из обоих потоков) */ - QAtomicInt m_abort{0}; - QAtomicInt m_paused{0}; - QAtomicInteger m_seekTargetMs{0}; - QAtomicInt m_buffered{0}; + QAtomicInt m_abort{0}; // 1 — прекратить run. + QAtomicInt m_paused{0}; // 1 — удерживать декодирование видео; seek всё равно обрабатывается. + QAtomicInteger m_seekTargetMs{0}; // Цель последнего seek в мс, публикуется перед m_epoch. + QAtomicInt m_buffered{0}; // Все отправленные, ещё не показанные/отброшенные кадры, включая queued-сигналы Qt. QAtomicInt m_epoch{0}; // GUI публикует новый target до release-инкремента; worker обрабатывает каждую последнюю эпоху. /* презентация (только GUI-поток) */ - std::deque> m_pending; - QImage m_renderFrame; + std::deque> m_pending; // RGB-кадр + PTS в мс, принятые GUI и ожидающие своих часов. + QImage m_renderFrame; // Последний представленный кадр, общий снимок для currentFrame. bool m_decodeEof = false; // EOF только текущей эпохи; не меняет состояние/возможность паузы. qint64 m_videoEndMs = 0; // Максимальный конец принятого кадра: PTS + его длительность. qint64 m_drainReportAt = 0; // Последний DEBUG-снимок доигрывания по m_elapsed, мс; не чаще раза в секунду. - QTimer m_presentTimer; - uint64_t m_audioPlayback = 0; - int m_audioSampleRate = 0; + QTimer m_presentTimer; // GUI tick 16мс; работает только во время Playing. + uint64_t m_audioPlayback = 0; // Принадлежащая engine PCM-сессия SoundManager, 0 — отсутствует. + int m_audioSampleRate = 0; // Гц принятого PCM; 0 при отсутствии аудио/fallback. uint64_t m_audioFrames = 0; // Полная длина PCM на канал; сессия сохраняется после Finished для replay/seek. bool m_audioClock = false; // Часы ещё следуют PCM; после его конца продолжаются через m_elapsed. - QElapsedTimer m_elapsed; - qint64 m_elapsedBaseMs = 0; + QElapsedTimer m_elapsed; // Монотонные часы без аудио либо после его конца; перезапускаются при play/seek. + qint64 m_elapsedBaseMs = 0; // Медиапозиция, соответствующая нулю m_elapsed, мс. };