scar-chat7/client/ui/video_grid_widget.cpp

330 lines
11 KiB
C++
Raw Normal View History

2025-12-07 12:00:44 -07:00
#include "video_grid_widget.h"
#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>
#include <QResizeEvent>
#include <algorithm>
2025-12-07 12:00:44 -07:00
#include <cmath>
#include <iostream>
2025-12-07 12:00:44 -07:00
namespace scar {
VideoGridWidget::VideoGridWidget(QWidget* parent)
: QWidget(parent),
isScreenSharing_(false),
screenShareStartSent_(false),
connection_(nullptr) {
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);
// 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 {
// 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;
}
});
}
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));
}
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::vector<uint8_t> rgbData, int width, int height) {
std::cout << "=== onScreenShareFrameReceived START: size=" << rgbData.size()
<< ", " << width << "x" << height << " ===" << std::endl;
if (rgbData.empty()) {
std::cout << "Empty rgbData, returning" << std::endl;
return;
}
static int frame_count = 0;
if (++frame_count % 30 == 0) {
std::cout << "Received screen share frame " << frame_count
<< " (" << width << "x" << height << ", " << rgbData.size() << " bytes)" << std::endl;
}
// Calculate linesize (must match decoder's alignment)
int linesize = width * 3;
linesize = (linesize + 3) & ~3;
// Create QImage and copy data directly (avoid non-owning constructor)
QImage frameCopy(width, height, QImage::Format_RGB888);
if (frameCopy.isNull()) {
std::cerr << "Failed to create QImage" << std::endl;
return;
}
// Manually copy each scanline to handle potential linesize differences
for (int y = 0; y < height; ++y) {
const uint8_t* src_line = rgbData.data() + (y * linesize);
uint8_t* dst_line = frameCopy.scanLine(y);
memcpy(dst_line, src_line, width * 3);
}
// 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()) {
// Create new stream for remote screen share
addStream(remoteStreamId, "Screen Share");
it = std::find_if(streams_.begin(), streams_.end(),
[&remoteStreamId](const VideoStream& s) { return s.streamId == remoteStreamId; });
}
if (it != streams_.end() && it->videoLabel) {
// Convert QImage to QPixmap and scale to fit the label
QPixmap pixmap = QPixmap::fromImage(frameCopy);
QPixmap scaled = pixmap.scaled(it->videoLabel->size(),
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
it->videoLabel->setPixmap(scaled);
}
}
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