Files
utun2/tools/chatgui/src/callwindow.cpp
T

533 lines
24 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <QCoreApplication>
#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 <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QComboBox>
#include <QCheckBox>
#include <QSpinBox>
#include <QTimer>
#include <QDateTime>
#include <QCloseEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QScreen>
#include <QPainter>
#include <QPainterPath>
#include <cstring>
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<int>::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<int>::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);
}