diff --git a/tools/chatgui/src/streamgrid_window.h b/tools/chatgui/src/streamgrid_window.h index a879e549..4134b92b 100644 --- a/tools/chatgui/src/streamgrid_window.h +++ b/tools/chatgui/src/streamgrid_window.h @@ -2,7 +2,9 @@ * Плитка сохраняет player/decoder/часы при перекладке; новая эпоха заменяет только соответствующий player. * Своя плитка получает setLocalPreview до кодирования, без reader/audio; клик запрашивает управление. * Чужая плитка разворачивается без пересоздания players; Esc/повторный клик возвращает сетку. - * Пропавшему источнику даётся дочитать FIN, затем плитка удаляется. Пустое окно ждёт новые публикации. */ + * Пропавшему источнику даётся дочитать FIN, затем плитка удаляется. Пустое окно ждёт новые публикации. + * Все методы — GUI-поток; диспетчер передаёт снимки через onSources, publisher — кадры через setLocalPreview. + * Закрытие терминально: останавливает таймер/плитки и снимает свой watch. WA_DeleteOnClose задаёт владелец окна. */ #pragma once #include #include @@ -21,52 +23,64 @@ class QCloseEvent; class StreamGridWindow : public QWidget { Q_OBJECT public: + // group — ненулевая CHAT-группа; parent владеет окном по правилам QWidget, конструктор начинает свой watch. explicit StreamGridWindow(uint64_t group, QWidget* parent = nullptr); + // Идемпотентно остановить viewers/watch; уничтожение плиток завершает их workers. ~StreamGridWindow() override; + // Неизменная группа этого окна. uint64_t group() const { return m_group; } + // Заимствованный CHAT_EVT_STREAM_SOURCES: ошибка/другая группа игнорируются; valid=false сохраняет все плитки. + // Новая эпоха или смена self пересоздаёт только соответствующую плитку, остальные сохраняют playback. void onSources(const uint8_t* data, int length); - void setLocalPreview(uint64_t group, const QVariant& frame); // Последний QVideoFrame локального publisher, GUI-поток. + // Последний QVideoFrame локального publisher в QVariant; пустой кадр сбрасывает preview, другая группа игнорируется. + void setLocalPreview(uint64_t group, const QVariant& frame); + // Число плиток, включая исчезнувшие источники, пока они дочитывают остаток. int tileCount() const { return static_cast(m_tiles.size()); } + // Сторона сетки ceil(sqrt(tileCount)); 0 для пустого окна, не меняется при разворачивании одной плитки. int gridSide() const { return m_side; } + // ID развёрнутой чужой плитки; 0 означает общий вид. uint64_t focusedSource() const { return m_focused; } signals: void publishRequested(quint64 group); // Открыть управление своей публикацией, сохранив это окно. protected: + // stop до штатного закрытия QWidget; повторное show не возобновляет просмотр. void closeEvent(QCloseEvent* event) override; + // Отложенная активация по короткому клику; drag/колесо остаются у видео для pan/zoom. bool eventFilter(QObject* watched, QEvent* event) override; private: struct Tile { - uint64_t source, epoch; - QString name; - bool self = false, previewPresented = false; + uint64_t source, epoch; // Узел источника и эпоха публикации из каталога. + QString name; // Имя заголовка/повторно открываемого player. + bool self = false, previewPresented = false; // Прямой локальный preview; хотя бы один его кадр уже был показан. QWidget* widget = nullptr; // Владеет player либо прямым preview, удаляется целиком. QWidget* video = nullptr; // Поверхность изображения; сюда направлены клики. - StreamPlayerWindow* player = nullptr; + StreamPlayerWindow* player = nullptr; // Только remote: владелец decoder, аудиочасов и viewer. QLabel* status = nullptr; // Статус прямого preview; у remote им владеет player. - qint64 removeAt = -1, retryAt = 0; - unsigned failures = 0; + qint64 removeAt = -1, retryAt = 0; // Дедлайны m_clock в мс: -1 не удалять, 0 повтор не назначен. + unsigned failures = 0; // Backoff decoder: 500мс, 1/2/4/8с, далее максимум 10с. }; - uint64_t m_group, m_owner; - std::vector m_tiles; + uint64_t m_group, m_owner; // Группа и независимый process-local owner каталога. + std::vector m_tiles; // Каждый widget принадлежит окну; удаляется целиком при смене идентичности/stop. + // Элементы интерфейса принадлежат QObject-дереву этого окна. QGridLayout* m_grid = nullptr; QLabel* m_status = nullptr; QLabel* m_empty = nullptr; QPushButton* m_close = nullptr; QVariant m_preview; // Только последний кадр издателя; сеть/декодер для self не открываются. - QTimer m_timer; - QElapsedTimer m_clock; - int m_side = 0; - bool m_watching = false, m_closed = false; + QTimer m_timer; // 250мс: повтор watch, backoff decoder и завершение исчезнувших источников. + QElapsedTimer m_clock; // Общие монотонные часы дедлайнов плиток. + int m_side = 0; // Последняя рассчитанная сторона сетки. + bool m_watching = false, m_closed = false; // Watch принят bridge; окно терминально остановлено. uint64_t m_focused = 0; // Source развёрнутой чужой плитки; 0 — сетка. - Qt::WindowStates m_gridWindowState; - QPointer m_pressed; - QPoint m_pressPosition; - bool m_dragged = false; - void createTile(Tile& tile); - void updatePreview(Tile& tile); - void activateTile(uint64_t source); - void restoreGrid(); - void arrange(); - void tick(); - void stop(); + Qt::WindowStates m_gridWindowState; // Состояние окна до разворачивания remote-плитки. + QPointer m_pressed; // Поверхность нажатия; удаление плитки безопасно обнуляет указатель. + QPoint m_pressPosition; // Глобальная позиция нажатия для отличия клика от drag. + bool m_dragged = false; // Пройден QApplication::startDragDistance. + void createTile(Tile& tile); // Создать self-preview либо embedded player, установить фильтры кликов. + void updatePreview(Tile& tile); // Передать m_preview поверхности self и обновить её статус. + void activateTile(uint64_t source); // Self запрашивает управление; remote разворачивается/возвращается в сетку. + void restoreGrid(); // Восстановить состояние окна и раскладку, сохранив playback всех плиток. + void arrange(); // Переложить/скрыть widgets, сохранив их decoder, reader и часы. + void tick(); // Повторить неудачный watch/decoder либо удалить дочитавшую/просроченную плитку. + void stop(); // Терминально остановить таймер/watch и уничтожить плитки; повтор безопасен. }; diff --git a/tools/chatgui/transport/stream_bridge.h b/tools/chatgui/transport/stream_bridge.h index 4a316077..b750e1a2 100644 --- a/tools/chatgui/transport/stream_bridge.h +++ b/tools/chatgui/transport/stream_bridge.h @@ -20,7 +20,7 @@ struct StreamRecord { uint64_t id = 0, pts = 0; // ID viewer и PTS этого источника в микросекундах; PTS значим для media. StreamRecordKind kind = StreamRecordKind::Gap; // Media либо событие открытия/разрыва/завершения трека. stream_media_format format{}; // Формат VIDEO/AUDIO; для управляющих событий нули. - int flags = 0, reason = 0; // VIDEO: STREAM_MEDIA_FLAG_KEY; CLOSED: stream_close_reason. + int flags = 0, reason = 0; // VIDEO: STREAM_MEDIA_KEY; CLOSED: stream_close_reason. QByteArray data; // Owned access unit/Opus; у управляющих событий пусто, живёт независимо от viewer/ядра. }; @@ -42,7 +42,7 @@ bool gui_stream_open(uint64_t id, uint64_t group, uint64_t source, uint64_t epoc void (*notify)(void*) = nullptr, void* arg = nullptr); /* Синхронно разбудить/снять notify и асинхронно закрыть viewer; старый id допустим, полученные records сохраняются. */ void gui_stream_stop(uint64_t id); -/* true — одна готовая запись записана в record. false — record не меняется; при наличии manager запрошена выдача. +/* true — одна готовая запись записана в record. false — record не меняется; при отсутствии готового ответа запрашивается выдача. * position<0 прогревает звук до KEY; иначе позиция playback в мкс. audioAhead=-1 отключает звук, иначе 0..60000000 мкс. * Это режим выдачи по одной записи: не смешивать с pull_batch на том же id без сброса reader. */ bool gui_stream_pull(uint64_t id, StreamRecord& record, int64_t position = -1,