Preserve early seek position and document playback lifecycle
This commit is contained in:
1 parent
e371cc4dd9
commit
71f5cc9c94
2 files changed
+52
-43
No files matched your search
@@ -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<VideoPlayerEngine> 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<std::vector<int16_t>> 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) {
|
||||
|
||||
@@ -25,35 +25,35 @@ Q_DECLARE_METATYPE(std::shared_ptr<std::vector<int16_t>>)
|
||||
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<qint64> m_seekTargetMs{0};
|
||||
QAtomicInt m_buffered{0};
|
||||
QAtomicInt m_abort{0}; // 1 — прекратить run.
|
||||
QAtomicInt m_paused{0}; // 1 — удерживать декодирование видео; seek всё равно обрабатывается.
|
||||
QAtomicInteger<qint64> m_seekTargetMs{0}; // Цель последнего seek в мс, публикуется перед m_epoch.
|
||||
QAtomicInt m_buffered{0}; // Все отправленные, ещё не показанные/отброшенные кадры, включая queued-сигналы Qt.
|
||||
QAtomicInt m_epoch{0}; // GUI публикует новый target до release-инкремента; worker обрабатывает каждую последнюю эпоху.
|
||||
|
||||
/* презентация (только GUI-поток) */
|
||||
std::deque<std::pair<QImage, qint64>> m_pending;
|
||||
QImage m_renderFrame;
|
||||
std::deque<std::pair<QImage, qint64>> 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, мс.
|
||||
};
|
||||
Reference in new issue
Block a user