533 lines
24 KiB
C++
533 lines
24 KiB
C++
#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);
|
||
}
|