Describe multiview tile lifetime, navigation and retry contracts

This commit is contained in:
evgeny committed 2026-10-28 00:58:59 +01:00
1 parent 1f0c7483e0
commit a03abcaa02
2 files changed
+41 -27

No files matched your search

+39 -25
View File
@@ -2,7 +2,9 @@
* Плитка сохраняет player/decoder/часы при перекладке; новая эпоха заменяет только соответствующий player.
* Своя плитка получает setLocalPreview до кодирования, без reader/audio; клик запрашивает управление.
* Чужая плитка разворачивается без пересоздания players; Esc/повторный клик возвращает сетку.
* Пропавшему источнику даётся дочитать FIN, затем плитка удаляется. Пустое окно ждёт новые публикации. */
* Пропавшему источнику даётся дочитать FIN, затем плитка удаляется. Пустое окно ждёт новые публикации.
* Все методы — GUI-поток; диспетчер передаёт снимки через onSources, publisher — кадры через setLocalPreview.
* Закрытие терминально: останавливает таймер/плитки и снимает свой watch. WA_DeleteOnClose задаёт владелец окна. */
#pragma once
#include <QWidget>
#include <QTimer>
@@ -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<int>(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<Tile> m_tiles;
uint64_t m_group, m_owner; // Группа и независимый process-local owner каталога.
std::vector<Tile> 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<QObject> 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<QObject> 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 и уничтожить плитки; повтор безопасен.
};
+2 -2
View File
@@ -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,