Add live call video controls and display to the desktop call window
This commit is contained in:
1 parent
e682f30ca9
commit
af35d03399
3 files changed
+70
-3
No files matched your search
@@ -139,7 +139,7 @@ if(PkgConfig_FOUND)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Общий sender остаётся в libutun; Qt захватывает видео, FFmpeg/Opus работают в publisher workers.
|
# Общий sender остаётся в libutun; Qt захватывает видео, FFmpeg/Opus работают в publisher workers.
|
||||||
add_library(chatgui_live_video STATIC src/live_video_encoder.cpp src/call_video_encoder.cpp)
|
add_library(chatgui_live_video STATIC src/live_video_encoder.cpp src/call_video_encoder.cpp src/call_video_decoder.cpp)
|
||||||
target_link_libraries(chatgui_live_video PUBLIC ${QT_LIBS} utun PRIVATE ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES})
|
target_link_libraries(chatgui_live_video PUBLIC ${QT_LIBS} utun PRIVATE ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES})
|
||||||
target_include_directories(chatgui_live_video PRIVATE ${FFMPEG_INCLUDE_DIRS})
|
target_include_directories(chatgui_live_video PRIVATE ${FFMPEG_INCLUDE_DIRS})
|
||||||
if(FFMPEG_FOUND)
|
if(FFMPEG_FOUND)
|
||||||
@@ -150,7 +150,7 @@ if(FFMPEG_FOUND)
|
|||||||
add_test(NAME test_call_video_encoder COMMAND test_call_video_encoder)
|
add_test(NAME test_call_video_encoder COMMAND test_call_video_encoder)
|
||||||
set_tests_properties(test_call_video_encoder PROPERTIES TIMEOUT 120)
|
set_tests_properties(test_call_video_encoder PROPERTIES TIMEOUT 120)
|
||||||
endif()
|
endif()
|
||||||
add_library(chatgui_stream_publish STATIC src/stream_publish_engine.cpp src/stream_publish_window.cpp)
|
add_library(chatgui_stream_publish STATIC src/stream_publish_engine.cpp src/stream_publish_window.cpp src/call_video_engine.cpp)
|
||||||
target_link_libraries(chatgui_stream_publish PUBLIC chatgui_live_video)
|
target_link_libraries(chatgui_stream_publish PUBLIC chatgui_live_video)
|
||||||
target_include_directories(chatgui_stream_publish PRIVATE ${CMAKE_SOURCE_DIR}/../../lib/libopus/include)
|
target_include_directories(chatgui_stream_publish PRIVATE ${CMAKE_SOURCE_DIR}/../../lib/libopus/include)
|
||||||
set(GUI_CAPTURE_ENABLED 0)
|
set(GUI_CAPTURE_ENABLED 0)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#include "callwindow.h"
|
#include "callwindow.h"
|
||||||
#include "../transport/gui_bridge.h"
|
#include "../transport/gui_bridge.h"
|
||||||
#include "call_audio_engine.h"
|
#include "call_audio_engine.h"
|
||||||
|
#include "call_video_engine.h"
|
||||||
|
#include "../../../src/call/call_video_port.h"
|
||||||
#include "sound_manager.h"
|
#include "sound_manager.h"
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@@ -9,6 +11,8 @@
|
|||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QSpinBox>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QCloseEvent>
|
#include <QCloseEvent>
|
||||||
@@ -138,7 +142,8 @@ static QWidget* wrapWithCaption(QWidget* btn, const QString& caption, QLabel** c
|
|||||||
CallWindow::CallWindow(QWidget* parent)
|
CallWindow::CallWindow(QWidget* parent)
|
||||||
: QWidget(parent, Qt::Window) {
|
: QWidget(parent, Qt::Window) {
|
||||||
setWindowTitle(QCoreApplication::translate("ChatGui", "Call"));
|
setWindowTitle(QCoreApplication::translate("ChatGui", "Call"));
|
||||||
setFixedSize(360, 480);
|
resize(640, 800);
|
||||||
|
setMinimumSize(400, 600);
|
||||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||||
|
|
||||||
m_avatarLabel = new QLabel(this);
|
m_avatarLabel = new QLabel(this);
|
||||||
@@ -193,6 +198,45 @@ CallWindow::CallWindow(QWidget* parent)
|
|||||||
lay->addWidget(m_connectionLabel);
|
lay->addWidget(m_connectionLabel);
|
||||||
lay->addWidget(m_bgpPathLabel);
|
lay->addWidget(m_bgpPathLabel);
|
||||||
lay->addWidget(m_statsLabel);
|
lay->addWidget(m_statsLabel);
|
||||||
|
m_video = new CallVideoEngine(this);
|
||||||
|
m_videoImage = new QLabel(this); m_videoImage->setMinimumHeight(180); m_videoImage->setAlignment(Qt::AlignCenter);
|
||||||
|
lay->addWidget(m_videoImage, 1);
|
||||||
|
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);
|
||||||
|
m_videoEnabled = new QCheckBox(tr("Send video"), this); videoLayout->addWidget(m_videoEnabled);
|
||||||
|
auto* modes = new QHBoxLayout();
|
||||||
|
m_camera = new QComboBox(this);
|
||||||
|
for (const auto& camera : CallVideoEngine::cameras()) m_camera->addItem(camera.first, camera.second);
|
||||||
|
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(tr("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(tr("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, m_videoStatus, &QLabel::setText);
|
||||||
|
connect(m_video, &CallVideoEngine::imageChanged, this, [this](const QImage& image) {
|
||||||
|
m_videoImage->setPixmap(image.isNull() ? QPixmap() : QPixmap::fromImage(image).scaled(m_videoImage->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||||
|
});
|
||||||
|
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();
|
||||||
lay->addStretch(1);
|
lay->addStretch(1);
|
||||||
|
|
||||||
m_acceptCell = wrapWithCaption(m_acceptBtn, QCoreApplication::translate("ChatGui", "Accept"));
|
m_acceptCell = wrapWithCaption(m_acceptBtn, QCoreApplication::translate("ChatGui", "Accept"));
|
||||||
@@ -269,6 +313,7 @@ void CallWindow::setState(State s) {
|
|||||||
m_muteBtn->setChecked(false);
|
m_muteBtn->setChecked(false);
|
||||||
break;
|
break;
|
||||||
case Ended:
|
case Ended:
|
||||||
|
m_video->stop(); m_videoCall = 0;
|
||||||
setConnectionType(0, 0);
|
setConnectionType(0, 0);
|
||||||
m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Ended"));
|
m_statusLabel->setText(QCoreApplication::translate("ChatGui", "Ended"));
|
||||||
m_durationTimer->stop();
|
m_durationTimer->stop();
|
||||||
@@ -300,6 +345,15 @@ void CallWindow::updateButtons() {
|
|||||||
m_hangupCell->setVisible(m_state == Outgoing || m_state == Ringing || m_state == Active);
|
m_hangupCell->setVisible(m_state == Outgoing || m_state == Ringing || m_state == Active);
|
||||||
m_muteCell->setVisible(m_state == Active);
|
m_muteCell->setVisible(m_state == Active);
|
||||||
m_deviceCombo->setVisible(m_state == Active);
|
m_deviceCombo->setVisible(m_state == Active);
|
||||||
|
m_videoControls->setVisible(m_state == Active);
|
||||||
|
m_videoImage->setVisible(m_state == Active);
|
||||||
|
m_videoStatus->setVisible(m_state == Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
void CallWindow::postCallFn(void (*fn)(void*), uint64_t callId) {
|
||||||
@@ -338,6 +392,7 @@ void CallWindow::onDeviceChanged(int index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CallWindow::closeEvent(QCloseEvent* event) {
|
void CallWindow::closeEvent(QCloseEvent* event) {
|
||||||
|
m_video->stop(); m_videoCall = 0;
|
||||||
if (m_state == Outgoing || m_state == Ringing || m_state == Active)
|
if (m_state == Outgoing || m_state == Ringing || m_state == Active)
|
||||||
postCallFn(call_hangup_trampoline, m_callId);
|
postCallFn(call_hangup_trampoline, m_callId);
|
||||||
emit closed(m_callId);
|
emit closed(m_callId);
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ class QLabel;
|
|||||||
class QPushButton;
|
class QPushButton;
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
class QTimer;
|
class QTimer;
|
||||||
|
class QCheckBox;
|
||||||
|
class QSpinBox;
|
||||||
|
class CallVideoEngine;
|
||||||
|
|
||||||
/* Окно звонка: состояния Outgoing (пытаемся связаться), Ringing (звоним),
|
/* Окно звонка: состояния Outgoing (пытаемся связаться), Ringing (звоним),
|
||||||
* Incoming (принять/отклонить), Active (идёт разговор, таймер), Ended (завершён). */
|
* Incoming (принять/отклонить), Active (идёт разговор, таймер), Ended (завершён). */
|
||||||
@@ -46,6 +49,15 @@ private:
|
|||||||
void updateButtons();
|
void updateButtons();
|
||||||
void populateDeviceCombo();
|
void populateDeviceCombo();
|
||||||
void postCallFn(void (*fn)(void*), uint64_t callId);
|
void postCallFn(void (*fn)(void*), uint64_t callId);
|
||||||
|
void applyVideo();
|
||||||
|
CallVideoEngine* m_video;
|
||||||
|
QCheckBox* m_videoEnabled;
|
||||||
|
QComboBox *m_camera, *m_resolution, *m_codec;
|
||||||
|
QSpinBox *m_videoMin, *m_videoMax;
|
||||||
|
QLabel *m_videoImage, *m_videoStatus;
|
||||||
|
QWidget* m_videoControls;
|
||||||
|
quint64 m_videoCall = 0;
|
||||||
|
bool m_videoRangeReady = false;
|
||||||
|
|
||||||
quint64 m_callId = 0;
|
quint64 m_callId = 0;
|
||||||
State m_state = Outgoing;
|
State m_state = Outgoing;
|
||||||
|
|||||||
Reference in new issue
Block a user