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
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; |
|
}
|
|
|