481 lines
30 KiB
C++
481 lines
30 KiB
C++
#include "stream_publish_window.h"
|
||
#include "capture_controls.h"
|
||
#include "sound_manager.h"
|
||
#include "debug_config.h"
|
||
#include <QVBoxLayout>
|
||
#include <QCoreApplication>
|
||
#include <QHBoxLayout>
|
||
#include <QFormLayout>
|
||
#include <QCheckBox>
|
||
#include <QLabel>
|
||
#include <QMessageBox>
|
||
#include <QPushButton>
|
||
#include <QMenuBar>
|
||
#include <QMenu>
|
||
#include <QActionGroup>
|
||
#include <QWidgetAction>
|
||
#include <QToolButton>
|
||
#include <QPainter>
|
||
#include <QSignalBlocker>
|
||
#include <QJsonDocument>
|
||
#include <QScopedValueRollback>
|
||
#include <QCloseEvent>
|
||
#include <QShortcut>
|
||
#include <algorithm>
|
||
#ifdef HAVE_STREAM_CAPTURE
|
||
#include <QVideoWidget>
|
||
#include <QVideoSink>
|
||
#include <QVideoFrame>
|
||
#endif
|
||
|
||
namespace {
|
||
constexpr int MaximumManualBitrate = 12000000;
|
||
QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); }
|
||
QString bytes(uint64_t value) { return QString::number(value / 1024.0, 'f', 1) + " KiB"; }
|
||
|
||
using namespace CaptureControls;
|
||
}
|
||
|
||
/* Горизонталь — сохранённое окно источника, одна линия объединяет субпотоки получателя. */
|
||
class StreamBufferIndicator : public QWidget {
|
||
public:
|
||
explicit StreamBufferIndicator(QWidget* parent) : QWidget(parent) {
|
||
setObjectName(QStringLiteral("streamBufferIndicator"));
|
||
setPositions({});
|
||
}
|
||
void setPositions(const stream_buffer_positions& positions) {
|
||
m_positions = positions;
|
||
QString description = text("Send buffer: %1 receivers. Left: oldest data; right: live. Slowest receivers first.")
|
||
.arg(positions.total);
|
||
for (unsigned i = 0; i < positions.count; i++) {
|
||
const auto& peer = positions.peers[i];
|
||
description += QStringLiteral("\n%1: %2–%3% (%4)").arg(peer.peer, 0, 16)
|
||
.arg(fraction(peer.worst) * 100, 0, 'f', 1).arg(fraction(peer.best) * 100, 0, 'f', 1).arg(peer.subflows);
|
||
}
|
||
setToolTip(description); setAccessibleName(description); update();
|
||
}
|
||
protected:
|
||
void paintEvent(QPaintEvent*) override {
|
||
QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing);
|
||
QColor background = palette().window().color();
|
||
painter.setPen(Qt::NoPen); painter.setBrush(background.lightness() < 128 ? background.lighter(135) : background.darker(106));
|
||
painter.drawRoundedRect(rect(), 5, 5);
|
||
QRectF area = QRectF(rect()).adjusted(3, 3, -3, -3);
|
||
if (!m_positions.count) {
|
||
QColor color = palette().windowText().color(); color.setAlphaF(0.3);
|
||
painter.setPen(QPen(color, 2)); painter.drawLine(QPointF(area.left(), area.center().y()), QPointF(area.right(), area.center().y()));
|
||
return;
|
||
}
|
||
const QColor colors[] = {QColor(225, 91, 85, 160), QColor(225, 173, 54, 160), QColor(64, 184, 120, 160)};
|
||
double pitch = area.height() / m_positions.count, thickness = std::min(4.0, pitch * 0.65);
|
||
for (unsigned i = 0; i < m_positions.count; i++) {
|
||
double y = area.top() + pitch * (i + 0.5);
|
||
for (unsigned zone = 0; zone < 3; zone++)
|
||
painter.fillRect(QRectF(area.left() + area.width() * zone / 3, y - thickness / 2, area.width() / 3, thickness), colors[zone]);
|
||
double worst = area.left() + area.width() * fraction(m_positions.peers[i].worst);
|
||
double best = area.left() + area.width() * fraction(m_positions.peers[i].best);
|
||
painter.setPen(QPen(palette().windowText(), thickness)); painter.drawLine(QPointF(worst, y), QPointF(best, y));
|
||
painter.setPen(QPen(palette().windowText(), 1));
|
||
painter.drawLine(QPointF(worst, y - thickness), QPointF(worst, y + thickness));
|
||
painter.drawLine(QPointF(best, y - thickness), QPointF(best, y + thickness));
|
||
}
|
||
}
|
||
private:
|
||
stream_buffer_positions m_positions{};
|
||
double fraction(uint64_t next) const {
|
||
if (m_positions.head <= m_positions.first) return 1;
|
||
next = std::clamp(next, m_positions.first, m_positions.head);
|
||
return double(next - m_positions.first) / double(m_positions.head - m_positions.first);
|
||
}
|
||
};
|
||
|
||
StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t group, QWidget* parent, const QString& saved)
|
||
: QWidget(parent, Qt::Window), m_engine(engine), m_group(group) {
|
||
if (!saved.isEmpty()) {
|
||
QJsonParseError error; auto document = QJsonDocument::fromJson(saved.toUtf8(), &error);
|
||
if (error.error != QJsonParseError::NoError || !document.isObject())
|
||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop capture settings invalid: %s", qPrintable(error.errorString()));
|
||
else m_settings = document.object();
|
||
}
|
||
m_audioMuted = m_settings.value("muted").toBool();
|
||
setWindowTitle(text("Broadcast stream")); resize(900, 620); setMinimumSize(780, 360);
|
||
auto* escape = new QShortcut(QKeySequence(Qt::Key_Escape), this); escape->setContext(Qt::WidgetWithChildrenShortcut);
|
||
connect(escape, &QShortcut::activated, this, &QWidget::close);
|
||
auto* layout = new QVBoxLayout(this);
|
||
auto* controls = new QHBoxLayout; controls->setSpacing(4);
|
||
m_menuBar = new QMenuBar(this); m_menuBar->setNativeMenuBar(false); m_menuBar->setMouseTracking(true);
|
||
auto* menuStyle = new MenuStyle; menuStyle->setParent(this); m_menuBar->setStyle(menuStyle);
|
||
m_menuBar->setObjectName(QStringLiteral("streamControls"));
|
||
auto makeMenu = [this](const char* heading, const char* name) {
|
||
auto* menu = m_menuBar->addMenu(QString()); menu->setObjectName(QString::fromLatin1(name));
|
||
menu->menuAction()->setToolTip(text(heading)); menu->setAccessibleName(text(heading));
|
||
new QActionGroup(menu); menu->addSection(text(heading)); return menu;
|
||
};
|
||
m_source = makeMenu("Video source", "streamVideoSource");
|
||
m_codec = makeMenu("Video codec", "streamVideoCodec");
|
||
m_bitrate = makeMenu("Video bitrate", "streamVideoBitrate");
|
||
m_resolution = makeMenu("Video resolution", "streamVideoResolution");
|
||
m_fps = makeMenu("Frame rate", "streamVideoFps");
|
||
m_rotation = makeMenu("Rotation", "streamVideoRotation");
|
||
m_maxQp = makeMenu("Maximum QP", "streamMaxQp");
|
||
m_microphone = makeMenu("Audio source", "streamAudioSource");
|
||
auto* settingsMenu = makeMenu("Settings", "streamExtraSettings");
|
||
settingsMenu->menuAction()->setIcon(controlIcon(palette(), "settings"));
|
||
controls->addWidget(m_menuBar);
|
||
m_sound = new QToolButton(this); m_sound->setCheckable(true); m_sound->setIconSize(QSize(40, 36)); m_sound->setFixedSize(48, 48);
|
||
m_sound->setObjectName(QStringLiteral("streamSound")); m_sound->setAutoRaise(true); controls->addWidget(m_sound);
|
||
controls->addStretch();
|
||
m_start = new QPushButton(text("Start stream"), this); m_start->setMinimumHeight(40);
|
||
m_start->setObjectName(QStringLiteral("streamStart"));
|
||
m_buffer = new StreamBufferIndicator(this); m_buffer->setFixedSize(std::max(80, m_start->sizeHint().width()), 20);
|
||
controls->addWidget(m_buffer); controls->addWidget(m_start); layout->addLayout(controls);
|
||
#ifdef HAVE_STREAM_CAPTURE
|
||
auto* preview = new QVideoWidget(this); m_preview = preview;
|
||
preview->setMinimumHeight(160); preview->setAspectRatioMode(Qt::KeepAspectRatio);
|
||
preview->videoSink()->setVideoFrame(engine->previewFrame().value<QVideoFrame>());
|
||
connect(engine, &StreamPublishEngine::previewVideoChanged, this, [this, preview](const QVariant& frame) {
|
||
auto video = frame.value<QVideoFrame>();
|
||
if (!video.isValid() || isVisible()) preview->videoSink()->setVideoFrame(video);
|
||
});
|
||
#else
|
||
m_preview = new QLabel(text("Video capture requires Qt Multimedia and FFmpeg"), this);
|
||
#endif
|
||
layout->addWidget(m_preview, 1);
|
||
auto* settings = new QWidget(settingsMenu); auto* form = new QFormLayout(settings);
|
||
form->setRowWrapPolicy(QFormLayout::WrapLongRows);
|
||
auto* settingsAction = new QWidgetAction(settingsMenu); settingsAction->setDefaultWidget(settings); settingsMenu->addAction(settingsAction);
|
||
addOption(m_bitrate, text("Automatic"), 0);
|
||
for (int bitrate : {250000, 500000, 1000000, 2000000, 4000000, 8000000, MaximumManualBitrate})
|
||
addOption(m_bitrate, QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate);
|
||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop manual stream bitrate selection maximum_bps=%d", MaximumManualBitrate);
|
||
for (int angle : {0, 90, 270}) {
|
||
auto* action = addOption(m_rotation, QStringLiteral("%1°").arg(angle), angle);
|
||
action->setIcon(controlIcon(palette(), "rotation", action->text(), {}, angle));
|
||
}
|
||
m_cursor = new QCheckBox(text("Show mouse cursor"), settings); m_cursor->setChecked(true);
|
||
m_cursor->setObjectName(QStringLiteral("streamShowCursor")); form->addRow(m_cursor);
|
||
m_noAudio = addOption(m_microphone, text("No audio"), QVariant());
|
||
addOption(m_microphone, text("Default microphone"), QString()); select(m_microphone, QString());
|
||
for (const auto& device : SoundManager::enumerateCaptureDevices()) addOption(m_microphone, device.name, device.id);
|
||
m_inputChannels = new QLabel(settings); form->addRow(text("Audio input"), m_inputChannels);
|
||
for (int qp : {0, 18, 23, 28, 35, 40}) addOption(m_maxQp, qp ? QString::number(qp) : text("Automatic"), qp);
|
||
select(m_maxQp, 28);
|
||
m_maxQp->setToolTip(text("Lower QP preserves more detail and may require a higher bitrate. Applies to H.264 and H.265."));
|
||
m_adaptiveQp = new QCheckBox(text("Adapt QP to the target bitrate"), settings);
|
||
m_adaptiveQp->setObjectName(QStringLiteral("streamAdaptiveQp"));
|
||
m_adaptiveQp->setToolTip(text(
|
||
"Gradually raise the QP ceiling above the selected value when bitrate is too high, then return it. Requires live QP adjustment."));
|
||
form->addRow(m_adaptiveQp);
|
||
connect(m_sound, &QToolButton::toggled, this, [this](bool enabled) {
|
||
m_audioMuted = !enabled;
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop stream audio enabled=%d active=%d", enabled, m_engine->active());
|
||
if (m_engine->active()) m_engine->setMuted(!enabled);
|
||
else { updateMenuButtons(); saveSettings(); }
|
||
});
|
||
m_status = new QLabel(this); m_status->setWordWrap(true); layout->addWidget(m_status);
|
||
m_stats = new QLabel(this); m_stats->setObjectName(QStringLiteral("streamPublishStats"));
|
||
m_stats->setWordWrap(true); m_stats->setMinimumHeight(66); layout->addWidget(m_stats);
|
||
connect(m_start, &QPushButton::clicked, this, [this] {
|
||
if (!m_engine->active()) m_status->setProperty("streamError", QString());
|
||
if (m_engine->active()) m_engine->stop(); else m_engine->start(m_group, selection()); synchronize();
|
||
});
|
||
connect(m_source, &QMenu::triggered, this, [this](QAction* action) {
|
||
if (!action->isCheckable()) return;
|
||
if (m_updating) return;
|
||
auto previous = selection();
|
||
refreshModes(previous, previous.source.kind == StreamVideoSource::Screen); apply();
|
||
if (!m_engine->active()) synchronize();
|
||
});
|
||
connect(m_resolution, &QMenu::triggered, this, [this] {
|
||
populateFrameRates(m_fps, m_modes, value(m_resolution).toSize(), value(m_fps).toInt()); apply();
|
||
});
|
||
for (auto* menu : {m_fps, m_codec, m_bitrate, m_maxQp, m_rotation, m_microphone})
|
||
connect(menu, &QMenu::triggered, this, &StreamPublishWindow::apply);
|
||
for (auto* menu : {m_codec, m_maxQp}) connect(menu, &QMenu::triggered, this, [this] { if (!m_updating) synchronize(); });
|
||
connect(m_cursor, &QCheckBox::toggled, this, &StreamPublishWindow::apply);
|
||
connect(m_adaptiveQp, &QCheckBox::toggled, this, &StreamPublishWindow::apply);
|
||
connect(engine, &StreamPublishEngine::codecsChanged, this, &StreamPublishWindow::refreshCodecs);
|
||
connect(engine, &StreamPublishEngine::sourcesChanged, this, &StreamPublishWindow::refreshSources);
|
||
connect(engine, &StreamPublishEngine::configurationChanged, this, [this] { synchronize(); saveSettings(); });
|
||
connect(engine, &StreamPublishEngine::stateChanged, this, [this](const QString& value) {
|
||
auto error = m_status->property("streamError").toString();
|
||
m_status->setText(error.isEmpty() ? value : value + "\n" + error); synchronize();
|
||
});
|
||
connect(engine, &StreamPublishEngine::errorOccurred, this, [this](const QString& value) {
|
||
m_status->setProperty("streamError", value); m_status->setText(value);
|
||
});
|
||
connect(engine, &StreamPublishEngine::softwareEncodingRequested, this,
|
||
[this](uint64_t generation, const QString& codec, const QString& details) {
|
||
auto* question = new QMessageBox(QMessageBox::Warning, text("Hardware encoding is unavailable"),
|
||
text("Hardware encoding for %1 is unavailable in the selected video mode. Check the graphics driver or choose another codec.\n\n"
|
||
"Use CPU encoding instead? This may cause high CPU usage and slow down the computer.").arg(codec),
|
||
QMessageBox::Yes | QMessageBox::No, this);
|
||
question->setObjectName(QStringLiteral("streamCpuConfirmation"));
|
||
question->setDefaultButton(QMessageBox::No); question->setEscapeButton(QMessageBox::No);
|
||
question->setDetailedText(details); question->setAttribute(Qt::WA_DeleteOnClose);
|
||
auto* engine = m_engine;
|
||
connect(question, &QMessageBox::finished, engine, [engine, generation](int result) {
|
||
engine->answerSoftwareEncoding(generation, result == QMessageBox::Yes);
|
||
});
|
||
connect(question, &QObject::destroyed, engine,
|
||
[engine, generation] { engine->answerSoftwareEncoding(generation, false); }, Qt::QueuedConnection);
|
||
connect(engine, &StreamPublishEngine::stateChanged, question, [engine, generation, question] {
|
||
if (engine->softwareEncodingRequest() != generation) question->reject();
|
||
});
|
||
question->open();
|
||
});
|
||
connect(engine, &StreamPublishEngine::audioChannelsChanged, this, [this](int channels) { m_inputChannels->setText(channels == 2 ? text("Stereo") : text("Mono")); });
|
||
connect(engine, &StreamPublishEngine::telemetryChanged, this, [this](const stream_publisher_status& stats) {
|
||
m_buffer->setPositions(stats.buffer);
|
||
m_stats->setText(text("Current video bitrate: %1 Mbit/s").arg(m_engine->videoBitrate() / 1000000.0, 0, 'f', 2) + '\n' +
|
||
(m_engine->videoMaxQp() >= 0 ? text("Current QP ceiling: %1").arg(m_engine->videoMaxQp()) + '\n' : QString()) +
|
||
text("Video queue: %1 · Inflight: %2 · Send buffer: %3\nACK, all connections, last second: %4")
|
||
.arg(bytes(stats.video_bytes), bytes(stats.inflight_bytes), bytes(stats.total_bytes), bytes(stats.acked_1s)));
|
||
});
|
||
refreshSources(); refreshCodecs(); restoreSettings(); synchronize();
|
||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "desktop publisher control row rendered_icon=40x36 button=48x48 spacing=4 minimum_width=%d", minimumWidth());
|
||
}
|
||
|
||
void StreamPublishWindow::closeEvent(QCloseEvent* event) {
|
||
QWidget::closeEvent(event);
|
||
if (event->isAccepted()) {
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop publisher controls closed group=%llx active=%d",
|
||
(unsigned long long)m_group, m_engine->active());
|
||
emit closed();
|
||
}
|
||
}
|
||
|
||
/* В БД храним пользовательский выбор, а не runtime handles источника и рассчитанный auto-битрейт. */
|
||
void StreamPublishWindow::restoreSettings() {
|
||
if (m_settings.isEmpty() || m_engine->active()) return;
|
||
m_updating = true;
|
||
auto restore = [](QMenu* menu, const QVariant& data, const char* field) {
|
||
if (!select(menu, data))
|
||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop capture setting unavailable field=%s value=%s", field, qPrintable(data.toString()));
|
||
};
|
||
int sourceIndex = -1;
|
||
for (int i = 0; i < m_sources.size(); i++) {
|
||
const auto& source = m_sources[i];
|
||
if (source.kind != m_settings.value("source_kind").toInt()) continue;
|
||
bool matches = source.kind == StreamVideoSource::Window ? source.name == m_settings.value("source_name").toString()
|
||
: source.id == QByteArray::fromBase64(m_settings.value("source_id").toString().toLatin1());
|
||
if (matches) { sourceIndex = i; break; }
|
||
}
|
||
if (sourceIndex >= 0) select(m_source, sourceIndex);
|
||
else DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved capture source unavailable; using current source");
|
||
auto restored = selection();
|
||
restored.video.width = m_settings.value("width").toInt(1280); restored.video.height = m_settings.value("height").toInt(720);
|
||
restored.video.fps = m_settings.value("fps").toInt(30); restored.nativeSize = m_settings.value("native_size").toBool();
|
||
refreshModes(restored, sourceIndex < 0 && restored.source.kind == StreamVideoSource::Screen);
|
||
int bitrate = m_settings.value("bitrate").toInt();
|
||
if (bitrate > MaximumManualBitrate) {
|
||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved stream bitrate adjusted old_bps=%d maximum_bps=%d", bitrate, MaximumManualBitrate);
|
||
bitrate = MaximumManualBitrate;
|
||
}
|
||
if (bitrate > 0 && !option(m_bitrate, bitrate))
|
||
addOption(m_bitrate, QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate);
|
||
restore(m_bitrate, bitrate, "bitrate");
|
||
int maxQp = m_settings.value("max_qp").toInt(28);
|
||
if (maxQp < 0 || maxQp > 51) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved QP ceiling invalid value=%d", maxQp);
|
||
restore(m_maxQp, maxQp, "max_qp"); m_adaptiveQp->setChecked(m_settings.value("adaptive_qp").toBool());
|
||
restore(m_rotation, m_settings.value("rotation").toInt(), "rotation");
|
||
m_cursor->setChecked(m_settings.value("show_cursor").toBool(true));
|
||
if (m_settings.value("audio").toBool(true)) restore(m_microphone, m_settings.value("microphone").toString(), "microphone");
|
||
else m_noAudio->setChecked(true);
|
||
m_updating = false;
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop capture settings restored: %s", QJsonDocument(m_settings).toJson(QJsonDocument::Compact).constData());
|
||
}
|
||
|
||
void StreamPublishWindow::saveSettings() {
|
||
if (m_updating) return;
|
||
auto config = selection(); auto saved = m_settings;
|
||
if (selected(m_source)) {
|
||
saved["source_kind"] = config.source.kind;
|
||
saved["source_id"] = QString::fromLatin1(config.source.id.toBase64());
|
||
saved["source_name"] = config.source.kind == StreamVideoSource::Window ? config.source.name : QString();
|
||
}
|
||
if (selected(m_resolution) && selected(m_fps)) {
|
||
saved["width"] = config.video.width; saved["height"] = config.video.height; saved["fps"] = config.video.fps;
|
||
saved["native_size"] = config.nativeSize;
|
||
}
|
||
if (selected(m_codec)) saved["codec"] = config.video.codec;
|
||
saved["bitrate"] = value(m_bitrate).toInt(); saved["rotation"] = config.video.rotation;
|
||
saved["audio"] = config.audio; saved["microphone"] = config.microphone;
|
||
saved["muted"] = m_audioMuted;
|
||
saved["show_cursor"] = config.showCursor;
|
||
saved["max_qp"] = value(m_maxQp).toInt(); saved["adaptive_qp"] = m_adaptiveQp->isChecked();
|
||
if (saved == m_settings) return;
|
||
m_settings = saved;
|
||
auto json = QJsonDocument(saved).toJson(QJsonDocument::Compact);
|
||
DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop capture settings selected: %s", json.constData());
|
||
emit settingsChanged(QString::fromUtf8(json));
|
||
}
|
||
|
||
void StreamPublishWindow::refreshSources() {
|
||
QScopedValueRollback<bool> updating(m_updating, true);
|
||
auto config = m_engine->active() ? m_engine->config() : selection();
|
||
m_sources = StreamPublishEngine::sources(); clearOptions(m_source, "Video source");
|
||
int selected = -1;
|
||
for (int i = 0; i < m_sources.size(); i++) {
|
||
auto& source = m_sources[i];
|
||
if (source.sameSource(config.source)) {
|
||
selected = i;
|
||
if (m_engine->active() && !config.source.size.isEmpty()) source.size = config.source.size;
|
||
}
|
||
QString kind = source.kind == StreamVideoSource::Camera ? text("Camera") : source.kind == StreamVideoSource::Screen ? text("Screen") : text("Window");
|
||
QString label = kind + ": " + source.name;
|
||
if (source.kind == StreamVideoSource::Screen && !source.size.isEmpty())
|
||
label += QStringLiteral(" (%1 × %2)").arg(source.size.width()).arg(source.size.height());
|
||
addOption(m_source, label, i);
|
||
}
|
||
bool changed = selected < 0;
|
||
if (changed && !m_engine->active() && !m_sources.isEmpty()) selected = 0;
|
||
if (selected >= 0) select(m_source, selected);
|
||
else if (auto* action = ::selected(m_source)) {
|
||
auto* group = m_source->findChild<QActionGroup*>(); group->setExclusive(false); action->setChecked(false); group->setExclusive(true);
|
||
}
|
||
m_source->addSeparator();
|
||
connect(m_source->addAction(text("Refresh")), &QAction::triggered, this, &StreamPublishWindow::refreshSources);
|
||
refreshModes(config, changed && selected >= 0 && m_sources[selected].kind == StreamVideoSource::Screen);
|
||
synchronize();
|
||
}
|
||
|
||
/* Перестраиваем весь список под блокировкой, чтобы в engine не ушёл промежуточный режим другого источника. */
|
||
void StreamPublishWindow::refreshModes(const StreamPublishConfig& previous, bool maximum) {
|
||
QScopedValueRollback<bool> updating(m_updating, true);
|
||
clearOptions(m_resolution, "Video resolution"); clearOptions(m_fps, "Frame rate"); m_modes.clear();
|
||
int sourceIndex = selected(m_source) ? value(m_source).toInt() : -1;
|
||
if (sourceIndex < 0 || sourceIndex >= m_sources.size()) return;
|
||
const auto& source = m_sources[sourceIndex];
|
||
if (source.kind == StreamVideoSource::Camera) m_modes = source.modes;
|
||
else if (source.kind == StreamVideoSource::Screen) m_modes = stream_video_screen_modes(source.size);
|
||
else {
|
||
for (QSize size : {QSize(1920, 1080), QSize(1280, 720), QSize(640, 360)})
|
||
for (int fps : {15, 24, 25, 30, 50, 60}) m_modes.append({size, fps});
|
||
}
|
||
for (const auto& mode : m_modes) {
|
||
if (option(m_resolution, mode.size)) continue;
|
||
QString label = mode.size.isEmpty() ? text("Original resolution")
|
||
: QStringLiteral("%1 × %2").arg(mode.size.width()).arg(mode.size.height());
|
||
addOption(m_resolution, label, mode.size);
|
||
}
|
||
maximum |= source.kind == StreamVideoSource::Screen && previous.nativeSize;
|
||
StreamVideoMode wanted{QSize(previous.video.width, previous.video.height), previous.video.fps};
|
||
int selected = stream_video_select_mode(m_modes, wanted, maximum);
|
||
if (selected < 0) {
|
||
DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop source has no selectable modes source=%s", qPrintable(source.name));
|
||
m_resolution->menuAction()->setToolTip(text("No supported video modes")); return;
|
||
}
|
||
const auto& mode = m_modes[selected];
|
||
select(m_resolution, mode.size); populateFrameRates(m_fps, m_modes, mode.size, mode.fps);
|
||
if (!maximum && wanted != mode) DEBUG_WARN(DEBUG_CATEGORY_VIDEO,
|
||
"desktop video mode adjusted source=%s requested=%dx%d/%d selected=%dx%d/%d",
|
||
qPrintable(source.name), wanted.size.width(), wanted.size.height(), wanted.fps, mode.size.width(), mode.size.height(), mode.fps);
|
||
DEBUG_TRACE(DEBUG_CATEGORY_VIDEO, "desktop video modes source=%s count=%lld selected=%dx%d/%d native=%d",
|
||
qPrintable(source.name), (long long)m_modes.size(), mode.size.width(), mode.size.height(), mode.fps,
|
||
source.kind == StreamVideoSource::Screen && mode.size == m_modes.front().size);
|
||
}
|
||
|
||
void StreamPublishWindow::refreshCodecs() {
|
||
QScopedValueRollback<bool> updating(m_updating, true);
|
||
int previous = selected(m_codec) ? value(m_codec).toInt() : m_settings.value("codec").toInt();
|
||
clearOptions(m_codec, "Video codec");
|
||
for (int codec : m_engine->codecs()) addOption(m_codec, LiveVideoEncoder::codecName(codec), codec);
|
||
if (!select(m_codec, previous) && previous && selected(m_codec))
|
||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop saved codec=%d unavailable; using codec=%d", previous, value(m_codec).toInt());
|
||
synchronize();
|
||
}
|
||
StreamPublishConfig StreamPublishWindow::selection() const {
|
||
StreamPublishConfig config;
|
||
int sourceIndex = selected(m_source) ? value(m_source).toInt() : -1;
|
||
if (sourceIndex >= 0 && sourceIndex < m_sources.size()) config.source = m_sources[sourceIndex];
|
||
QSize size = value(m_resolution).toSize();
|
||
if (!size.isEmpty()) { config.video.width = size.width(); config.video.height = size.height(); }
|
||
if (selected(m_fps)) config.video.fps = value(m_fps).toInt();
|
||
config.nativeSize = config.source.kind == StreamVideoSource::Screen && !m_modes.isEmpty() && size == m_modes.front().size;
|
||
config.video.codec = value(m_codec).toInt(); config.video.rotation = value(m_rotation).toInt();
|
||
config.video.bitrate = value(m_bitrate).toInt();
|
||
config.automaticBitrate = !config.video.bitrate;
|
||
if (config.automaticBitrate) config.video.bitrate = std::clamp(config.video.width * config.video.height * config.video.fps / 11, 250000, 16000000);
|
||
if (config.video.codec == 1 || config.video.codec == 2) {
|
||
config.video.maxQp = value(m_maxQp).toInt() ? value(m_maxQp).toInt() : -1;
|
||
config.adaptiveQp = config.video.maxQp >= 0 && m_adaptiveQp->isChecked();
|
||
}
|
||
config.showCursor = m_cursor->isChecked();
|
||
config.microphone = value(m_microphone).toString(); config.audio = selected(m_microphone) != m_noAudio;
|
||
config.startMuted = m_audioMuted; return config;
|
||
}
|
||
void StreamPublishWindow::apply() {
|
||
if (m_updating || !selected(m_resolution) || !selected(m_fps)) return;
|
||
if (m_engine->active()) m_engine->reconfigure(selection());
|
||
if (!m_engine->active()) synchronize(); else updateMenuButtons();
|
||
saveSettings();
|
||
}
|
||
void StreamPublishWindow::synchronize() {
|
||
QScopedValueRollback<bool> updating(m_updating, true); bool active = m_engine->active();
|
||
if (!active) m_buffer->setPositions({});
|
||
m_start->setText(active ? text("Stop stream") : text("Start stream"));
|
||
m_start->setEnabled((active && m_engine->running()) || (!active && StreamPublishEngine::supported()
|
||
&& selected(m_source) && selected(m_resolution) && selected(m_fps) && selected(m_codec)));
|
||
for (auto* menu : {m_source, m_resolution, m_fps, m_codec, m_bitrate, m_rotation})
|
||
menu->menuAction()->setEnabled(!active || m_engine->running());
|
||
m_microphone->menuAction()->setEnabled(!active);
|
||
if (active) m_audioMuted = m_engine->muted();
|
||
bool audio = active ? m_engine->config().audio : selection().audio;
|
||
m_sound->setEnabled(audio && (!active || m_engine->running()));
|
||
{ QSignalBlocker blocked(m_sound); m_sound->setChecked(audio && !m_audioMuted); }
|
||
if (active) {
|
||
auto config = m_engine->config();
|
||
for (int i = 0; i < m_sources.size(); i++) if (m_sources[i].sameSource(config.source)) {
|
||
select(m_source, i); if (!config.source.size.isEmpty()) m_sources[i].size = config.source.size;
|
||
}
|
||
refreshModes(config);
|
||
select(m_codec, config.video.codec);
|
||
int bitrate = config.automaticBitrate ? 0 : config.video.bitrate;
|
||
if (!option(m_bitrate, bitrate)) addOption(m_bitrate, QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate);
|
||
select(m_bitrate, bitrate);
|
||
if (config.video.codec == 1 || config.video.codec == 2) {
|
||
int qp = config.video.maxQp < 0 ? 0 : config.video.maxQp;
|
||
if (!select(m_maxQp, qp)) DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop active QP ceiling unavailable value=%d", qp);
|
||
m_adaptiveQp->setChecked(config.adaptiveQp);
|
||
}
|
||
select(m_rotation, config.video.rotation);
|
||
if (config.audio) select(m_microphone, config.microphone); else m_noAudio->setChecked(true);
|
||
m_cursor->setChecked(config.showCursor);
|
||
}
|
||
m_cursor->setVisible(StreamPublishEngine::cursorSupported(selection().source));
|
||
bool qpSupported = value(m_codec).toInt() == 1 || value(m_codec).toInt() == 2;
|
||
m_maxQp->menuAction()->setEnabled(qpSupported && (!active || m_engine->running()));
|
||
m_adaptiveQp->setEnabled(m_maxQp->menuAction()->isEnabled() && value(m_maxQp).toInt() > 0);
|
||
bool portal = selection().source.id == "portal";
|
||
m_cursor->setEnabled(!active || (m_engine->running() && !portal));
|
||
m_cursor->setToolTip(portal ? text("Change cursor visibility before starting screen sharing") : QString());
|
||
updateMenuButtons();
|
||
}
|
||
|
||
void StreamPublishWindow::updateMenuButtons() {
|
||
auto update = [this](QMenu* menu, const char* heading, const QString& shape, const QString& label = {}, const QString& detail = {}, int rotation = 0) {
|
||
menu->menuAction()->setIcon(controlIcon(palette(), shape, label, detail, rotation));
|
||
QString description = text(heading);
|
||
if (auto* action = selected(menu)) description += QStringLiteral(": ") + action->text();
|
||
menu->menuAction()->setToolTip(description); menu->setAccessibleName(description);
|
||
};
|
||
auto config = selection();
|
||
update(m_source, "Video source", config.source.kind == StreamVideoSource::Camera ? "camera" : "screen", config.source.name);
|
||
update(m_codec, "Video codec", "chip", selected(m_codec) ? LiveVideoEncoder::codecName(config.video.codec).remove('.') : "—");
|
||
QString bitrate = config.video.bitrate >= 1000000 ? QString::number(config.video.bitrate / 1000000.0, 'g', 2) + 'M'
|
||
: QString::number(config.video.bitrate / 1000) + 'k';
|
||
update(m_bitrate, "Video bitrate", "gauge", bitrate, config.automaticBitrate ? "A" : QString());
|
||
QSize size = value(m_resolution).toSize();
|
||
update(m_resolution, "Video resolution", "screen", size.isEmpty() ? "Auto" : QString::number(std::min(size.width(), size.height())) + 'p');
|
||
update(m_fps, "Frame rate", "fps", selected(m_fps) ? QString::number(config.video.fps) : "—", "FPS");
|
||
update(m_rotation, "Rotation", "rotation", QStringLiteral("%1°").arg(config.video.rotation), {}, config.video.rotation);
|
||
update(m_maxQp, "Maximum QP", "qp", value(m_maxQp).toInt() ? value(m_maxQp).toString() : "Auto", "QP");
|
||
update(m_microphone, "Audio source", config.audio ? "microphone" : "silent", config.audio ? selected(m_microphone)->text() : QString());
|
||
m_sound->setIcon(controlIcon(palette(), m_sound->isChecked() ? "sound" : "silent"));
|
||
m_sound->setToolTip(text(m_sound->isChecked() ? "Turn stream audio off" : "Turn stream audio on"));
|
||
m_sound->setAccessibleName(m_sound->toolTip());
|
||
m_menuBar->setFixedWidth(m_menuBar->sizeHint().width());
|
||
setMinimumWidth(layout()->minimumSize().width());
|
||
}
|