/* Окно всех активных трансляций одного канала. Собственный catalogue owner не зависит от выбранного чата. * Плитка сохраняет player/decoder/часы при перекладке; новая эпоха заменяет только соответствующий player. * Своя плитка получает setLocalPreview до кодирования, без reader/audio; клик запрашивает управление. * Чужая плитка разворачивается без пересоздания players; Esc/повторный клик возвращает сетку. * Пропавшему источнику даётся дочитать FIN, затем плитка удаляется. Пустое окно ждёт новые публикации. * Все методы — GUI-поток; диспетчер передаёт снимки через onSources, publisher — кадры через setLocalPreview. * Закрытие терминально: останавливает таймер/плитки и снимает свой watch. WA_DeleteOnClose задаёт владелец окна. */ #pragma once #include #include #include #include #include #include #include class StreamPlayerWindow; class QGridLayout; class QLabel; class QPushButton; 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); // Последний 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; // Имя заголовка/повторно открываемого player. bool self = false, previewPresented = false; // Прямой локальный preview; последний переданный ему кадр был валиден. QWidget* widget = nullptr; // Владеет player либо прямым preview, удаляется целиком. QWidget* video = nullptr; // Поверхность изображения; сюда направлены клики. StreamPlayerWindow* player = nullptr; // Только remote: владелец decoder, аудиочасов и viewer. QLabel* title = nullptr; // Подпись self-preview; подпись remote принадлежит StreamPlayerWindow. QLabel* status = nullptr; // Статус прямого preview; у remote им владеет player. 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; // Группа и независимый 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; // 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; // Состояние окна до разворачивания 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 и уничтожить плитки; повтор безопасен. };