#include #include "callwindow.h" #include "../transport/gui_bridge.h" #include "call_audio_engine.h" #include "call_video_engine.h" #include "../../../src/call/call_video_port.h" #include "sound_manager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include "call/call.h" #include "../../lib/mem.h" #include "../../lib/debug_config.h" } /* ── рисование иконок звонка (QPainter, без внешних ассетов) ── */ enum class Glyph { Phone, PhoneDown, Mic, MicOff }; static QString callText(const char* text) { return QCoreApplication::translate("ChatGui", text); } // Отдельное окно показа. Закрытие скрывает только изображение, не завершает звонок. class CallVideoWindow : public QWidget { public: explicit CallVideoWindow(QWidget* parent) : QWidget(parent, Qt::Window) { setObjectName("callVideoWindow"); setMinimumSize(320, 240); setAttribute(Qt::WA_QuitOnClose, false); QSize available = parent->screen() ? parent->screen()->availableGeometry().size() : QSize(1280, 900); resize(qMin(1100, available.width() * 9 / 10), qMin(800, available.height() * 9 / 10)); } QImage remote, local; protected: void closeEvent(QCloseEvent* event) override { DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video window closed; call continues"); event->accept(); } void keyPressEvent(QKeyEvent* event) override { if (event->key() == Qt::Key_F11 || (event->key() == Qt::Key_Escape && isFullScreen())) { toggleFullScreen(); event->accept(); return; } QWidget::keyPressEvent(event); } void mouseDoubleClickEvent(QMouseEvent* event) override { if (event->button() == Qt::LeftButton) { toggleFullScreen(); event->accept(); return; } QWidget::mouseDoubleClickEvent(event); } void paintEvent(QPaintEvent*) override { QPainter painter(this); painter.fillRect(rect(), Qt::black); painter.setRenderHint(QPainter::SmoothPixmapTransform); if (remote.isNull()) return; QSize fitted = remote.size().scaled(size(), Qt::KeepAspectRatio); QRect videoRect(QPoint((width() - fitted.width()) / 2, (height() - fitted.height()) / 2), fitted); painter.drawImage(videoRect, remote); if (!local.isNull()) { QSize preview = local.size().scaled(qMin(200, fitted.width() / 4), qMin(150, fitted.height() / 4), Qt::KeepAspectRatio); QRect target(QPoint(videoRect.right() - preview.width() - 12, videoRect.top() + 12), preview); painter.setPen(QPen(Qt::white, 2)); painter.drawRect(target.adjusted(-1, -1, 1, 1)); painter.drawImage(target, local); } } private: void toggleFullScreen() { if (isFullScreen()) { showNormal(); if (!m_geometry.isEmpty()) restoreGeometry(m_geometry); } else { m_geometry = saveGeometry(); showFullScreen(); } } QByteArray m_geometry; }; static QColor colorForName(const QString& name) { static const QColor palette[] = { QColor("#5B9BD5"), QColor("#70AD47"), QColor("#ED7D31"), QColor("#9B59B6"), QColor("#F4B400"), QColor("#E67E22"), QColor("#16A085"), QColor("#D35400"), }; quint32 h = 0; for (const QChar c : name) h = h * 31u + c.unicode(); return palette[h % 8]; } static QPixmap makeAvatar(const QString& name, int size) { QPixmap pm(size, size); pm.fill(Qt::transparent); QPainter p(&pm); p.setRenderHint(QPainter::Antialiasing); p.setBrush(colorForName(name)); p.setPen(Qt::NoPen); p.drawEllipse(0, 0, size, size); QString initial = name.trimmed().isEmpty() ? QStringLiteral("?") : name.trimmed().left(1).toUpper(); p.setPen(Qt::white); QFont f = p.font(); f.setBold(true); f.setPixelSize(size / 2); p.setFont(f); p.drawText(QRect(0, 0, size, size), Qt::AlignCenter, initial); p.end(); return pm; } static void drawPhoneGlyph(QPainter* p, const QRectF& r, bool down) { const qreal s = r.width(); p->save(); p->translate(r.center()); p->rotate(down ? 135.0 : -45.0); const qreal w = s * 0.36, h = s * 0.64; QPainterPath path; path.addRoundedRect(QRectF(-w / 2, -h / 2, w, h), w * 0.5, w * 0.5); p->setPen(Qt::NoPen); p->setBrush(Qt::white); p->drawPath(path); p->restore(); } static void drawMicGlyph(QPainter* p, const QRectF& r, bool off) { const qreal s = r.width(); const QPointF c = r.center(); p->save(); QPen pen(Qt::white, s * 0.09, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin); p->setPen(pen); p->setBrush(Qt::NoBrush); const qreal cw = s * 0.30; p->drawRoundedRect(QRectF(c.x() - cw / 2, r.top() + s * 0.10, cw, s * 0.40), cw / 2, cw / 2); p->drawArc(QRectF(c.x() - s * 0.20, r.top() + s * 0.42, s * 0.40, s * 0.26), 180 * 16, 180 * 16); p->drawLine(QPointF(c.x(), r.top() + s * 0.60), QPointF(c.x(), r.top() + s * 0.68)); p->drawLine(QPointF(c.x() - s * 0.17, r.top() + s * 0.68), QPointF(c.x() + s * 0.17, r.top() + s * 0.68)); if (off) { p->setPen(QPen(Qt::white, s * 0.09, Qt::SolidLine, Qt::RoundCap)); p->drawLine(QPointF(c.x() - s * 0.26, c.y() - s * 0.26), QPointF(c.x() + s * 0.26, c.y() + s * 0.26)); } p->restore(); } static QIcon makeRoundIcon(const QColor& bg, Glyph glyph, int size) { QPixmap pm(size, size); pm.fill(Qt::transparent); QPainter p(&pm); p.setRenderHint(QPainter::Antialiasing); p.setPen(Qt::NoPen); p.setBrush(bg); p.drawEllipse(0, 0, size, size); const QRectF glyphRect(size * 0.24, size * 0.24, size * 0.52, size * 0.52); switch (glyph) { case Glyph::Phone: drawPhoneGlyph(&p, glyphRect, false); break; case Glyph::PhoneDown: drawPhoneGlyph(&p, glyphRect, true); break; case Glyph::Mic: drawMicGlyph(&p, glyphRect, false); break; case Glyph::MicOff: drawMicGlyph(&p, glyphRect, true); break; } p.end(); return QIcon(pm); } static QPushButton* makeRoundBtn(const QIcon& icon, const QString& tooltip, QWidget* parent) { auto* btn = new QPushButton(parent); btn->setIcon(icon); btn->setIconSize(QSize(52, 52)); btn->setFixedSize(64, 64); btn->setCursor(Qt::PointingHandCursor); btn->setToolTip(tooltip); btn->setStyleSheet( "QPushButton { border: none; border-radius: 32px; background: transparent; }" "QPushButton:pressed { background: rgba(255,255,255,0.15); }"); return btn; } static QWidget* wrapWithCaption(QWidget* btn, const QString& caption, QLabel** captionOut = nullptr) { auto* w = new QWidget(btn->parentWidget()); auto* l = new QVBoxLayout(w); l->setContentsMargins(0, 0, 0, 0); l->setSpacing(6); l->addWidget(btn, 0, Qt::AlignHCenter); auto* cap = new QLabel(caption, w); cap->setAlignment(Qt::AlignCenter); cap->setStyleSheet("color: palette(text); font-size: 11px; background: transparent;"); l->addWidget(cap, 0, Qt::AlignHCenter); if (captionOut) *captionOut = cap; return w; } CallWindow::CallWindow(QWidget* parent) : QWidget(parent, Qt::Window) { setWindowTitle(QCoreApplication::translate("ChatGui", "Call")); resize(520, 520); setMinimumSize(420, 360); setAttribute(Qt::WA_DeleteOnClose, false); m_avatarLabel = new QLabel(this); m_avatarLabel->setFixedSize(80, 80); m_avatarLabel->setAlignment(Qt::AlignCenter); m_peerLabel = new QLabel(this); m_peerLabel->setAlignment(Qt::AlignCenter); m_peerLabel->setStyleSheet("font-size: 17px; font-weight: bold; color: palette(text);"); m_statusLabel = new QLabel(this); m_statusLabel->setAlignment(Qt::AlignCenter); m_statusLabel->setStyleSheet("color: gray; font-size: 13px;"); m_durationLabel = new QLabel(QStringLiteral("00:00"), this); m_durationLabel->setAlignment(Qt::AlignCenter); m_durationLabel->setStyleSheet("font-size: 15px; color: palette(text); font-family: monospace;"); m_bgpPathLabel = new QLabel(this); m_bgpPathLabel->setAlignment(Qt::AlignCenter); m_bgpPathLabel->setWordWrap(true); m_bgpPathLabel->setStyleSheet("color: gray; font-size: 11px;"); m_bgpPathLabel->hide(); m_connectionLabel = new QLabel(this); m_connectionLabel->setAlignment(Qt::AlignCenter); m_connectionLabel->setStyleSheet("color: gray; font-size: 13px;"); setConnectionType(0, 0); m_statsLabel = new QLabel(this); m_statsLabel->setAlignment(Qt::AlignCenter); m_statsLabel->setWordWrap(true); m_statsLabel->setStyleSheet("color: gray; font-size: 11px;"); m_statsLabel->hide(); m_acceptBtn = makeRoundBtn(makeRoundIcon(QColor("#27ae60"), Glyph::Phone, 96), QCoreApplication::translate("ChatGui", "Accept"), this); m_declineBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QCoreApplication::translate("ChatGui", "Decline"), this); m_hangupBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QCoreApplication::translate("ChatGui", "End call"), this); m_muteBtn = makeRoundBtn(makeRoundIcon(QColor("#7f8c8d"), Glyph::Mic, 96), QCoreApplication::translate("ChatGui", "Mute"), this); m_muteBtn->setCheckable(true); m_deviceCombo = new QComboBox(this); m_deviceCombo->setStyleSheet( "QComboBox { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); auto* root = new QVBoxLayout(this); root->setContentsMargins(12, 12, 12, 12); m_videoWindow = new CallVideoWindow(this); m_panel = new QWidget(this); m_panel->setObjectName("callPanel"); m_panel->setMaximumWidth(640); m_panel->setStyleSheet("QWidget#callPanel { background: palette(window); border-radius: 12px; }"); root->addWidget(m_panel, 0, Qt::AlignCenter); QVBoxLayout* lay = new QVBoxLayout(m_panel); lay->setContentsMargins(16, 20, 16, 16); lay->setSpacing(8); lay->addWidget(m_avatarLabel, 0, Qt::AlignHCenter); lay->addWidget(m_peerLabel); lay->addWidget(m_statusLabel); lay->addWidget(m_durationLabel); lay->addWidget(m_connectionLabel); lay->addWidget(m_bgpPathLabel); lay->addWidget(m_statsLabel); m_video = new CallVideoEngine(this); m_localPreview = new QLabel(this); m_localPreview->setAlignment(Qt::AlignCenter); m_localPreview->hide(); lay->addWidget(m_localPreview, 0, Qt::AlignHCenter); m_videoStatus = new QLabel(this); m_videoStatus->setWordWrap(true); lay->addWidget(m_videoStatus); m_videoControls = new QWidget(this); auto* videoLayout = new QVBoxLayout(m_videoControls); videoLayout->setContentsMargins(0, 0, 0, 0); auto* actions = new QHBoxLayout(); m_videoEnabled = new QCheckBox(callText("Send video"), this); actions->addWidget(m_videoEnabled); actions->addStretch(1); m_settingsBtn = new QPushButton(callText("Call settings"), this); m_settingsBtn->setCheckable(true); actions->addWidget(m_settingsBtn); m_showVideoBtn = new QPushButton(callText("Show video"), this); actions->addWidget(m_showVideoBtn); lay->addLayout(actions); connect(m_settingsBtn, &QPushButton::toggled, this, [this] { updateVideoPresentation(); }); connect(m_showVideoBtn, &QPushButton::clicked, this, [this] { if (m_videoWindow->isMinimized()) m_videoWindow->showNormal(); else m_videoWindow->show(); m_videoWindow->raise(); m_videoWindow->activateWindow(); DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video window reopened"); }); auto* modes = new QHBoxLayout(); m_camera = new QComboBox(this); connect(m_video, &CallVideoEngine::camerasChanged, this, [this](const CallCameraDevices& cameras) { const auto selected = m_camera->currentData(); m_camera->clear(); for (const auto& camera : cameras) m_camera->addItem(camera.first, camera.second); const int index = m_camera->findData(selected); if (index >= 0) m_camera->setCurrentIndex(index); if (m_videoEnabled->isChecked()) applyVideo(); }); m_resolution = new QComboBox(this); m_resolution->addItem("640×480", QSize(640, 480)); m_resolution->addItem("1280×720", QSize(1280, 720)); m_codec = new QComboBox(this); m_codec->addItem(callText("Auto (H.265 → H.264)"), 0); m_codec->addItem("H.265", 2); m_codec->addItem("H.264", 1); modes->addWidget(m_camera, 1); modes->addWidget(m_resolution); modes->addWidget(m_codec); videoLayout->addLayout(modes); auto* rates = new QHBoxLayout(); rates->addWidget(new QLabel(callText("Bitrate range, kbit/s:"), this)); m_videoMin = new QSpinBox(this); m_videoMax = new QSpinBox(this); for (auto* spin : {m_videoMin, m_videoMax}) { spin->setRange(300, 4000); spin->setSingleStep(100); rates->addWidget(spin); } m_videoMin->setValue(300); m_videoMax->setValue(4000); videoLayout->addLayout(rates); lay->addWidget(m_videoControls); connect(m_videoEnabled, &QCheckBox::toggled, this, [this] { applyVideo(); }); for (auto* combo : {m_camera, m_resolution, m_codec}) connect(combo, QOverload::of(&QComboBox::activated), this, [this] { applyVideo(); }); auto updateRange = [this] { if (m_videoMin->value() > m_videoMax->value()) m_videoMax->setValue(m_videoMin->value()); if (m_videoCall) call_video_port_range(m_videoCall, m_videoMin->value() * 1000, m_videoMax->value() * 1000); }; connect(m_videoMin, &QSpinBox::editingFinished, this, updateRange); connect(m_videoMax, &QSpinBox::editingFinished, this, updateRange); connect(m_video, &CallVideoEngine::statusChanged, this, [this](const QString& text) { m_videoStatus->setText(text); m_videoStatus->setVisible(m_state == Active && !text.isEmpty()); }); connect(m_video, &CallVideoEngine::imageChanged, this, [this](const QImage& image) { bool changed = m_remoteImage.isNull() != image.isNull(); m_remoteImage = image; m_videoWindow->remote = image; m_videoWindow->update(); if (changed) updateVideoPresentation(); update(); }); connect(m_video, &CallVideoEngine::localImageChanged, this, [this](const QImage& image) { bool changed = m_localImage.isNull() != image.isNull(); m_localImage = image; m_videoWindow->local = image; m_videoWindow->update(); if (!image.isNull()) { QSize size = image.size().scaled(200, 150, Qt::KeepAspectRatio); m_localPreview->setFixedSize(size); m_localPreview->setPixmap(QPixmap::fromImage(image).scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } else m_localPreview->clear(); if (changed) updateVideoPresentation(); update(); }); auto* videoTimer = new QTimer(this); videoTimer->setInterval(100); connect(videoTimer, &QTimer::timeout, this, [this] { if (m_state != Active || !CallAudioEngine::instance()->isActive() || CallAudioEngine::instance()->callId() != m_callId) return; if (m_videoCall != m_callId) { m_videoCall = m_callId; m_videoRangeReady = false; m_video->start(m_callId); applyVideo(); } struct call_video_port_status status{}; if (!m_videoRangeReady && call_video_port_status(m_callId, &status) == 0 && status.minimum_bps) { m_videoMin->setValue(status.minimum_bps / 1000); m_videoMax->setValue(status.maximum_bps / 1000); m_videoRangeReady = true; } }); videoTimer->start(); m_acceptCell = wrapWithCaption(m_acceptBtn, QCoreApplication::translate("ChatGui", "Accept")); m_declineCell = wrapWithCaption(m_declineBtn, QCoreApplication::translate("ChatGui", "Decline")); m_hangupCell = wrapWithCaption(m_hangupBtn, QCoreApplication::translate("ChatGui", "End call")); m_muteCell = wrapWithCaption(m_muteBtn, QCoreApplication::translate("ChatGui", "Mute"), &m_muteCaption); QHBoxLayout* btn = new QHBoxLayout(); btn->setSpacing(10); btn->addStretch(1); btn->addWidget(m_acceptCell); btn->addWidget(m_declineCell); btn->addWidget(m_hangupCell); btn->addWidget(m_muteCell); btn->addStretch(1); lay->addLayout(btn); videoLayout->addWidget(m_deviceCombo); connect(m_acceptBtn, &QPushButton::clicked, this, &CallWindow::onAccept); connect(m_declineBtn, &QPushButton::clicked, this, &CallWindow::onDecline); connect(m_hangupBtn, &QPushButton::clicked, this, &CallWindow::onHangup); connect(m_muteBtn, &QPushButton::toggled, this, &CallWindow::onMuteToggled); connect(m_deviceCombo, QOverload::of(&QComboBox::activated), this, &CallWindow::onDeviceChanged); m_durationTimer = new QTimer(this); m_durationTimer->setInterval(1000); connect(m_durationTimer, &QTimer::timeout, this, &CallWindow::updateDuration); setState(Outgoing); } CallWindow::~CallWindow() { // Остановить callbacks до удаления дочерних виджетов окна. m_video->disconnect(this); m_video->stop(); } void CallWindow::setPeerName(const QString& name) { m_peerLabel->setText(name); m_avatarLabel->setPixmap(makeAvatar(name, 80)); m_videoWindow->setWindowTitle(callText("Video — %1").arg(name)); } void CallWindow::setBgpPath(const QString& path) { m_bgpPathLabel->setText(path); m_bgpPathLabel->setVisible(m_state == Active && !path.isEmpty()); } void CallWindow::setConnectionType(uint8_t type, quint64 nextHop) { const QString names[] = { QCoreApplication::translate("ChatGui", "not connected"), QCoreApplication::translate("ChatGui", "direct"), QCoreApplication::translate("ChatGui", "reverse"), QCoreApplication::translate("ChatGui", "indirect") }; m_connectionLabel->setText(QCoreApplication::translate("ChatGui", "Connection: %1").arg(names[type <= 3 ? type : 0])); m_connectionLabel->setToolTip(type == 3 ? QCoreApplication::translate("ChatGui", "Relay: 0x%1").arg(nextHop, 16, 16, QChar('0')) : QString()); } void CallWindow::setState(State s) { m_state = s; switch (s) { case Outgoing: m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Connecting…")); m_durationLabel->hide(); break; case Ringing: m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Calling…")); m_durationLabel->hide(); break; case Incoming: m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Incoming call…")); m_durationLabel->hide(); break; case Active: m_statusLabel->setText(QCoreApplication::translate("ChatGui", "In call")); m_durationLabel->setText(QStringLiteral("00:00")); m_durationLabel->show(); m_startMs = QDateTime::currentMSecsSinceEpoch(); m_durationTimer->start(); populateDeviceCombo(); m_muteBtn->setChecked(false); break; case Ended: m_video->stop(); m_videoCall = 0; setConnectionType(0, 0); m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Ended")); m_durationTimer->stop(); break; } updateButtons(); } void CallWindow::populateDeviceCombo() { m_deviceCombo->blockSignals(true); m_deviceCombo->clear(); m_deviceCombo->addItem(QCoreApplication::translate("ChatGui", "Default (auto)"), QString()); const auto devs = SoundManager::enumeratePlaybackDevices(); for (int i = 0; i < devs.size(); i++) { QString label = devs[i].name; if (devs[i].isDefault) label += QCoreApplication::translate("ChatGui", " (default)"); m_deviceCombo->addItem(label, devs[i].id); } QString cur = CallAudioEngine::instance()->playbackDevice(); for (int i = 0; i < m_deviceCombo->count(); i++) { if (m_deviceCombo->itemData(i).toString() == cur) { m_deviceCombo->setCurrentIndex(i); break; } } m_deviceCombo->blockSignals(false); } void CallWindow::updateButtons() { m_acceptCell->setVisible(m_state == Incoming); m_declineCell->setVisible(m_state == Incoming); m_hangupCell->setVisible(m_state == Outgoing || m_state == Ringing || m_state == Active); m_muteCell->setVisible(m_state == Active); updateVideoPresentation(); } void CallWindow::updateVideoPresentation() { bool receiving = m_state == Active && !m_remoteImage.isNull(); if (receiving != m_receivingVideo) { m_receivingVideo = receiving; DEBUG_INFO(DEBUG_CATEGORY_CALL, "call video separate window remote=%d local=%d", receiving, !m_localImage.isNull()); if (receiving) m_videoWindow->show(); else m_videoWindow->hide(); } bool active = m_state == Active, settings = active && m_settingsBtn->isChecked(); m_localPreview->setVisible(active && !receiving && !m_localImage.isNull()); m_connectionLabel->setVisible(active); m_bgpPathLabel->setVisible(active && !m_bgpPathLabel->text().isEmpty()); m_statsLabel->setVisible(active && !m_statsLabel->text().isEmpty()); m_videoEnabled->setVisible(active); m_settingsBtn->setVisible(active); m_videoControls->setVisible(settings); m_showVideoBtn->setVisible(receiving); m_videoStatus->setVisible(active && !m_videoStatus->text().isEmpty()); update(); } void CallWindow::applyVideo() { if (!m_videoCall) return; QSize size = m_resolution->currentData().toSize(); m_video->setVideo(m_videoEnabled->isChecked(), m_camera->currentData().toByteArray(), size.width(), size.height(), m_codec->currentData().toInt()); } void CallWindow::postCallFn(void (*fn)(void*), uint64_t callId) { struct call_id_arg* a = (struct call_id_arg*)u_malloc(sizeof(*a)); if (!a) return; a->inst = gui_bridge_get_inst(); a->call_id = callId; gui_bridge_post_uasync_fn(fn, a); } void CallWindow::onAccept() { SoundManager::instance()->stopLoop(); setState(Active); postCallFn(call_accept_trampoline, m_callId); } void CallWindow::onDecline() { postCallFn(call_decline_trampoline, m_callId); } void CallWindow::onHangup() { postCallFn(call_hangup_trampoline, m_callId); } void CallWindow::onMuteToggled(bool checked) { CallAudioEngine::instance()->setMuted(checked); m_muteBtn->setIcon(makeRoundIcon(checked ? QColor("#e74c3c") : QColor("#7f8c8d"), checked ? Glyph::MicOff : Glyph::Mic, 96)); if (m_muteCaption) m_muteCaption->setText(checked ? QCoreApplication::translate("ChatGui", "Unmute") : QCoreApplication::translate("ChatGui", "Mute")); } void CallWindow::onDeviceChanged(int index) { QString id = m_deviceCombo->itemData(index).toString(); CallAudioEngine::instance()->setPlaybackDevice(id); } void CallWindow::closeEvent(QCloseEvent* event) { m_video->stop(); m_videoCall = 0; if (m_state == Outgoing || m_state == Ringing || m_state == Active) postCallFn(call_hangup_trampoline, m_callId); emit closed(m_callId); event->accept(); } void CallWindow::updateDuration() { if (m_state != Active) return; qint64 ms = QDateTime::currentMSecsSinceEpoch() - m_startMs; qint64 s = ms / 1000; m_durationLabel->setText(QStringLiteral("%1:%2") .arg(s / 60, 2, 10, QChar('0')) .arg(s % 60, 2, 10, QChar('0'))); } void CallWindow::setAudioStats(int rtt, int buffer, int tempo, int dropped, int underruns, int minimum, int maximum, int reserve) { QString text = QCoreApplication::translate("ChatGui", "Buffer: %1 ms · Reserve: %2 ms\nOver 10 s: minimum %3 · maximum %4 ms\nSpeed: %5× · RTT: %6 " "ms\nBuffer drops: %7 · Underruns: %8") .arg(buffer).arg(reserve).arg(minimum).arg(maximum) .arg(tempo / 100.0, 0, 'f', 2).arg(rtt).arg(dropped).arg(underruns); auto* audio = CallAudioEngine::instance(); if (audio->callId() == m_callId) { auto output = audio->outputLatency(); auto value = [](int ms) { return ms >= 0 ? QString::number(ms) : QStringLiteral("—"); }; QString total = value(output.outputMs); if (output.outputMs >= 0 && !output.measured) total.prepend(QStringLiteral("~")); text += QLatin1Char('\n') + QCoreApplication::translate("ChatGui", "Audio output: %1 ms (includes app queue: %2 ms)") .arg(total).arg(value(output.queueMs)); } m_statsLabel->setText(text); m_statsLabel->setVisible(m_state == Active); }