diff --git a/tools/chatgui/src/streamplayer_window.h b/tools/chatgui/src/streamplayer_window.h index d3f2f8b9..c74c9104 100644 --- a/tools/chatgui/src/streamplayer_window.h +++ b/tools/chatgui/src/streamplayer_window.h @@ -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 @@ -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 m_playback; - bool m_audioAttached = false; - QTimer m_timer; - VideoRenderWidget* m_render = nullptr; + std::unique_ptr 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 и отбрасывание старых результатов. }; diff --git a/tools/chatgui/src/videoplayer_window.h b/tools/chatgui/src/videoplayer_window.h index 645e2373..d28794f5 100644 --- a/tools/chatgui/src/videoplayer_window.h +++ b/tools/chatgui/src/videoplayer_window.h @@ -1,3 +1,6 @@ +/* GUI-модуль: VideoRenderWidget показывает готовые QImage; VideoPlayerWindow владеет файловым + * VideoPlayerEngine и закрывается после полного проигрывания. Методы вызываются в GUI-потоке. + * Родитель QObject владеет дочерними виджетами; closed сообщает о closeEvent, само окно не удаляет. */ #pragma once #include @@ -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. };