#include "video_grid_widget.h" #include "../media/screen_capture.h" #include "../connection/client_connection.h" #include "../../shared/protocol/message.h" #include #include #include #include #include namespace scar { VideoGridWidget::VideoGridWidget(QWidget* parent) : QWidget(parent), isScreenSharing_(false), screenShareStartSent_(false), connection_(nullptr), processingFrame_(false) { 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); // 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& frameData, int width, int height) { if (!connection_) { return; } try { // 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; } 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; } // frameData is already H.264 encoded by ScreenCapture // Send it directly to server 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; } }); } } 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(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)); } void VideoGridWidget::onScreenShareClicked() { if (isScreenSharing_) { // Stop screen sharing screenCapture_->stop(); isScreenSharing_ = false; 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 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;" "}" ); // ScreenShareStart will be sent with actual dimensions when first frame arrives emit screenShareRequested(); } } } void VideoGridWidget::onScreenShareFrameReceived(std::shared_ptr> rgbData, int width, int height, int linesize) { static int frame_count = 0; static int dropped_frames = 0; ++frame_count; // 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; } return; } // RAII guard to ensure we always clear the processing flag struct FrameGuard { std::atomic& flag; ~FrameGuard() { flag = false; } } guard{processingFrame_}; std::cout << "=== onScreenShareFrameReceived START frame #" << frame_count << ": size=" << rgbData->size() << ", " << width << "x" << height << ", linesize=" << linesize << " ===" << std::endl; if (rgbData->empty()) { std::cerr << "Empty rgbData, returning" << std::endl; return; } // Validate buffer size BEFORE doing anything size_t expected_size = static_cast(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; } // 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; // 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()) { std::cout << "Creating new stream for remote screen share..." << std::endl; // Create new stream for remote screen share addStream(remoteStreamId, "Screen Share"); std::cout << "Stream created, finding it..." << std::endl; it = std::find_if(streams_.begin(), streams_.end(), [&remoteStreamId](const VideoStream& s) { return s.streamId == remoteStreamId; }); } std::cout << "About to convert QImage to QPixmap..." << std::endl; if (it != streams_.end() && it->videoLabel) { 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 QPixmap scaled = pixmap.scaled(it->videoLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); std::cout << "Scaled pixmap created, about to set on label..." << std::endl; // Set the pixmap - QLabel makes its own copy internally it->videoLabel->setPixmap(scaled); 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; } } 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 } 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); } } // namespace scar