diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 75a276be..9f05c946 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -139,7 +139,7 @@ if(PkgConfig_FOUND) endif() # Общий 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_include_directories(chatgui_live_video PRIVATE ${FFMPEG_INCLUDE_DIRS}) if(FFMPEG_FOUND) @@ -150,7 +150,7 @@ if(FFMPEG_FOUND) add_test(NAME test_call_video_encoder COMMAND test_call_video_encoder) set_tests_properties(test_call_video_encoder PROPERTIES TIMEOUT 120) 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_include_directories(chatgui_stream_publish PRIVATE ${CMAKE_SOURCE_DIR}/../../lib/libopus/include) set(GUI_CAPTURE_ENABLED 0) diff --git a/tools/chatgui/src/callwindow.cpp b/tools/chatgui/src/callwindow.cpp index 6c3bbb2a..15ddbea3 100644 --- a/tools/chatgui/src/callwindow.cpp +++ b/tools/chatgui/src/callwindow.cpp @@ -2,6 +2,8 @@ #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 @@ -9,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -138,7 +142,8 @@ static QWidget* wrapWithCaption(QWidget* btn, const QString& caption, QLabel** c CallWindow::CallWindow(QWidget* parent) : QWidget(parent, Qt::Window) { setWindowTitle(QCoreApplication::translate("ChatGui", "Call")); - setFixedSize(360, 480); + resize(640, 800); + setMinimumSize(400, 600); setAttribute(Qt::WA_DeleteOnClose, false); m_avatarLabel = new QLabel(this); @@ -193,6 +198,45 @@ CallWindow::CallWindow(QWidget* parent) lay->addWidget(m_connectionLabel); lay->addWidget(m_bgpPathLabel); 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::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); m_acceptCell = wrapWithCaption(m_acceptBtn, QCoreApplication::translate("ChatGui", "Accept")); @@ -269,6 +313,7 @@ void CallWindow::setState(State s) { 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(); @@ -300,6 +345,15 @@ void CallWindow::updateButtons() { m_hangupCell->setVisible(m_state == Outgoing || m_state == Ringing || m_state == Active); m_muteCell->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) { @@ -338,6 +392,7 @@ void CallWindow::onDeviceChanged(int index) { } 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); diff --git a/tools/chatgui/src/callwindow.h b/tools/chatgui/src/callwindow.h index e0dd53fd..33c104f4 100644 --- a/tools/chatgui/src/callwindow.h +++ b/tools/chatgui/src/callwindow.h @@ -7,6 +7,9 @@ class QLabel; class QPushButton; class QComboBox; class QTimer; +class QCheckBox; +class QSpinBox; +class CallVideoEngine; /* Окно звонка: состояния Outgoing (пытаемся связаться), Ringing (звоним), * Incoming (принять/отклонить), Active (идёт разговор, таймер), Ended (завершён). */ @@ -46,6 +49,15 @@ private: void updateButtons(); void populateDeviceCombo(); 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; State m_state = Outgoing;