You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

144 lines
10 KiB

#include "stream_publish_window.h"
#include "sound_manager.h"
#include "debug_config.h"
#include <QVBoxLayout>
#include <QCoreApplication>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QComboBox>
#include <QCheckBox>
#include <QLabel>
#include <QPushButton>
#include <QButtonGroup>
#include <QPainter>
#include <QScrollArea>
#ifdef HAVE_STREAM_CAPTURE
#include <QVideoWidget>
#include <QVideoSink>
#include <QVideoFrame>
#endif
namespace {
QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); }
QString bytes(uint64_t value) { return QString::number(value / 1024.0, 'f', 1) + " KiB"; }
}
StreamPublishWindow::StreamPublishWindow(StreamPublishEngine* engine, uint64_t group, QWidget* parent)
: QWidget(parent, Qt::Window), m_engine(engine), m_group(group) {
setWindowTitle(text("Broadcast stream")); resize(720, 720); setMinimumSize(460, 480);
auto* layout = new QVBoxLayout(this);
#ifdef HAVE_STREAM_CAPTURE
auto* preview = new QVideoWidget(this); m_preview = preview;
preview->setMinimumHeight(160); preview->setAspectRatioMode(Qt::KeepAspectRatio);
connect(engine, &StreamPublishEngine::previewVideoChanged, this, [this, preview](const QVariant& frame) {
if (isVisible()) preview->videoSink()->setVideoFrame(frame.value<QVideoFrame>());
});
#else
m_preview = new QLabel(text("Video capture requires Qt Multimedia and FFmpeg"), this);
#endif
layout->addWidget(m_preview, 1);
auto* scroll = new QScrollArea(this); scroll->setWidgetResizable(true);
auto* settings = new QWidget(scroll); auto* form = new QFormLayout(settings);
scroll->setWidget(settings); scroll->setMinimumHeight(220); layout->addWidget(scroll);
m_source = new QComboBox(settings); m_source->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
m_source->setMinimumContentsLength(16); m_source->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
auto* sourceRow = new QWidget(settings); auto* sourceLayout = new QHBoxLayout(sourceRow); sourceLayout->setContentsMargins(0, 0, 0, 0);
sourceLayout->addWidget(m_source, 1); auto* refresh = new QPushButton(text("Refresh"), settings); sourceLayout->addWidget(refresh);
connect(refresh, &QPushButton::clicked, this, &StreamPublishWindow::refreshSources); form->addRow(text("Video source"), sourceRow);
m_mode = new QComboBox(settings);
for (auto mode : {QSize(640, 360), QSize(1280, 720), QSize(1920, 1080)})
for (int fps : {15, 30, 60}) m_mode->addItem(QStringLiteral("%1 × %2 · %3 fps").arg(mode.width()).arg(mode.height()).arg(fps),
QVariantList{mode.width(), mode.height(), fps});
m_mode->setCurrentIndex(4); form->addRow(text("Video mode"), m_mode);
m_codec = new QComboBox(settings); form->addRow(text("Video codec"), m_codec);
m_bitrate = new QComboBox(settings); m_bitrate->addItem(text("Automatic"), 0);
for (int bitrate : {250000, 500000, 1000000, 2000000, 4000000, 8000000, 16000000})
m_bitrate->addItem(QStringLiteral("%1 Mbit/s").arg(bitrate / 1000000.0, 0, 'f', 2), bitrate);
form->addRow(text("Video bitrate"), m_bitrate);
m_rotation = new QButtonGroup(this); auto* rotationRow = new QWidget(settings); auto* angles = new QHBoxLayout(rotationRow);
angles->setContentsMargins(0, 0, 0, 0);
for (int angle : {0, 90, 270}) {
auto* button = new QPushButton(QStringLiteral("%1°").arg(angle), settings); button->setCheckable(true); button->setFixedSize(66, 34);
QPixmap icon(20, 20); icon.fill(Qt::transparent); QPainter painter(&icon);
painter.translate(10, 10); painter.rotate(angle); painter.setPen(QPen(QColor("#e53935"), 3)); painter.drawLine(-6, -6, 6, -6);
painter.setPen(QPen(palette().color(QPalette::Text), 1)); painter.drawLine(-6, -4, -6, 6); painter.drawLine(6, -4, 6, 6); painter.drawLine(-6, 6, 6, 6);
painter.end(); button->setIcon(QIcon(icon)); button->setToolTip(text("The red edge marks the top for viewers"));
button->setStyleSheet("QPushButton:checked { background: #1976d2; color: white; }");
m_rotation->addButton(button, angle); angles->addWidget(button);
}
m_rotation->button(0)->setChecked(true); angles->addStretch(); form->addRow(text("Rotation"), rotationRow);
m_audio = new QCheckBox(text("Microphone enabled"), settings); m_audio->setChecked(true); form->addRow(m_audio);
m_microphone = new QComboBox(settings); m_microphone->addItem(text("Default microphone"), QString());
for (const auto& device : SoundManager::enumerateCaptureDevices()) m_microphone->addItem(device.name, device.id);
form->addRow(text("Microphone"), m_microphone);
m_inputChannels = new QLabel(settings); form->addRow(text("Audio input"), m_inputChannels);
m_mute = new QCheckBox(text("Mute microphone"), settings); form->addRow(m_mute);
connect(m_mute, &QCheckBox::toggled, engine, &StreamPublishEngine::setMuted);
m_status = new QLabel(this); m_status->setWordWrap(true); layout->addWidget(m_status);
m_stats = new QLabel(this); m_stats->setWordWrap(true); m_stats->setMinimumHeight(46); layout->addWidget(m_stats);
m_start = new QPushButton(text("Start stream"), this); layout->addWidget(m_start);
connect(m_start, &QPushButton::clicked, this, [this] {
if (m_engine->active()) m_engine->stop(); else m_engine->start(m_group, selection()); synchronize();
});
for (auto* box : {m_source, m_mode, m_codec, m_bitrate}) connect(box, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &StreamPublishWindow::apply);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
connect(m_rotation, &QButtonGroup::idClicked, this, &StreamPublishWindow::apply);
#else
connect(m_rotation, QOverload<int>::of(&QButtonGroup::buttonClicked), this, &StreamPublishWindow::apply);
#endif
connect(engine, &StreamPublishEngine::codecsChanged, this, &StreamPublishWindow::refreshCodecs);
connect(engine, &StreamPublishEngine::configurationChanged, this, &StreamPublishWindow::synchronize);
connect(engine, &StreamPublishEngine::stateChanged, this, [this](const QString& value) { m_status->setText(value); synchronize(); });
connect(engine, &StreamPublishEngine::errorOccurred, this, [this](const QString& value) { m_status->setText(value); });
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_stats->setText(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(); synchronize();
}
void StreamPublishWindow::refreshSources() {
m_updating = true; auto config = m_engine->active() ? m_engine->config() : selection();
m_sources = StreamPublishEngine::sources(); m_source->clear();
int selected = 0;
for (int i = 0; i < m_sources.size(); i++) {
const auto& source = m_sources[i];
QString kind = source.kind == StreamVideoSource::Camera ? text("Camera") : source.kind == StreamVideoSource::Screen ? text("Screen") : text("Window");
m_source->addItem(kind + ": " + source.name);
if (source.kind == config.source.kind && source.id == config.source.id && source.window == config.source.window) selected = i;
}
m_source->setCurrentIndex(m_sources.isEmpty() ? -1 : selected); m_updating = false;
}
void StreamPublishWindow::refreshCodecs() {
m_updating = true; int previous = m_codec->currentData().toInt(); m_codec->clear();
for (int codec : m_engine->codecs()) m_codec->addItem(StreamVideoEncoder::codecName(codec), codec);
int index = m_codec->findData(previous); if (index >= 0) m_codec->setCurrentIndex(index);
m_updating = false; synchronize();
}
StreamPublishConfig StreamPublishWindow::selection() const {
StreamPublishConfig config;
if (m_source->currentIndex() >= 0 && m_source->currentIndex() < m_sources.size()) config.source = m_sources[m_source->currentIndex()];
auto mode = m_mode->currentData().toList();
if (mode.size() == 3) { config.video.width = mode[0].toInt(); config.video.height = mode[1].toInt(); config.video.fps = mode[2].toInt(); }
config.video.codec = m_codec->currentData().toInt(); config.video.rotation = m_rotation->checkedId();
config.video.bitrate = m_bitrate->currentData().toInt();
if (!config.video.bitrate) config.video.bitrate = std::clamp(config.video.width * config.video.height * config.video.fps / 11, 250000, 16000000);
config.microphone = m_microphone->currentData().toString(); config.audio = m_audio->isChecked(); return config;
}
void StreamPublishWindow::apply() { if (!m_updating && m_engine->active()) m_engine->reconfigure(selection()); }
void StreamPublishWindow::synchronize() {
m_updating = true; bool active = m_engine->active();
m_start->setText(active ? text("Stop stream") : text("Start stream"));
m_start->setEnabled(active || (StreamPublishEngine::supported() && m_source->count() && m_codec->count()));
m_audio->setEnabled(!active); m_microphone->setEnabled(!active); m_mute->setEnabled(active && m_engine->config().audio);
if (active) {
auto config = m_engine->config();
for (int i = 0; i < m_sources.size(); i++) if (m_sources[i].kind == config.source.kind
&& m_sources[i].id == config.source.id && m_sources[i].window == config.source.window) m_source->setCurrentIndex(i);
for (int i = 0; i < m_mode->count(); i++) if (m_mode->itemData(i).toList() == QVariantList{config.video.width, config.video.height, config.video.fps}) m_mode->setCurrentIndex(i);
m_codec->setCurrentIndex(m_codec->findData(config.video.codec));
if (auto* button = m_rotation->button(config.video.rotation)) button->setChecked(true);
m_audio->setChecked(config.audio); m_microphone->setCurrentIndex(m_microphone->findData(config.microphone));
}
m_updating = false;
}