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

652 lines
32 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 "../../../src/call/call_video_playout.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;
CallVideoEngine* engine = nullptr;
quint64 serial = 0;
quint64 callId = 0;
int64_t requestedAt = 0; // GUI-получение кадра -> начало paintEvent, отдельно от самой работы painter.
protected:
void moveEvent(QMoveEvent* event) override { ++m_moves; QWidget::moveEvent(event); }
void resizeEvent(QResizeEvent* event) override { ++m_resizes; QWidget::resizeEvent(event); }
void hideEvent(QHideEvent* event) override { presented(); QWidget::hideEvent(event); }
void changeEvent(QEvent* event) override {
QWidget::changeEvent(event);
if (isMinimized()) presented();
}
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 {
const int64_t started = call_video_playout_now_us(), requestWait = serial && requestedAt ? started - requestedAt : 0;
const bool selected = serial != 0;
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);
const int64_t prepared = call_video_playout_now_us();
painter.drawImage(videoRect, remote);
const int64_t drawn = call_video_playout_now_us();
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);
}
const int64_t overlay = call_video_playout_now_us();
painter.end();
const int64_t finished = call_video_playout_now_us();
presented();
++m_paints; if (selected) ++m_selectedPaints;
m_requestMax = std::max(m_requestMax, requestWait); m_paintMax = std::max(m_paintMax, finished - started);
m_prepareMax = std::max(m_prepareMax, prepared - started); m_drawMax = std::max(m_drawMax, drawn - prepared);
m_overlayMax = std::max(m_overlayMax, overlay - drawn); m_finishMax = std::max(m_finishMax, finished - overlay);
if (!m_slowReported && (requestWait > 50000 || finished - started > 20000)) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "video GUI paint delayed call=%llu request_wait_us=%lld paint_us=%lld "
"prepare_us=%lld draw_us=%lld overlay_us=%lld finish_us=%lld window=%dx%d image=%dx%d dpr_x1000=%d mono_us=%lld",
(unsigned long long)callId, (long long)requestWait, (long long)(finished - started),
(long long)(prepared - started), (long long)(drawn - prepared), (long long)(overlay - drawn),
(long long)(finished - overlay), width(), height(), remote.width(), remote.height(),
int(devicePixelRatioF() * 1000), (long long)finished);
m_slowReported = true;
}
if (!m_reportAt) m_reportAt = started;
if (finished - m_reportAt >= 1000000) {
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video GUI paint call=%llu interval_us=%lld paints=%u selected=%u moves=%u resizes=%u "
"request_wait_max_us=%lld paint_max_us=%lld prepare_max_us=%lld draw_max_us=%lld overlay_max_us=%lld finish_max_us=%lld",
(unsigned long long)callId, (long long)(finished - m_reportAt), m_paints, m_selectedPaints, m_moves, m_resizes,
(long long)m_requestMax, (long long)m_paintMax, (long long)m_prepareMax, (long long)m_drawMax,
(long long)m_overlayMax, (long long)m_finishMax);
m_reportAt = finished; m_paints = m_selectedPaints = m_moves = m_resizes = 0;
m_requestMax = m_paintMax = m_prepareMax = m_drawMax = m_overlayMax = m_finishMax = 0; m_slowReported = false;
}
}
private:
void presented() {
if (engine && serial) { engine->imagePresented(serial); serial = 0; }
}
void toggleFullScreen() {
if (isFullScreen()) { showNormal(); if (!m_geometry.isEmpty()) restoreGeometry(m_geometry); }
else { m_geometry = saveGeometry(); showFullScreen(); }
}
QByteArray m_geometry;
int64_t m_reportAt = 0, m_requestMax = 0, m_paintMax = 0, m_prepareMax = 0, m_drawMax = 0, m_overlayMax = 0, m_finishMax = 0;
unsigned m_paints = 0, m_selectedPaints = 0, m_moves = 0, m_resizes = 0;
bool m_slowReported = false;
};
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);
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_statusLabel->setWordWrap(true);
connect(CallAudioEngine::instance(), &CallAudioEngine::captureErrorChanged, this, [this] {
auto* audio = CallAudioEngine::instance();
if (m_state == Active && audio->callId() == m_callId)
m_statusLabel->setText(audio->captureError().isEmpty() ? callText("In call") : audio->captureError());
});
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);
root->setSizeConstraint(QLayout::SetMinimumSize);
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->addStretch(1);
root->addWidget(m_panel, 0, Qt::AlignHCenter);
root->addStretch(1);
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);
m_continueBtn = new QPushButton(callText("Call anyway"), this);
lay->addWidget(m_continueBtn);
connect(m_continueBtn, &QPushButton::clicked, this, [this] {
if (m_state != WaitDecision) return;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "desktop caller overrides phone busy id=%016llx", (unsigned long long)m_callId);
postCallFn(call_continue_trampoline, m_callId);
setState(Outgoing);
SoundManager::instance()->playLoop("call_connecting");
});
lay->addWidget(m_durationLabel);
lay->addWidget(m_connectionLabel);
lay->addWidget(m_bgpPathLabel);
lay->addWidget(m_statsLabel);
m_video = new CallVideoEngine(this);
m_video->setPresentationDriven(true); m_videoWindow->engine = m_video;
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);
m_resolution = new QComboBox(this);
auto refreshModes = [this] {
QSize wanted = m_resolution->currentData().toSize();
const int previousFps = m_resolution->currentData(Qt::UserRole + 1).toInt();
const int wantedFps = previousFps ? previousFps : 30;
if (wanted.isEmpty()) wanted = QSize(640, 480);
m_resolution->clear();
for (const auto& value : m_camera->currentData(Qt::UserRole + 1).toList()) {
const auto mode = value.toList(); const QSize size = mode[0].toSize(); const int fps = mode[1].toInt();
m_resolution->addItem(QStringLiteral("%1×%2 · %3 fps").arg(size.width()).arg(size.height()).arg(fps), size);
m_resolution->setItemData(m_resolution->count() - 1, fps, Qt::UserRole + 1);
}
int index = -1;
for (int i = 0; i < m_resolution->count(); ++i)
if (m_resolution->itemData(i).toSize() == wanted && m_resolution->itemData(i, Qt::UserRole + 1).toInt() == wantedFps) index = i;
if (index < 0) index = m_resolution->findData(QSize(640, 480));
if (index < 0) index = m_resolution->count() - 1;
m_resolution->setCurrentIndex(index); m_resolution->setEnabled(index >= 0);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call camera resolutions device=%s count=%d requested=%dx%d selected=%dx%d fps=%d",
m_camera->currentData().toByteArray().constData(), m_resolution->count(), wanted.width(), wanted.height(),
m_resolution->currentData().toSize().width(), m_resolution->currentData().toSize().height(),
m_resolution->currentData(Qt::UserRole + 1).toInt());
};
connect(m_video, &CallVideoEngine::camerasChanged, this, [this, refreshModes](const CallCameraDevices& cameras) {
const auto selected = m_camera->currentData();
m_camera->clear();
for (const auto& camera : cameras) {
QVariantList sizes;
for (const auto& mode : camera.modes) sizes.append(QVariant(QVariantList{mode.size, mode.fps}));
m_camera->addItem(camera.name, camera.id);
m_camera->setItemData(m_camera->count() - 1, sizes, Qt::UserRole + 1);
}
int index = m_camera->findData(selected);
if (index < 0) for (int i = 0; i < m_camera->count(); ++i) {
if (!m_camera->itemData(i, Qt::UserRole + 1).toList().isEmpty()) { index = i; break; }
}
if (index >= 0) m_camera->setCurrentIndex(index);
refreshModes();
if (m_videoEnabled->isChecked()) applyVideo();
});
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(); });
connect(m_camera, QOverload<int>::of(&QComboBox::activated), this, [this, refreshModes] { refreshModes(); applyVideo(); });
for (auto* combo : {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, quint64 serial) {
bool changed = m_remoteImage.isNull() != image.isNull(); m_remoteImage = image;
m_videoWindow->remote = image; m_videoWindow->serial = serial; m_videoWindow->callId = m_videoCall;
m_videoWindow->requestedAt = call_video_playout_now_us(); m_videoWindow->update();
if (changed) updateVideoPresentation();
// Скрытое окно продолжает принимать поток, не ждёт невозможного paintEvent.
if (!m_videoWindow->isVisible() || m_videoWindow->isMinimized()) m_video->imagePresented(serial);
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 WaitDecision:
m_statusLabel->setText(callText("Subscriber is on a phone call"));
m_durationLabel->hide();
break;
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(CallAudioEngine::instance()->captureError().isEmpty()
? callText("In call") : CallAudioEngine::instance()->captureError());
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_continueBtn->setVisible(m_state == WaitDecision);
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_state == WaitDecision);
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());
QSize panelMinimum = m_panel->minimumSizeHint();
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call layout active=%d preview=%d settings=%d window=%dx%d panel_min=%dx%d",
active, m_localPreview->isVisible(), settings, width(), height(), panelMinimum.width(), panelMinimum.height());
update();
}
void CallWindow::applyVideo() {
if (!m_videoCall) return;
QSize size = m_resolution->currentData().toSize();
const bool enabled = m_videoEnabled->isChecked() && !size.isEmpty();
if (m_videoEnabled->isChecked() && !enabled) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call camera has no selected mode device=%s", m_camera->currentData().toByteArray().constData());
m_videoStatus->setText(callText("No supported video modes")); m_videoStatus->show();
}
if (size.isEmpty()) size = QSize(640, 480);
const int fps = m_resolution->currentData(Qt::UserRole + 1).toInt();
m_video->setVideo(enabled, m_camera->currentData().toByteArray(), size.width(), size.height(), m_codec->currentData().toInt(), fps ? fps : 30);
}
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 || m_state == WaitDecision)
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);
}