Document desktop file and live player window contracts

This commit is contained in:
evgeny committed 2026-10-28 00:58:59 +01:00
1 parent cfa53af3a3
commit cb95cf7d27
2 files changed
+72 -61

No files matched your search

+24 -20
View File
@@ -1,6 +1,9 @@
/* Просмотр live: audio worker получает батчи и готовит PCM, отдельный worker владеет FFmpeg.
* GUI владеет окном; общий output смешивает PCM, callback сохраняет отдельные часы этой трансляции.
* Закрытие отсоединяет output, останавливает оба worker и снимает reader. */
/* Просмотр одной live-сессии (group, source, epoch): конструктор запускает подключение и worker.
* GUI владеет окном; audio worker получает батчи/готовит PCM, video worker единолично владеет FFmpeg.
* Общий SoundManager смешивает output; отдельные часы этого player учитывают PCM, задержку устройства и адаптацию tempo.
* GUI показывает готовые кадры/телеметрию; zoom/drag ограничены физическим 1:1. Пауза/seek не предусмотрены.
* Публичные методы вызываются в GUI-потоке; stop/close/деструктор отменяют свой reader, отсоединяют output и join обоих worker.
* После конца/ошибки окно остаётся открытым с последним кадром; родительский grid решает, когда удалить или пересоздать player. */
#pragma once
#include <QWidget>
@@ -16,33 +19,34 @@ class QKeyEvent;
class StreamPlayerWindow : public QWidget {
Q_OBJECT
public:
// group/source/epoch фиксируют media-сессию; name — подпись. embedded скрывает кнопки fullscreen/close и уплотняет layout.
explicit StreamPlayerWindow(uint64_t group, uint64_t source, uint64_t epoch, const QString& name,
QWidget* parent = nullptr, bool embedded = false);
~StreamPlayerWindow() override;
// GUI-поток: обновить подпись, tooltip и заголовок окна; decoder, reader, аудиочасы и громкость сохраняются.
void setSourceName(const QString& name);
void stop();
bool finished() const;
bool failed() const;
int drainTimeoutMs() const; // Остаток готового media + 2s для FIN; максимум 30s при исчезновении источника.
void stop(); // Идемпотентная окончательная остановка; повторного старта нет, сама finished/failed не устанавливает.
bool finished() const; // Полное естественное доигрывание либо терминальное закрытие/ошибка; не просто stop().
bool failed() const; // Ошибка, для которой grid должен переподключиться; штатные SOURCE_CHANGED/MEMBER/GROUP/SERVICE дают false.
int drainTimeoutMs() const; // finished → 0; иначе запас ready-clock в мс + 2000, ограниченный [2000, 30000], для grid.
signals:
void statusChanged(const QString& status);
void framePresented();
void statusChanged(const QString& status); // Статус/ошибка из worker; QLabel подключён queued. «Live»/«Stream ended» GUI пишет напрямую.
void framePresented(); // GUI передал новый кадр renderer; промежуточные опоздавшие кадры могут быть пропущены.
protected:
void closeEvent(QCloseEvent* event) override;
void keyPressEvent(QKeyEvent* event) override;
private:
struct Playback;
std::unique_ptr<Playback> m_playback;
bool m_audioAttached = false;
QTimer m_timer;
VideoRenderWidget* m_render = nullptr;
std::unique_ptr<Playback> m_playback; // Владелец reader-id, двух worker, очередей/часов и DSP; разделяемые данные под lock/атомиками.
bool m_audioAttached = false; // GUI зарегистрировал callback общего output; detach завершается до удаления Playback.
QTimer m_timer; // GUI tick 15мс: показ кадров, телеметрия и завершение output.
VideoRenderWidget* m_render = nullptr; // Дочерний renderer с zoom, сохраняющий вид при следующих кадрах того же размера.
QLabel* m_title = nullptr; // Подпись источника; принадлежит QObject-дереву окна, меняется без перезапуска playback.
QLabel* m_status = nullptr;
QLabel* m_telemetry = nullptr;
int64_t m_telemetryAt = 0;
void attachAudio();
void tick();
void decode();
void decodeVideo();
QLabel* m_status = nullptr; // Статус подключения, ожидания ключевого кадра, показа либо ошибки.
QLabel* m_telemetry = nullptr; // Rich text: цепочка имён/буферов и tempo; полные имена доступны в tooltip.
int64_t m_telemetryAt = 0; // Монотонное время последней телеметрии, микросекунды steady_clock.
void attachAudio(); // Регистрирует callback 48кГц stereo; он забирает ближайший PCM и учитывает часы DAC/generation output.
void tick(); // GUI берёт кадры, готовые по часам, и снимает output при полном доигрывании/ошибке.
void decode(); // Audio worker: открывает/закрывает свой reader, получает события/батчи, готовит PCM, управляет resync/tempo.
void decodeVideo(); // Video worker: ограниченная очередь кадров, revision/generation, FFmpeg drain и отбрасывание старых результатов.
};
+48 -41
View File
@@ -1,3 +1,6 @@
/* GUI-модуль: VideoRenderWidget показывает готовые QImage; VideoPlayerWindow владеет файловым
* VideoPlayerEngine и закрывается после полного проигрывания. Методы вызываются в GUI-потоке.
* Родитель QObject владеет дочерними виджетами; closed сообщает о closeEvent, само окно не удаляет. */
#pragma once
#include <QWidget>
@@ -11,13 +14,15 @@ class QPushButton;
class QLabel;
class VideoPlayerEngine;
/* Отрисовка кадра на чёрном фоне. zoomEnabled включает колесо до физического 1:1 и drag с ограничением краёв. */
/* Кадр на чёрном фоне с сохранением пропорций. Без zoom заполняет доступную область, включая увеличение.
* zoomEnabled ограничивает масштаб диапазоном от вписывания без увеличения до физического 1:1;
* колесо сохраняет точку под курсором, drag двигает только выступающие части кадра. */
class VideoRenderWidget : public QWidget {
Q_OBJECT
public:
explicit VideoRenderWidget(QWidget* parent = nullptr, bool zoomEnabled = false);
void setFrame(const QImage& img);
void clearFrame();
void setFrame(const QImage& img); // Shared-снимок кадра; в zoom смена размеров сбрасывает масштаб/drag, равный размер сохраняет вид.
void clearFrame(); // Удаляет кадр, масштаб и drag, возвращает обычный курсор и чёрный фон.
protected:
bool event(QEvent* event) override;
@@ -29,30 +34,32 @@ protected:
void mouseReleaseEvent(QMouseEvent* event) override;
private:
qreal frameScale() const;
void constrainView(const char* reason);
void finishDrag();
qreal frameScale() const; // Логические пиксели Qt на пиксель изображения; 0 без кадра/доступной области.
void constrainView(const char* reason); // Ограничивает масштаб/смещение после zoom, resize или смены экрана; reason — для TRACE.
void finishDrag(); // Завершает pan без сброса выбранного участка; также при hide, потере окна/захвата мыши.
QImage m_frame;
const bool m_zoomEnabled;
QImage m_frame; // Последний shared-кадр, пустой рисуется чёрным.
const bool m_zoomEnabled; // Выбирается при создании; файловое окно использует false, live-плеер — true.
qreal m_scale = 0; // Физические пиксели на пиксель кадра; 0 — автоматическое вписывание.
QPointF m_offset; // Смещение от центра в пикселях исходного кадра.
QPointF m_lastMouse;
bool m_panning = false;
QPointF m_lastMouse; // Предыдущая позиция мыши в логических координатах виджета во время pan.
bool m_panning = false; // Активен drag левой кнопкой на увеличенном кадре.
};
/* Отдельное окно плеера без рамки: drag-перенос, dblclick/Alt+Enter — fullscreen,
* Esc — выход/закрытие, timeline + play/pause/close. */
/* Файловое окно без рамки: drag-перенос, resize краями с текущими пропорциями и минимумом ширины 160.
* Dblclick/Alt+Enter — fullscreen, Esc — выйти из fullscreen либо закрыть, Space — play/pause.
* Размер/центр выбираются по primary screen; видео больше доступной области открывается fullscreen.
* В fullscreen панель/курсор скрываются через 2500мс, кроме Paused/Finished; движение мыши показывает панель. */
class VideoPlayerWindow : public QWidget {
Q_OBJECT
public:
explicit VideoPlayerWindow(QWidget* parent = nullptr);
~VideoPlayerWindow() override;
void playFile(const QString& path);
void playFile(const QString& path); // Показывает окно, сбрасывает кадр/шкалу и асинхронно открывает новый путь через engine.
signals:
void closed();
void closed(); // Engine уже закрыт; владелец может запланировать deleteLater (MainWindow так и делает).
protected:
void keyPressEvent(QKeyEvent* event) override;
@@ -70,11 +77,11 @@ private slots:
void onStateChanged(int state);
void onError(const QString& msg);
void onFramePresented();
void onSliderPressed();
void onSliderReleased();
void onSliderPressed(); // Удерживает значение ползунка от обновлений engine; воспроизведение продолжается.
void onSliderReleased(); // Seek к значению в мс, затем возобновление, включая ранее приостановленный файл.
void onPlayPause();
void onClose();
void onFinished();
void onFinished(); // Finished engine означает доигранный файл; закрывает engine и окно.
void showControls();
void hideControls();
@@ -85,30 +92,30 @@ private:
ResizeTopLeft, ResizeTopRight, ResizeBottomLeft, ResizeBottomRight
};
void toggleFullscreen();
void updateTimeLabels(qint64 posMs);
void setControlsVisible(bool visible);
QSize fitWindowSize(int vw, int vh) const;
ResizeMode hitTestResize(const QPoint& localPos) const;
void updateResizeCursor(const QPoint& localPos);
void applyResize(const QPoint& globalPos);
void beginResize(ResizeMode m, const QPoint& globalPos);
void toggleFullscreen(); // Переключает нормальное/полноэкранное состояние, настраивает видимость панели и курсора.
void updateTimeLabels(qint64 posMs); // Отображает текущую позицию/длительность как минуты:секунды.
void setControlsVisible(bool visible); // Панель и отдельная верхняя кнопка закрытия показываются/скрываются вместе.
QSize fitWindowSize(int vw, int vh) const; // Вписывает исходные размеры в primary screen без увеличения; некорректные → 640×360.
ResizeMode hitTestResize(const QPoint& localPos) const; // Край/угол в полосе 6 логических пикселей; иначе ResizeNone.
void updateResizeCursor(const QPoint& localPos); // Курсор соответствует краю/углу окна.
void applyResize(const QPoint& globalPos); // Пересчитывает геометрию от начала жеста, сохраняет исходные пропорции окна.
void beginResize(ResizeMode m, const QPoint& globalPos); // Запоминает режим, геометрию и глобальную позицию начала жеста.
VideoRenderWidget* m_render = nullptr;
QWidget* m_controls = nullptr;
QSlider* m_slider = nullptr;
QPushButton* m_playBtn = nullptr;
QPushButton* m_closeBtn = nullptr;
QLabel* m_timeLabel = nullptr;
VideoPlayerEngine* m_engine = nullptr;
VideoRenderWidget* m_render = nullptr; // Дочерний renderer без zoom, растянут на всё окно и прозрачен для мыши.
QWidget* m_controls = nullptr; // Нижняя панель высотой 48; пустые поля маршрутизируют resize нижнего края.
QSlider* m_slider = nullptr; // Позиция в мс, диапазон 0..duration; клик прыгает к выбранной точке.
QPushButton* m_playBtn = nullptr; // Play/pause и соответствующая подсказка.
QPushButton* m_closeBtn = nullptr; // Кнопка поверх кадра справа вверху, вне нижней панели.
QLabel* m_timeLabel = nullptr; // Позиция/длительность либо текст ошибки engine.
VideoPlayerEngine* m_engine = nullptr; // Дочерний файловый плеер; closeEvent освобождает его media-ресурсы.
bool m_fullscreen = false;
bool m_sliderDragging = false;
bool m_dragging = false;
QPoint m_dragStart;
QPoint m_windowStart;
ResizeMode m_resizeMode = ResizeNone;
QRect m_resizeStartGeom;
QPoint m_resizeStartGlobal;
QTimer* m_hideTimer = nullptr;
bool m_fullscreen = false; // Режим, переключаемый логикой окна.
bool m_sliderDragging = false; // Не подменять значение слайдера текущей позицией engine.
bool m_dragging = false; // Перенос окна за область кадра левой кнопкой.
QPoint m_dragStart; // Глобальная позиция мыши в начале переноса.
QPoint m_windowStart; // Верхний левый угол frameGeometry в начале переноса.
ResizeMode m_resizeMode = ResizeNone; // Активный край/угол либо отсутствие жеста resize.
QRect m_resizeStartGeom; // Геометрия до resize; её пропорции сохраняются на протяжении жеста.
QPoint m_resizeStartGlobal; // Начальная глобальная позиция мыши для resize.
QTimer* m_hideTimer = nullptr; // Дочерний одноразовый таймер 2500мс, скрытие разрешено только в fullscreen.
};