Show silent local preview and focus remote desktop stream tiles

This commit is contained in:
evgeny committed 2026-10-20 20:32:52 +02:00
1 parent f1cccb1c92
commit 6dab5db56a
2 files changed
+164 -27

No files matched your search

+139 -25
View File
@@ -1,5 +1,6 @@
#include "streamgrid_window.h"
#include "streamplayer_window.h"
#include "videoplayer_window.h"
#include "stream_sources.h"
#include "../transport/stream_bridge.h"
#include <QVBoxLayout>
@@ -8,7 +9,14 @@
#include <QLabel>
#include <QPushButton>
#include <QCloseEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QApplication>
#include <QShortcut>
#ifdef HAVE_STREAM_CAPTURE
#include <QVideoWidget>
#include <QVideoSink>
#include <QVideoFrame>
#endif
#include <atomic>
#include <algorithm>
#include <cmath>
@@ -24,9 +32,15 @@ StreamGridWindow::StreamGridWindow(uint64_t group, QWidget* parent)
auto* layout = new QVBoxLayout(this); auto* controls = new QHBoxLayout;
m_status = new QLabel(text("Connecting…"), this); controls->addWidget(m_status, 1);
auto* fullscreen = new QPushButton(text("Fullscreen"), this); controls->addWidget(fullscreen);
auto* close = new QPushButton(text("Close player"), this); controls->addWidget(close); layout->addLayout(controls);
m_close = new QPushButton(text("Close player"), this); controls->addWidget(m_close); layout->addLayout(controls);
connect(fullscreen, &QPushButton::clicked, this, [this] { if (isFullScreen()) showNormal(); else showFullScreen(); });
connect(close, &QPushButton::clicked, this, &QWidget::close);
connect(m_close, &QPushButton::clicked, this, [this] { if (m_focused) restoreGrid(); else close(); });
auto* escape = new QShortcut(QKeySequence(Qt::Key_Escape), this); escape->setContext(Qt::WidgetWithChildrenShortcut);
connect(escape, &QShortcut::activated, this, [this] {
if (m_focused) restoreGrid();
else if (isFullScreen()) showNormal();
else close();
});
m_empty = new QLabel(text("Waiting for active streams…"), this); m_empty->setAlignment(Qt::AlignCenter);
layout->addWidget(m_empty);
m_grid = new QGridLayout; m_grid->setSpacing(4); layout->addLayout(m_grid, 1);
@@ -38,6 +52,103 @@ StreamGridWindow::StreamGridWindow(uint64_t group, QWidget* parent)
StreamGridWindow::~StreamGridWindow() { stop(); }
/* Self получает прямую поверхность издателя: ни worker, ни reader, ни вход микшера не создаются. */
void StreamGridWindow::createTile(Tile& tile) {
if (tile.self) {
tile.widget = new QWidget(this);
tile.widget->setObjectName(QStringLiteral("streamPreview_%1").arg(tile.source));
auto* layout = new QVBoxLayout(tile.widget); layout->setContentsMargins(3, 3, 3, 3); layout->setSpacing(2);
auto* name = new QLabel(tile.name, tile.widget); name->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
name->setToolTip(tile.name); layout->addWidget(name);
tile.status = new QLabel(text("Connecting…"), tile.widget); layout->addWidget(tile.status);
#ifdef HAVE_STREAM_CAPTURE
auto* preview = new QVideoWidget(tile.widget); preview->setAspectRatioMode(Qt::KeepAspectRatio);
tile.video = preview;
#else
tile.video = new QLabel(text("Video capture requires Qt Multimedia and FFmpeg"), tile.widget);
#endif
tile.video->setToolTip(text("Manage stream")); layout->addWidget(tile.video, 1);
updatePreview(tile);
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview local preview group=%llx source=%llx epoch=%llx decoder=0 audio=0",
(unsigned long long)m_group, (unsigned long long)tile.source, (unsigned long long)tile.epoch);
} else {
tile.player = new StreamPlayerWindow(m_group, tile.source, tile.epoch, tile.name, this, true);
tile.widget = tile.player; tile.video = tile.player->findChild<VideoRenderWidget*>();
}
if (!tile.video) {
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "desktop multiview tile has no video source=%llx", (unsigned long long)tile.source); return;
}
tile.video->installEventFilter(this);
for (auto* child : tile.video->findChildren<QWidget*>()) child->installEventFilter(this);
}
/* QVariant хранит shared QVideoFrame; оба окна читают последний захваченный кадр без перекодирования. */
void StreamGridWindow::setLocalPreview(uint64_t group, const QVariant& frame) {
if (m_closed || group != m_group) return;
m_preview = frame;
for (auto& tile : m_tiles) if (tile.self) updatePreview(tile);
}
void StreamGridWindow::updatePreview(Tile& tile) {
#ifdef HAVE_STREAM_CAPTURE
auto frame = m_preview.value<QVideoFrame>();
static_cast<QVideoWidget*>(tile.video)->videoSink()->setVideoFrame(frame);
tile.status->setText(text(frame.isValid() ? "Live" : "Connecting…"));
if (frame.isValid() && !tile.previewPresented)
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview first local frame source=%llx size=%dx%d decoder=0 audio=0",
(unsigned long long)tile.source, frame.width(), frame.height());
tile.previewPresented = frame.isValid();
#else
Q_UNUSED(tile);
#endif
}
/* Изменяется лишь видимость/геометрия: все удалённые players и их аудиочасы продолжают работать. */
void StreamGridWindow::activateTile(uint64_t source) {
auto tile = std::find_if(m_tiles.begin(), m_tiles.end(), [source](const auto& tile) { return tile.source == source; });
if (m_closed || tile == m_tiles.end()) {
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop multiview click on removed source=%llx", (unsigned long long)source); return;
}
if (tile->self) {
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview opens publisher group=%llx source=%llx",
(unsigned long long)m_group, (unsigned long long)source);
emit publishRequested(m_group);
} else if (m_focused == source) restoreGrid();
else {
if (!m_focused) m_gridWindowState = windowState();
m_focused = source; arrange(); showFullScreen();
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview focus source=%llx players_preserved=%zu",
(unsigned long long)source, m_tiles.size());
}
}
void StreamGridWindow::restoreGrid() {
if (!m_focused) return;
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview returns to grid source=%llx players_preserved=%zu",
(unsigned long long)m_focused, m_tiles.size());
m_focused = 0; setWindowState(m_gridWindowState); arrange();
}
/* Не перехватываем drag/колесо zoom. Активация после release даёт VideoRenderWidget завершить drag. */
bool StreamGridWindow::eventFilter(QObject* watched, QEvent* event) {
if (event->type() == QEvent::MouseButtonPress) {
auto* mouse = static_cast<QMouseEvent*>(event);
if (mouse->button() == Qt::LeftButton) { m_pressed = watched; m_pressPosition = mouse->globalPos(); m_dragged = false; }
} else if (event->type() == QEvent::MouseMove && m_pressed == watched) {
auto* mouse = static_cast<QMouseEvent*>(event);
if ((mouse->globalPos() - m_pressPosition).manhattanLength() >= QApplication::startDragDistance()) m_dragged = true;
} else if (event->type() == QEvent::MouseButtonRelease && m_pressed == watched) {
auto* mouse = static_cast<QMouseEvent*>(event); m_pressed = nullptr;
if (mouse->button() == Qt::LeftButton && !m_dragged &&
(mouse->globalPos() - m_pressPosition).manhattanLength() < QApplication::startDragDistance()) {
for (const auto& tile : m_tiles) if (tile.video == watched || tile.video->isAncestorOf(qobject_cast<QWidget*>(watched))) {
QMetaObject::invokeMethod(this, [this, source = tile.source] { activateTile(source); }, Qt::QueuedConnection); break;
}
}
}
return QWidget::eventFilter(watched, event);
}
/* Сверяем идентичности, не трогая остальные players при добавлении, удалении или новой эпохе. */
void StreamGridWindow::onSources(const uint8_t* data, int length) {
if (m_closed) return;
@@ -53,23 +164,22 @@ void StreamGridWindow::onSources(const uint8_t* data, int length) {
auto source = std::find_if(snapshot.sources.begin(), snapshot.sources.end(), [&](const auto& s) { return s.id == tile.source; });
if (source == snapshot.sources.end()) {
if (tile.removeAt < 0) {
tile.removeAt = m_clock.elapsed() + tile.player->drainTimeoutMs();
tile.removeAt = m_clock.elapsed() + (tile.self ? 0 : tile.player->drainTimeoutMs());
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview drains source=%llx deadline_ms=%lld",
(unsigned long long)tile.source, (long long)tile.removeAt);
}
continue;
}
tile.removeAt = -1;
if (tile.epoch == source->epoch) continue;
delete tile.player;
tile.epoch = source->epoch; tile.name = source->name; tile.failures = 0; tile.retryAt = 0;
tile.player = new StreamPlayerWindow(m_group, tile.source, tile.epoch, tile.name, this, true);
if (tile.epoch == source->epoch && tile.self == source->self) continue;
if (m_focused == tile.source) restoreGrid();
delete tile.widget;
tile = Tile{source->id, source->epoch, source->name, source->self}; createTile(tile);
changed = true;
}
for (const auto& source : snapshot.sources) {
if (std::any_of(m_tiles.begin(), m_tiles.end(), [&](const auto& tile) { return tile.source == source.id; })) continue;
Tile tile{source.id, source.epoch, source.name};
tile.player = new StreamPlayerWindow(m_group, tile.source, tile.epoch, tile.name, this, true);
Tile tile{source.id, source.epoch, source.name, source.self}; createTile(tile);
m_tiles.push_back(std::move(tile)); changed = true;
}
m_status->setText(text("Active streams: %1").arg(snapshot.sources.size()));
@@ -82,14 +192,22 @@ void StreamGridWindow::arrange() {
while (auto* item = m_grid->takeAt(0)) delete item;
for (int i = 0; i < m_side; ++i) { m_grid->setRowStretch(i, 0); m_grid->setColumnStretch(i, 0); }
m_side = m_tiles.empty() ? 0 : static_cast<int>(std::ceil(std::sqrt(static_cast<double>(m_tiles.size()))));
if (m_focused && std::none_of(m_tiles.begin(), m_tiles.end(), [this](const auto& tile) { return tile.source == m_focused; })) {
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview focused source removed source=%llx", (unsigned long long)m_focused);
m_focused = 0; setWindowState(m_gridWindowState);
}
for (int i = 0; i < m_side; ++i) { m_grid->setRowStretch(i, 1); m_grid->setColumnStretch(i, 1); }
for (size_t i = 0; i < m_tiles.size(); ++i) {
m_grid->addWidget(m_tiles[i].player, static_cast<int>(i) / m_side, static_cast<int>(i) % m_side);
m_tiles[i].player->show();
auto& tile = m_tiles[i];
if (m_focused && m_focused != tile.source) { tile.widget->hide(); continue; }
if (m_focused) m_grid->addWidget(tile.widget, 0, 0, m_side, m_side);
else m_grid->addWidget(tile.widget, static_cast<int>(i) / m_side, static_cast<int>(i) % m_side);
tile.widget->show();
}
m_empty->setVisible(m_tiles.empty());
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview layout group=%llx tiles=%zu grid=%dx%d",
(unsigned long long)m_group, m_tiles.size(), m_side, m_side);
m_close->setText(text(m_focused ? "All active streams" : "Close player"));
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview layout group=%llx tiles=%zu grid=%dx%d focused=%llx",
(unsigned long long)m_group, m_tiles.size(), m_side, m_side, (unsigned long long)m_focused);
}
/* Сбой одного decoder повторяется с backoff, окончание и исчезновение не сбрасывают соседние плитки. */
@@ -100,20 +218,19 @@ void StreamGridWindow::tick() {
qint64 now = m_clock.elapsed();
for (auto it = m_tiles.begin(); it != m_tiles.end();) {
if (it->removeAt >= 0) {
if (it->player->finished() || now >= it->removeAt) {
if (!it->player->finished())
if (it->self || it->player->finished() || now >= it->removeAt) {
if (!it->self && !it->player->finished())
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop multiview source disappeared before drain source=%llx",
(unsigned long long)it->source);
delete it->player; it = m_tiles.erase(it); changed = true; continue;
delete it->widget; it = m_tiles.erase(it); changed = true; continue;
}
} else if (it->player->failed()) {
} else if (!it->self && it->player->failed()) {
if (!it->retryAt) {
it->retryAt = now + std::min(10000u, 500u << std::min(it->failures++, 5u));
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop multiview retry source=%llx attempt=%u delay_ms=%lld",
(unsigned long long)it->source, it->failures, (long long)(it->retryAt - now));
} else if (now >= it->retryAt) {
delete it->player;
it->player = new StreamPlayerWindow(m_group, it->source, it->epoch, it->name, this, true);
delete it->widget; createTile(*it);
it->retryAt = 0; changed = true;
}
}
@@ -125,12 +242,9 @@ void StreamGridWindow::tick() {
void StreamGridWindow::stop() {
if (m_closed) return;
m_closed = true; m_timer.stop(); gui_stream_watch(0, m_owner);
for (auto& tile : m_tiles) delete tile.player;
for (auto& tile : m_tiles) delete tile.widget;
m_tiles.clear();
m_preview.clear();
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop multiview closed group=%llx", (unsigned long long)m_group);
}
void StreamGridWindow::closeEvent(QCloseEvent* event) { stop(); QWidget::closeEvent(event); }
void StreamGridWindow::keyPressEvent(QKeyEvent* event) {
if (event->key() == Qt::Key_Escape) { if (isFullScreen()) showNormal(); else close(); }
else QWidget::keyPressEvent(event);
}