2025-12-07 12:00:44 -07:00
|
|
|
#include "video_grid_widget.h"
|
2025-12-07 14:03:08 -07:00
|
|
|
#include "../media/screen_capture.h"
|
|
|
|
|
#include "../connection/client_connection.h"
|
|
|
|
|
#include "../../shared/protocol/message.h"
|
2025-12-07 12:00:44 -07:00
|
|
|
#include <QFrame>
|
2025-12-07 14:03:08 -07:00
|
|
|
#include <QResizeEvent>
|
2025-12-07 17:52:28 -07:00
|
|
|
#include <algorithm>
|
2025-12-07 12:00:44 -07:00
|
|
|
#include <cmath>
|
2025-12-07 14:03:08 -07:00
|
|
|
#include <iostream>
|
2025-12-07 12:00:44 -07:00
|
|
|
|
|
|
|
|
namespace scar {
|
|
|
|
|
|
|
|
|
|
VideoGridWidget::VideoGridWidget(QWidget* parent)
|
2025-12-07 14:03:08 -07:00
|
|
|
: QWidget(parent),
|
|
|
|
|
isScreenSharing_(false),
|
2025-12-07 17:52:28 -07:00
|
|
|
screenShareStartSent_(false),
|
2025-12-07 20:12:08 -07:00
|
|
|
connection_(nullptr),
|
|
|
|
|
processingFrame_(false) {
|
2025-12-07 12:00:44 -07:00
|
|
|
|
|
|
|
|
gridLayout_ = new QGridLayout(this);
|
|
|
|
|
gridLayout_->setSpacing(5);
|
|
|
|
|
gridLayout_->setContentsMargins(5, 5, 5, 5);
|
|
|
|
|
|
|
|
|
|
// Placeholder when no streams
|
|
|
|
|
placeholderLabel_ = new QLabel("No active video streams", this);
|
|
|
|
|
placeholderLabel_->setAlignment(Qt::AlignCenter);
|
|
|
|
|
placeholderLabel_->setStyleSheet("color: #888; font-size: 16px;");
|
|
|
|
|
gridLayout_->addWidget(placeholderLabel_, 0, 0);
|
2025-12-07 14:03:08 -07:00
|
|
|
|
|
|
|
|
// Create screen share button (floating above video streams)
|
|
|
|
|
screenShareButton_ = new QPushButton("Share Screen", this);
|
|
|
|
|
screenShareButton_->setStyleSheet(
|
|
|
|
|
"QPushButton {"
|
|
|
|
|
" background-color: #5865F2;"
|
|
|
|
|
" color: white;"
|
|
|
|
|
" border: none;"
|
|
|
|
|
" border-radius: 8px;"
|
|
|
|
|
" padding: 12px 24px;"
|
|
|
|
|
" font-size: 14px;"
|
|
|
|
|
" font-weight: bold;"
|
|
|
|
|
"}"
|
|
|
|
|
"QPushButton:hover {"
|
|
|
|
|
" background-color: #4752C4;"
|
|
|
|
|
"}"
|
|
|
|
|
"QPushButton:pressed {"
|
|
|
|
|
" background-color: #3C45A5;"
|
|
|
|
|
"}"
|
|
|
|
|
"QPushButton:disabled {"
|
|
|
|
|
" background-color: #4F545C;"
|
|
|
|
|
" color: #72767D;"
|
|
|
|
|
"}"
|
|
|
|
|
);
|
|
|
|
|
screenShareButton_->setCursor(Qt::PointingHandCursor);
|
|
|
|
|
screenShareButton_->setFixedSize(160, 48);
|
|
|
|
|
screenShareButton_->raise(); // Ensure button is above other widgets
|
|
|
|
|
|
|
|
|
|
connect(screenShareButton_, &QPushButton::clicked, this, &VideoGridWidget::onScreenShareClicked);
|
|
|
|
|
|
|
|
|
|
// Initialize screen capture
|
|
|
|
|
screenCapture_ = new ScreenCapture();
|
|
|
|
|
|
|
|
|
|
updateScreenShareButtonPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VideoGridWidget::~VideoGridWidget() {
|
|
|
|
|
delete screenCapture_;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void VideoGridWidget::setConnection(ClientConnection* connection) {
|
|
|
|
|
connection_ = connection;
|
|
|
|
|
|
|
|
|
|
// Set up frame callback for screen capture
|
|
|
|
|
if (screenCapture_) {
|
|
|
|
|
screenCapture_->setFrameCallback([this](const std::vector<uint8_t>& frameData, int width, int height) {
|
|
|
|
|
if (!connection_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-07 17:52:28 -07:00
|
|
|
// Send ScreenShareStart with actual dimensions on first frame
|
|
|
|
|
if (!screenShareStartSent_) {
|
|
|
|
|
ScreenShareStart startMsg(width, height);
|
|
|
|
|
auto startSerialized = startMsg.serialize();
|
|
|
|
|
connection_->sendData(startSerialized);
|
|
|
|
|
std::cout << "Sent ScreenShareStart with actual dimensions: " << width << "x" << height << std::endl;
|
|
|
|
|
screenShareStartSent_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
static int callback_count = 0;
|
|
|
|
|
if (++callback_count % 30 == 0) {
|
|
|
|
|
std::cout << "Sending screen share frame: " << callback_count
|
|
|
|
|
<< " Size: " << frameData.size() << " bytes"
|
|
|
|
|
<< " Resolution: " << width << "x" << height << std::endl;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 17:52:28 -07:00
|
|
|
// frameData is already H.264 encoded by ScreenCapture
|
|
|
|
|
// Send it directly to server
|
2025-12-07 14:03:08 -07:00
|
|
|
ScreenShareData frameMsg(frameData);
|
|
|
|
|
auto serialized = frameMsg.serialize();
|
|
|
|
|
connection_->sendData(serialized);
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
std::cerr << "Error in frame callback: " << e.what() << std::endl;
|
|
|
|
|
} catch (...) {
|
|
|
|
|
std::cerr << "Unknown error in frame callback" << std::endl;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-07 12:00:44 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void VideoGridWidget::addStream(const QString& streamId, const QString& username) {
|
|
|
|
|
if (streams_.size() >= MAX_STREAMS) {
|
|
|
|
|
return; // Max streams reached
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove placeholder if this is first stream
|
|
|
|
|
if (streams_.empty()) {
|
|
|
|
|
placeholderLabel_->hide();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
VideoStream stream;
|
|
|
|
|
stream.streamId = streamId;
|
|
|
|
|
stream.username = username;
|
|
|
|
|
|
|
|
|
|
// Create video label
|
|
|
|
|
stream.videoLabel = new QLabel(this);
|
|
|
|
|
stream.videoLabel->setMinimumSize(160, 120);
|
|
|
|
|
stream.videoLabel->setStyleSheet("QLabel { background-color: #2C2F33; border: 1px solid #23272A; }");
|
|
|
|
|
stream.videoLabel->setScaledContents(true);
|
|
|
|
|
stream.videoLabel->setAlignment(Qt::AlignCenter);
|
|
|
|
|
stream.videoLabel->setText(username + "\n(No video)");
|
|
|
|
|
stream.videoLabel->setToolTip(username);
|
|
|
|
|
|
|
|
|
|
streams_.push_back(stream);
|
|
|
|
|
updateGridLayout();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void VideoGridWidget::removeStream(const QString& streamId) {
|
|
|
|
|
for (auto it = streams_.begin(); it != streams_.end(); ++it) {
|
|
|
|
|
if (it->streamId == streamId) {
|
|
|
|
|
delete it->videoLabel;
|
|
|
|
|
streams_.erase(it);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (streams_.empty()) {
|
|
|
|
|
placeholderLabel_->show();
|
|
|
|
|
} else {
|
|
|
|
|
updateGridLayout();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void VideoGridWidget::updateFrame(const QString& streamId, const QPixmap& frame) {
|
|
|
|
|
for (auto& stream : streams_) {
|
|
|
|
|
if (stream.streamId == streamId) {
|
|
|
|
|
stream.videoLabel->setPixmap(frame);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void VideoGridWidget::clear() {
|
|
|
|
|
for (auto& stream : streams_) {
|
|
|
|
|
delete stream.videoLabel;
|
|
|
|
|
}
|
|
|
|
|
streams_.clear();
|
|
|
|
|
placeholderLabel_->show();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void VideoGridWidget::updateGridLayout() {
|
|
|
|
|
// Remove all widgets from layout
|
|
|
|
|
for (auto& stream : streams_) {
|
|
|
|
|
gridLayout_->removeWidget(stream.videoLabel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int columns = calculateColumns(streams_.size());
|
|
|
|
|
int rows = std::ceil(static_cast<double>(streams_.size()) / columns);
|
|
|
|
|
|
|
|
|
|
// Re-add widgets in grid
|
|
|
|
|
for (size_t i = 0; i < streams_.size(); ++i) {
|
|
|
|
|
int row = i / columns;
|
|
|
|
|
int col = i % columns;
|
|
|
|
|
gridLayout_->addWidget(streams_[i].videoLabel, row, col);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int VideoGridWidget::calculateColumns(int streamCount) {
|
|
|
|
|
if (streamCount <= 1) return 1;
|
|
|
|
|
if (streamCount <= 4) return 2;
|
|
|
|
|
if (streamCount <= 9) return 3;
|
|
|
|
|
if (streamCount <= 16) return 4;
|
|
|
|
|
if (streamCount <= 25) return 5;
|
|
|
|
|
if (streamCount <= 36) return 6;
|
|
|
|
|
return std::ceil(std::sqrt(streamCount));
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
void VideoGridWidget::onScreenShareClicked() {
|
|
|
|
|
if (isScreenSharing_) {
|
|
|
|
|
// Stop screen sharing
|
|
|
|
|
screenCapture_->stop();
|
|
|
|
|
isScreenSharing_ = false;
|
2025-12-07 17:52:28 -07:00
|
|
|
screenShareButton_->setText("Share Screen");
|
|
|
|
|
screenShareButton_->setStyleSheet(
|
|
|
|
|
"QPushButton {"
|
|
|
|
|
" background-color: #5865F2;"
|
|
|
|
|
" color: white;"
|
|
|
|
|
" border: none;"
|
|
|
|
|
" border-radius: 8px;"
|
|
|
|
|
" padding: 12px 24px;"
|
|
|
|
|
" font-size: 14px;"
|
|
|
|
|
" font-weight: bold;"
|
|
|
|
|
"}"
|
|
|
|
|
"QPushButton:hover {"
|
|
|
|
|
" background-color: #4752C4;"
|
|
|
|
|
"}"
|
|
|
|
|
"QPushButton:pressed {"
|
|
|
|
|
" background-color: #3C45A5;"
|
|
|
|
|
"}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
screenShareStartSent_ = false; // Send stop message
|
2025-12-07 14:03:08 -07:00
|
|
|
if (connection_) {
|
|
|
|
|
ScreenShareStop stopMsg;
|
|
|
|
|
auto serialized = stopMsg.serialize();
|
|
|
|
|
connection_->sendData(serialized);
|
|
|
|
|
qDebug() << "Sent screen share stop message";
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Start screen sharing - this will open xdg-desktop-portal dialog
|
|
|
|
|
if (screenCapture_->start()) {
|
|
|
|
|
isScreenSharing_ = true;
|
|
|
|
|
screenShareButton_->setText("Stop Sharing");
|
|
|
|
|
screenShareButton_->setStyleSheet(
|
|
|
|
|
"QPushButton {"
|
|
|
|
|
" background-color: #ED4245;"
|
|
|
|
|
" color: white;"
|
|
|
|
|
" border: none;"
|
|
|
|
|
" border-radius: 8px;"
|
|
|
|
|
" padding: 12px 24px;"
|
|
|
|
|
" font-size: 14px;"
|
|
|
|
|
" font-weight: bold;"
|
|
|
|
|
"}"
|
|
|
|
|
"QPushButton:hover {"
|
|
|
|
|
" background-color: #C03537;"
|
|
|
|
|
"}"
|
|
|
|
|
"QPushButton:pressed {"
|
|
|
|
|
" background-color: #A12D2F;"
|
|
|
|
|
"}"
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-07 17:52:28 -07:00
|
|
|
// ScreenShareStart will be sent with actual dimensions when first frame arrives
|
2025-12-07 14:03:08 -07:00
|
|
|
emit screenShareRequested();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 20:12:08 -07:00
|
|
|
void VideoGridWidget::onScreenShareFrameReceived(std::shared_ptr<std::vector<uint8_t>> rgbData, int width, int height, int linesize) {
|
|
|
|
|
static int frame_count = 0;
|
|
|
|
|
static int dropped_frames = 0;
|
|
|
|
|
++frame_count;
|
2025-12-07 17:52:28 -07:00
|
|
|
|
2025-12-07 20:12:08 -07:00
|
|
|
// Drop frame if we're still processing the previous one
|
|
|
|
|
bool expected = false;
|
|
|
|
|
if (!processingFrame_.compare_exchange_strong(expected, true)) {
|
|
|
|
|
++dropped_frames;
|
|
|
|
|
if (dropped_frames % 10 == 1) {
|
|
|
|
|
std::cout << "Dropped frame " << frame_count << " (total dropped: " << dropped_frames
|
|
|
|
|
<< ") - still processing previous frame" << std::endl;
|
|
|
|
|
}
|
2025-12-07 17:52:28 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 20:12:08 -07:00
|
|
|
// RAII guard to ensure we always clear the processing flag
|
|
|
|
|
struct FrameGuard {
|
|
|
|
|
std::atomic<bool>& flag;
|
|
|
|
|
~FrameGuard() { flag = false; }
|
|
|
|
|
} guard{processingFrame_};
|
2025-12-07 17:52:28 -07:00
|
|
|
|
2025-12-07 20:12:08 -07:00
|
|
|
std::cout << "=== onScreenShareFrameReceived START frame #" << frame_count
|
|
|
|
|
<< ": size=" << rgbData->size()
|
|
|
|
|
<< ", " << width << "x" << height << ", linesize=" << linesize << " ===" << std::endl;
|
2025-12-07 17:52:28 -07:00
|
|
|
|
2025-12-07 20:12:08 -07:00
|
|
|
if (rgbData->empty()) {
|
|
|
|
|
std::cerr << "Empty rgbData, returning" << std::endl;
|
2025-12-07 17:52:28 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 20:12:08 -07:00
|
|
|
// Validate buffer size BEFORE doing anything
|
|
|
|
|
size_t expected_size = static_cast<size_t>(linesize) * height;
|
|
|
|
|
if (rgbData->size() < expected_size) {
|
|
|
|
|
std::cerr << "Buffer too small: " << rgbData->size() << " < " << expected_size << std::endl;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::cout << "Creating QImage wrapper (no allocation) for frame " << frame_count << std::endl;
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Use QImage constructor that wraps external buffer (NO COPY)
|
|
|
|
|
// QImage does NOT take ownership - it just references our data
|
|
|
|
|
// We need to ensure rgbData stays alive until QPixmap copies it
|
|
|
|
|
QImage tempImage(rgbData->data(), width, height, linesize, QImage::Format_RGB888);
|
|
|
|
|
|
|
|
|
|
if (tempImage.isNull()) {
|
|
|
|
|
std::cerr << "Failed to create QImage wrapper" << std::endl;
|
|
|
|
|
return;
|
2025-12-07 17:52:28 -07:00
|
|
|
}
|
|
|
|
|
|
2025-12-07 20:12:08 -07:00
|
|
|
// No manual copy needed - tempImage wraps rgbData directly (zero-copy)
|
|
|
|
|
// QImage will use our linesize, no mismatch issues
|
|
|
|
|
std::cout << "QImage wrapper verified, buffer size=" << rgbData->size()
|
|
|
|
|
<< ", linesize=" << linesize << std::endl;
|
|
|
|
|
|
2025-12-07 17:52:28 -07:00
|
|
|
// Create a stream if this is the first frame from a remote screen share
|
|
|
|
|
// Use a special stream ID for remote screen shares
|
|
|
|
|
const QString remoteStreamId = "remote_screen_share";
|
|
|
|
|
|
|
|
|
|
// Find existing stream or create new one
|
|
|
|
|
auto it = std::find_if(streams_.begin(), streams_.end(),
|
|
|
|
|
[&remoteStreamId](const VideoStream& s) { return s.streamId == remoteStreamId; });
|
|
|
|
|
|
|
|
|
|
if (it == streams_.end()) {
|
2025-12-07 20:12:08 -07:00
|
|
|
std::cout << "Creating new stream for remote screen share..." << std::endl;
|
2025-12-07 17:52:28 -07:00
|
|
|
// Create new stream for remote screen share
|
|
|
|
|
addStream(remoteStreamId, "Screen Share");
|
2025-12-07 20:12:08 -07:00
|
|
|
std::cout << "Stream created, finding it..." << std::endl;
|
2025-12-07 17:52:28 -07:00
|
|
|
it = std::find_if(streams_.begin(), streams_.end(),
|
|
|
|
|
[&remoteStreamId](const VideoStream& s) { return s.streamId == remoteStreamId; });
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 20:12:08 -07:00
|
|
|
std::cout << "About to convert QImage to QPixmap..." << std::endl;
|
|
|
|
|
|
2025-12-07 17:52:28 -07:00
|
|
|
if (it != streams_.end() && it->videoLabel) {
|
2025-12-07 20:12:08 -07:00
|
|
|
std::cout << "Converting to QPixmap (this will make a deep copy)..." << std::endl;
|
|
|
|
|
// QPixmap::fromImage makes its own deep copy - after this, rgbData can be released
|
|
|
|
|
QPixmap pixmap = QPixmap::fromImage(tempImage);
|
|
|
|
|
|
|
|
|
|
std::cout << "QPixmap created, use_count=" << rgbData.use_count() << std::endl;
|
|
|
|
|
|
|
|
|
|
// CRITICAL: Explicitly destroy tempImage BEFORE rgbData goes out of scope
|
|
|
|
|
// to prevent QImage from holding dangling pointer during destruction
|
|
|
|
|
tempImage = QImage(); // Assign empty QImage to release reference
|
|
|
|
|
|
|
|
|
|
std::cout << "tempImage cleared, use_count=" << rgbData.use_count() << std::endl;
|
|
|
|
|
|
|
|
|
|
if (pixmap.isNull()) {
|
|
|
|
|
std::cerr << "Failed to create QPixmap from QImage" << std::endl;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::cout << "QPixmap valid, about to scale..." << std::endl;
|
|
|
|
|
|
|
|
|
|
// Scale to fit the label - this creates another copy
|
2025-12-07 17:52:28 -07:00
|
|
|
QPixmap scaled = pixmap.scaled(it->videoLabel->size(),
|
|
|
|
|
Qt::KeepAspectRatio,
|
|
|
|
|
Qt::SmoothTransformation);
|
2025-12-07 20:12:08 -07:00
|
|
|
|
|
|
|
|
std::cout << "Scaled pixmap created, about to set on label..." << std::endl;
|
|
|
|
|
|
|
|
|
|
// Set the pixmap - QLabel makes its own copy internally
|
2025-12-07 17:52:28 -07:00
|
|
|
it->videoLabel->setPixmap(scaled);
|
2025-12-07 20:12:08 -07:00
|
|
|
|
|
|
|
|
std::cout << "Pixmap set on label" << std::endl;
|
|
|
|
|
|
|
|
|
|
if (frame_count == 1 || frame_count % 30 == 0) {
|
|
|
|
|
std::cout << "Frame " << frame_count << " displayed successfully" << std::endl;
|
|
|
|
|
}
|
2025-12-07 17:52:28 -07:00
|
|
|
}
|
2025-12-07 20:12:08 -07:00
|
|
|
|
|
|
|
|
std::cout << "Function complete, rgbData going out of scope, use_count="
|
|
|
|
|
<< rgbData.use_count() << std::endl;
|
|
|
|
|
|
|
|
|
|
// rgbData will be destroyed when all references are gone
|
|
|
|
|
// tempImage was explicitly cleared above
|
2025-12-07 17:52:28 -07:00
|
|
|
}
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
void VideoGridWidget::resizeEvent(QResizeEvent* event) {
|
|
|
|
|
QWidget::resizeEvent(event);
|
|
|
|
|
updateScreenShareButtonPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void VideoGridWidget::updateScreenShareButtonPosition() {
|
|
|
|
|
// Position button at bottom center of the widget
|
|
|
|
|
int buttonWidth = screenShareButton_->width();
|
|
|
|
|
int buttonHeight = screenShareButton_->height();
|
|
|
|
|
int x = (width() - buttonWidth) / 2;
|
|
|
|
|
int y = height() - buttonHeight - 20; // 20px margin from bottom
|
|
|
|
|
|
|
|
|
|
screenShareButton_->move(x, y);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 12:00:44 -07:00
|
|
|
} // namespace scar
|