2025-12-07 12:00:44 -07:00
|
|
|
#include "screen_capture.h"
|
2025-12-07 14:03:08 -07:00
|
|
|
#include "video_encoder.h"
|
2025-12-07 12:00:44 -07:00
|
|
|
#include <iostream>
|
2025-12-07 12:30:16 -07:00
|
|
|
#include <cstring>
|
|
|
|
|
#include <random>
|
|
|
|
|
#include <sstream>
|
|
|
|
|
#include <iomanip>
|
|
|
|
|
#include <unistd.h>
|
2025-12-07 14:03:08 -07:00
|
|
|
#include <sys/mman.h>
|
|
|
|
|
#include <cerrno>
|
2025-12-07 12:00:44 -07:00
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
#ifdef __linux__
|
|
|
|
|
#include <sdbus-c++/sdbus-c++.h>
|
|
|
|
|
#include <pipewire/pipewire.h>
|
|
|
|
|
#include <spa/param/video/format-utils.h>
|
|
|
|
|
#include <spa/param/video/type-info.h>
|
|
|
|
|
#include <spa/param/format-utils.h>
|
|
|
|
|
#include <spa/param/video/format.h>
|
|
|
|
|
#include <spa/utils/hook.h>
|
|
|
|
|
#include <spa/pod/builder.h>
|
2025-12-07 14:03:08 -07:00
|
|
|
#include <spa/buffer/buffer.h>
|
2025-12-07 12:30:16 -07:00
|
|
|
#include <QImage>
|
|
|
|
|
#endif
|
2025-12-07 12:00:44 -07:00
|
|
|
|
|
|
|
|
namespace scar {
|
|
|
|
|
|
|
|
|
|
ScreenCapture::ScreenCapture()
|
2025-12-07 12:30:16 -07:00
|
|
|
: capturing_(false),
|
|
|
|
|
pw_loop_(nullptr),
|
|
|
|
|
pw_stream_(nullptr),
|
|
|
|
|
pw_context_(nullptr),
|
|
|
|
|
frame_width_(0),
|
2025-12-07 14:03:08 -07:00
|
|
|
frame_height_(0),
|
|
|
|
|
portal_session_ready_(false),
|
|
|
|
|
portal_sources_selected_(false),
|
|
|
|
|
portal_started_(false),
|
|
|
|
|
encoder_(nullptr) {
|
2025-12-07 12:00:44 -07:00
|
|
|
backend_ = detectBestBackend();
|
2025-12-07 12:30:16 -07:00
|
|
|
#ifdef __linux__
|
|
|
|
|
memset(&stream_listener_, 0, sizeof(stream_listener_));
|
|
|
|
|
#endif
|
2025-12-07 12:00:44 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ScreenCapture::~ScreenCapture() {
|
|
|
|
|
stop();
|
2025-12-07 12:30:16 -07:00
|
|
|
cleanupPipewire();
|
|
|
|
|
cleanupPortalConnection();
|
2025-12-07 12:00:44 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ScreenCaptureBackend ScreenCapture::detectBestBackend() {
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
return ScreenCaptureBackend::FFMPEG_WINDOWS;
|
|
|
|
|
#else
|
|
|
|
|
// Check for Wayland/Hyprland
|
|
|
|
|
const char* waylandDisplay = std::getenv("WAYLAND_DISPLAY");
|
|
|
|
|
const char* hyprlandInstance = std::getenv("HYPRLAND_INSTANCE_SIGNATURE");
|
|
|
|
|
|
|
|
|
|
if (waylandDisplay || hyprlandInstance) {
|
|
|
|
|
std::cout << "Detected Wayland/Hyprland environment" << std::endl;
|
|
|
|
|
return ScreenCaptureBackend::PORTAL_PIPEWIRE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback to X11
|
|
|
|
|
std::cout << "Detected X11 environment" << std::endl;
|
|
|
|
|
return ScreenCaptureBackend::FFMPEG_X11;
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ScreenCapture::start() {
|
|
|
|
|
return start(backend_);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ScreenCapture::start(ScreenCaptureBackend backend) {
|
|
|
|
|
if (capturing_) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
backend_ = backend;
|
|
|
|
|
|
|
|
|
|
switch (backend_) {
|
2025-12-07 12:30:16 -07:00
|
|
|
case ScreenCaptureBackend::PORTAL_PIPEWIRE:
|
|
|
|
|
return startPortalCapture();
|
2025-12-07 12:00:44 -07:00
|
|
|
case ScreenCaptureBackend::FFMPEG_X11:
|
|
|
|
|
return startX11Capture();
|
|
|
|
|
case ScreenCaptureBackend::FFMPEG_WAYLAND:
|
|
|
|
|
return startWaylandCapture();
|
|
|
|
|
case ScreenCaptureBackend::FFMPEG_WINDOWS:
|
|
|
|
|
return startWindowsCapture();
|
2025-12-07 12:30:16 -07:00
|
|
|
default:
|
|
|
|
|
std::cerr << "Unknown backend" << std::endl;
|
|
|
|
|
return false;
|
2025-12-07 12:00:44 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ScreenCapture::stop() {
|
|
|
|
|
if (!capturing_) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::cout << "Stopping screen capture..." << std::endl;
|
|
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
cleanupPipewire();
|
|
|
|
|
cleanupPortalConnection();
|
2025-12-07 12:00:44 -07:00
|
|
|
|
|
|
|
|
capturing_ = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ScreenCapture::setFrameCallback(FrameCallback callback) {
|
|
|
|
|
frameCallback_ = std::move(callback);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ScreenCapture::startX11Capture() {
|
2025-12-07 12:30:16 -07:00
|
|
|
std::cout << "X11 screen capture not yet implemented" << std::endl;
|
|
|
|
|
// TODO: Implement FFmpeg x11grab
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ScreenCapture::startWaylandCapture() {
|
|
|
|
|
std::cout << "FFmpeg Wayland capture not yet implemented" << std::endl;
|
|
|
|
|
// TODO: Implement FFmpeg Wayland
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ScreenCapture::startWindowsCapture() {
|
|
|
|
|
std::cout << "Windows screen capture not yet implemented" << std::endl;
|
|
|
|
|
// TODO: Implement FFmpeg GDI
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ScreenCapture::startPortalCapture() {
|
|
|
|
|
#ifndef __linux__
|
|
|
|
|
std::cerr << "Portal capture only supported on Linux" << std::endl;
|
|
|
|
|
return false;
|
|
|
|
|
#else
|
|
|
|
|
std::cout << "Starting Portal + Pipewire screen capture..." << std::endl;
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
// Initialize state
|
|
|
|
|
portal_session_ready_ = false;
|
|
|
|
|
portal_sources_selected_ = false;
|
|
|
|
|
portal_started_ = false;
|
|
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
// Step 1: Initialize portal connection
|
|
|
|
|
if (!initPortalConnection()) {
|
|
|
|
|
std::cerr << "Failed to connect to xdg-desktop-portal" << std::endl;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
// Step 2: Create portal session (async - will trigger onCreateSessionResponse)
|
2025-12-07 12:30:16 -07:00
|
|
|
std::string session_handle = createPortalSession();
|
|
|
|
|
if (session_handle.empty()) {
|
|
|
|
|
std::cerr << "Failed to create portal session" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
session_path_ = session_handle;
|
2025-12-07 12:00:44 -07:00
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
// The rest of the workflow continues in async callbacks
|
|
|
|
|
// We return true here as the request was initiated successfully
|
|
|
|
|
std::cout << "Portal workflow initiated, waiting for user interaction..." << std::endl;
|
2025-12-07 12:00:44 -07:00
|
|
|
return true;
|
2025-12-07 12:30:16 -07:00
|
|
|
#endif
|
2025-12-07 12:00:44 -07:00
|
|
|
}
|
|
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
#ifdef __linux__
|
|
|
|
|
bool ScreenCapture::initPortalConnection() {
|
|
|
|
|
try {
|
2025-12-07 14:03:08 -07:00
|
|
|
// Create session bus connection with event loop processing
|
2025-12-07 12:30:16 -07:00
|
|
|
portal_connection_ = sdbus::createSessionBusConnection();
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
// Enter event loop in async mode to process signals
|
|
|
|
|
portal_connection_->enterEventLoopAsync();
|
|
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
// Create proxy for ScreenCast portal interface
|
|
|
|
|
// Using standard org.freedesktop.portal.Desktop interface
|
|
|
|
|
// xdg-desktop-portal-hyprland implements this standard interface
|
|
|
|
|
screencast_proxy_ = sdbus::createProxy(
|
|
|
|
|
*portal_connection_,
|
|
|
|
|
sdbus::ServiceName{"org.freedesktop.portal.Desktop"},
|
|
|
|
|
sdbus::ObjectPath{"/org/freedesktop/portal/desktop"}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
std::cout << "Connected to xdg-desktop-portal (Hyprland implementation)" << std::endl;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (const sdbus::Error& e) {
|
|
|
|
|
std::cerr << "Failed to connect to portal: " << e.what() << std::endl;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ScreenCapture::cleanupPortalConnection() {
|
2025-12-07 14:03:08 -07:00
|
|
|
request_proxies_.clear(); // Clear signal handlers
|
2025-12-07 12:30:16 -07:00
|
|
|
screencast_proxy_.reset();
|
|
|
|
|
portal_connection_.reset();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string ScreenCapture::createPortalSession() {
|
|
|
|
|
try {
|
|
|
|
|
// Generate unique handle for this request
|
|
|
|
|
std::random_device rd;
|
|
|
|
|
std::mt19937 gen(rd());
|
|
|
|
|
std::uniform_int_distribution<> dis(0, 999999);
|
|
|
|
|
|
|
|
|
|
std::stringstream ss;
|
|
|
|
|
ss << "/org/freedesktop/portal/desktop/request/"
|
|
|
|
|
<< getpid() << "_" << dis(gen);
|
|
|
|
|
std::string request_handle = ss.str();
|
|
|
|
|
|
|
|
|
|
ss.str("");
|
|
|
|
|
ss << "/org/freedesktop/portal/desktop/session/"
|
|
|
|
|
<< getpid() << "_" << dis(gen);
|
|
|
|
|
std::string session_handle = ss.str();
|
|
|
|
|
|
|
|
|
|
std::cout << "Creating portal session..." << std::endl;
|
2025-12-07 14:03:08 -07:00
|
|
|
std::cout << " Predicted request handle: " << request_handle << std::endl;
|
2025-12-07 12:30:16 -07:00
|
|
|
std::cout << " Session handle: " << session_handle << std::endl;
|
|
|
|
|
|
|
|
|
|
// Build options dictionary
|
|
|
|
|
std::map<std::string, sdbus::Variant> options;
|
|
|
|
|
options["handle_token"] = sdbus::Variant{request_handle.substr(request_handle.rfind('/') + 1)};
|
|
|
|
|
options["session_handle_token"] = sdbus::Variant{session_handle.substr(session_handle.rfind('/') + 1)};
|
|
|
|
|
|
|
|
|
|
// Call CreateSession
|
|
|
|
|
sdbus::ObjectPath response_path;
|
|
|
|
|
screencast_proxy_->callMethod("CreateSession")
|
|
|
|
|
.onInterface("org.freedesktop.portal.ScreenCast")
|
|
|
|
|
.withArguments(options)
|
|
|
|
|
.storeResultsTo(response_path);
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
std::cout << "CreateSession returned request path: " << response_path << std::endl;
|
|
|
|
|
std::cout << " Expected path: " << request_handle << std::endl;
|
|
|
|
|
|
|
|
|
|
// Setup signal handler with the actual path returned by portal
|
|
|
|
|
setupResponseSignal(response_path, [this](uint32_t response, const std::map<std::string, sdbus::Variant>& results) {
|
|
|
|
|
onCreateSessionResponse(response, results);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
std::cout << "CreateSession request sent, waiting for response..." << std::endl;
|
2025-12-07 12:30:16 -07:00
|
|
|
return session_handle;
|
|
|
|
|
}
|
|
|
|
|
catch (const sdbus::Error& e) {
|
|
|
|
|
std::cerr << "Failed to create portal session: " << e.what() << std::endl;
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ScreenCapture::selectPortalSources(const std::string& session_handle) {
|
|
|
|
|
try {
|
|
|
|
|
// Generate unique request handle
|
|
|
|
|
std::random_device rd;
|
|
|
|
|
std::mt19937 gen(rd());
|
|
|
|
|
std::uniform_int_distribution<> dis(0, 999999);
|
|
|
|
|
|
|
|
|
|
std::stringstream ss;
|
|
|
|
|
ss << "/org/freedesktop/portal/desktop/request/"
|
|
|
|
|
<< getpid() << "_" << dis(gen);
|
|
|
|
|
std::string request_handle = ss.str();
|
|
|
|
|
|
|
|
|
|
std::cout << "Selecting portal sources..." << std::endl;
|
|
|
|
|
|
|
|
|
|
// Build options dictionary
|
|
|
|
|
std::map<std::string, sdbus::Variant> options;
|
|
|
|
|
options["handle_token"] = sdbus::Variant{request_handle.substr(request_handle.rfind('/') + 1)};
|
|
|
|
|
options["types"] = sdbus::Variant{uint32_t(1 | 2)}; // MONITOR (1) | WINDOW (2)
|
|
|
|
|
options["multiple"] = sdbus::Variant{false};
|
|
|
|
|
options["cursor_mode"] = sdbus::Variant{uint32_t(2)}; // Embedded cursor
|
|
|
|
|
|
|
|
|
|
// Call SelectSources
|
|
|
|
|
sdbus::ObjectPath response_path;
|
|
|
|
|
screencast_proxy_->callMethod("SelectSources")
|
|
|
|
|
.onInterface("org.freedesktop.portal.ScreenCast")
|
|
|
|
|
.withArguments(sdbus::ObjectPath{session_handle}, options)
|
|
|
|
|
.storeResultsTo(response_path);
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
std::cout << "SelectSources returned request path: " << response_path << std::endl;
|
|
|
|
|
|
|
|
|
|
// Set up Response signal handler with actual path
|
|
|
|
|
setupResponseSignal(response_path,
|
|
|
|
|
[this](uint32_t response, const std::map<std::string, sdbus::Variant>& results) {
|
|
|
|
|
onSelectSourcesResponse(response, results);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
std::cout << "SelectSources request sent (user will see hyprland-share-picker)..." << std::endl;
|
2025-12-07 12:30:16 -07:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (const sdbus::Error& e) {
|
|
|
|
|
std::cerr << "Failed to select portal sources: " << e.what() << std::endl;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ScreenCapture::startPortalSession(const std::string& session_handle, uint32_t& node_id) {
|
|
|
|
|
try {
|
|
|
|
|
// Generate unique request handle
|
|
|
|
|
std::random_device rd;
|
|
|
|
|
std::mt19937 gen(rd());
|
|
|
|
|
std::uniform_int_distribution<> dis(0, 999999);
|
|
|
|
|
|
|
|
|
|
std::stringstream ss;
|
|
|
|
|
ss << "/org/freedesktop/portal/desktop/request/"
|
|
|
|
|
<< getpid() << "_" << dis(gen);
|
|
|
|
|
std::string request_handle = ss.str();
|
|
|
|
|
|
|
|
|
|
std::cout << "Starting portal session..." << std::endl;
|
|
|
|
|
|
|
|
|
|
// Build options dictionary
|
|
|
|
|
std::map<std::string, sdbus::Variant> options;
|
|
|
|
|
options["handle_token"] = sdbus::Variant{request_handle.substr(request_handle.rfind('/') + 1)};
|
|
|
|
|
|
|
|
|
|
// Call Start
|
|
|
|
|
sdbus::ObjectPath response_path;
|
|
|
|
|
screencast_proxy_->callMethod("Start")
|
|
|
|
|
.onInterface("org.freedesktop.portal.ScreenCast")
|
|
|
|
|
.withArguments(sdbus::ObjectPath{session_handle}, std::string(""), options)
|
|
|
|
|
.storeResultsTo(response_path);
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
std::cout << "Start returned request path: " << response_path << std::endl;
|
2025-12-07 12:30:16 -07:00
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
// Set up Response signal handler with actual path
|
|
|
|
|
setupResponseSignal(response_path,
|
|
|
|
|
[this](uint32_t response, const std::map<std::string, sdbus::Variant>& results) {
|
|
|
|
|
onStartSessionResponse(response, results);
|
|
|
|
|
});
|
2025-12-07 12:30:16 -07:00
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
std::cout << "Start request sent, waiting for response..." << std::endl;
|
|
|
|
|
node_id = 0; // Will be set in response handler
|
2025-12-07 12:30:16 -07:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (const sdbus::Error& e) {
|
|
|
|
|
std::cerr << "Failed to start portal session: " << e.what() << std::endl;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int ScreenCapture::openPipeWireRemote(const std::string& session_handle) {
|
|
|
|
|
try {
|
|
|
|
|
std::cout << "Opening PipeWire remote..." << std::endl;
|
|
|
|
|
|
|
|
|
|
// Build options dictionary (empty for now)
|
|
|
|
|
std::map<std::string, sdbus::Variant> options;
|
|
|
|
|
|
|
|
|
|
// Call OpenPipeWireRemote
|
|
|
|
|
sdbus::UnixFd pipewire_fd;
|
|
|
|
|
screencast_proxy_->callMethod("OpenPipeWireRemote")
|
|
|
|
|
.onInterface("org.freedesktop.portal.ScreenCast")
|
|
|
|
|
.withArguments(sdbus::ObjectPath{session_handle}, options)
|
|
|
|
|
.storeResultsTo(pipewire_fd);
|
|
|
|
|
|
|
|
|
|
// Extract the actual file descriptor from UnixFd
|
|
|
|
|
int fd = dup(pipewire_fd.get());
|
|
|
|
|
|
|
|
|
|
if (fd < 0) {
|
|
|
|
|
std::cerr << "Failed to duplicate PipeWire file descriptor" << std::endl;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::cout << "PipeWire remote opened, fd: " << fd << std::endl;
|
|
|
|
|
return fd;
|
|
|
|
|
}
|
|
|
|
|
catch (const sdbus::Error& e) {
|
|
|
|
|
std::cerr << "Failed to open PipeWire remote: " << e.what() << std::endl;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t ScreenCapture::getStreamsNodeId(const std::string& session_handle) {
|
|
|
|
|
try {
|
|
|
|
|
std::cout << "Getting streams from session..." << std::endl;
|
|
|
|
|
|
|
|
|
|
// Create a proxy for the session object
|
|
|
|
|
auto session_proxy = sdbus::createProxy(
|
|
|
|
|
*portal_connection_,
|
|
|
|
|
sdbus::ServiceName{"org.freedesktop.portal.Desktop"},
|
|
|
|
|
sdbus::ObjectPath{session_handle}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Get the Streams property from org.freedesktop.portal.Session interface
|
|
|
|
|
auto streams_variant = session_proxy->getProperty("Streams")
|
|
|
|
|
.onInterface("org.freedesktop.portal.Session");
|
|
|
|
|
|
|
|
|
|
// Streams is a(ua{sv}) - array of (node_id, properties)
|
|
|
|
|
auto streams = streams_variant.get<std::vector<sdbus::Struct<uint32_t, std::map<std::string, sdbus::Variant>>>>();
|
|
|
|
|
|
|
|
|
|
if (streams.empty()) {
|
|
|
|
|
std::cerr << "No streams available in session" << std::endl;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the first stream's node_id
|
|
|
|
|
uint32_t node_id = std::get<0>(streams[0]);
|
|
|
|
|
auto& properties = std::get<1>(streams[0]);
|
|
|
|
|
|
|
|
|
|
std::cout << "Found stream node_id: " << node_id << std::endl;
|
|
|
|
|
|
|
|
|
|
// Print stream properties if available
|
|
|
|
|
if (properties.count("size")) {
|
|
|
|
|
auto size = properties["size"].get<sdbus::Struct<int32_t, int32_t>>();
|
|
|
|
|
std::cout << " Stream size: " << std::get<0>(size) << "x" << std::get<1>(size) << std::endl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return node_id;
|
|
|
|
|
}
|
|
|
|
|
catch (const sdbus::Error& e) {
|
|
|
|
|
std::cerr << "Failed to get streams: " << e.what() << std::endl;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
void ScreenCapture::setupResponseSignal(const std::string& request_handle,
|
|
|
|
|
std::function<void(uint32_t, const std::map<std::string, sdbus::Variant>&)> callback) {
|
|
|
|
|
try {
|
|
|
|
|
// Create a proxy for the Request object to receive the Response signal
|
|
|
|
|
auto request_proxy = sdbus::createProxy(
|
|
|
|
|
*portal_connection_,
|
|
|
|
|
sdbus::ServiceName{"org.freedesktop.portal.Desktop"},
|
|
|
|
|
sdbus::ObjectPath{request_handle}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Register signal handler for Response
|
|
|
|
|
// Capture callback by value to extend its lifetime
|
|
|
|
|
request_proxy->uponSignal("Response")
|
|
|
|
|
.onInterface("org.freedesktop.portal.Request")
|
|
|
|
|
.call([this, callback](uint32_t response, std::map<std::string, sdbus::Variant> results) {
|
|
|
|
|
std::cout << "Response signal received!" << std::endl;
|
|
|
|
|
callback(response, results);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Store the proxy to keep signal handler alive
|
|
|
|
|
request_proxies_.push_back(std::move(request_proxy));
|
|
|
|
|
|
|
|
|
|
std::cout << "Signal handler registered for: " << request_handle << std::endl;
|
|
|
|
|
}
|
|
|
|
|
catch (const sdbus::Error& e) {
|
|
|
|
|
std::cerr << "Failed to setup response signal: " << e.what() << std::endl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ScreenCapture::onCreateSessionResponse(uint32_t response, const std::map<std::string, sdbus::Variant>& results) {
|
|
|
|
|
std::cout << "CreateSession Response received: " << response << std::endl;
|
|
|
|
|
|
|
|
|
|
if (response != 0) {
|
|
|
|
|
std::cerr << "CreateSession failed or was cancelled by user" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract the actual session handle from the response
|
|
|
|
|
std::cout << "Response contains " << results.size() << " fields:" << std::endl;
|
|
|
|
|
for (const auto& [key, value] : results) {
|
|
|
|
|
std::cout << " Field: " << key << ", ContainsValueOfType: " << value.peekValueType() << std::endl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The session handle should be in the results
|
|
|
|
|
try {
|
|
|
|
|
if (results.find("session_handle") != results.end()) {
|
|
|
|
|
const auto& variant = results.at("session_handle");
|
|
|
|
|
std::cout << "Attempting to extract session_handle..." << std::endl;
|
|
|
|
|
std::cout << " Variant type signature: " << variant.peekValueType() << std::endl;
|
|
|
|
|
|
|
|
|
|
// Try as ObjectPath
|
|
|
|
|
try {
|
|
|
|
|
auto obj_path = variant.get<sdbus::ObjectPath>();
|
|
|
|
|
session_path_ = obj_path;
|
|
|
|
|
std::cout << " Extracted as ObjectPath: " << session_path_ << std::endl;
|
|
|
|
|
} catch (...) {
|
|
|
|
|
// Try as string
|
|
|
|
|
std::cout << " ObjectPath extraction failed, trying as string..." << std::endl;
|
|
|
|
|
auto str_path = variant.get<std::string>();
|
|
|
|
|
session_path_ = str_path;
|
|
|
|
|
std::cout << " Extracted as string: " << session_path_ << std::endl;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
std::cerr << "No session_handle in response!" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
std::cerr << "Failed to extract session_handle: " << e.what() << std::endl;
|
|
|
|
|
std::cerr << "Exception type: " << typeid(e).name() << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
portal_session_ready_ = true;
|
|
|
|
|
std::cout << "Session ready, proceeding to SelectSources with path: " << session_path_ << std::endl;
|
|
|
|
|
|
|
|
|
|
// Now we can call SelectSources
|
|
|
|
|
if (!selectPortalSources(session_path_)) {
|
|
|
|
|
std::cerr << "Failed to call SelectSources" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ScreenCapture::onSelectSourcesResponse(uint32_t response, const std::map<std::string, sdbus::Variant>& results) {
|
|
|
|
|
std::cout << "SelectSources Response received: " << response << std::endl;
|
|
|
|
|
|
|
|
|
|
if (response != 0) {
|
|
|
|
|
std::cerr << "SelectSources failed or was cancelled by user" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
portal_sources_selected_ = true;
|
|
|
|
|
std::cout << "Sources selected, proceeding to Start..." << std::endl;
|
|
|
|
|
|
|
|
|
|
// Now we can call Start
|
|
|
|
|
uint32_t node_id = 0;
|
|
|
|
|
if (!startPortalSession(session_path_, node_id)) {
|
|
|
|
|
std::cerr << "Failed to call Start" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ScreenCapture::onStartSessionResponse(uint32_t response, const std::map<std::string, sdbus::Variant>& results) {
|
|
|
|
|
std::cout << "Start Response received: " << response << std::endl;
|
|
|
|
|
|
|
|
|
|
if (response != 0) {
|
|
|
|
|
std::cerr << "Start failed or was cancelled by user" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
portal_started_ = true;
|
|
|
|
|
std::cout << "Session started, extracting streams from response..." << std::endl;
|
|
|
|
|
|
|
|
|
|
// List all fields in response
|
|
|
|
|
std::cout << "Start Response contains " << results.size() << " fields:" << std::endl;
|
|
|
|
|
for (const auto& [key, value] : results) {
|
|
|
|
|
std::cout << " Field: " << key << ", Type: " << value.peekValueType() << std::endl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t node_id = 0;
|
|
|
|
|
try {
|
|
|
|
|
// Extract streams from the response - format is a(ua{sv})
|
|
|
|
|
if (results.find("streams") != results.end()) {
|
|
|
|
|
const auto& streams_variant = results.at("streams");
|
|
|
|
|
auto streams = streams_variant.get<std::vector<sdbus::Struct<uint32_t, std::map<std::string, sdbus::Variant>>>>();
|
|
|
|
|
|
|
|
|
|
if (streams.empty()) {
|
|
|
|
|
std::cerr << "No streams in Start response" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the first stream's node_id
|
|
|
|
|
node_id = std::get<0>(streams[0]);
|
|
|
|
|
std::cout << "Extracted PipeWire node ID from response: " << node_id << std::endl;
|
|
|
|
|
} else {
|
|
|
|
|
std::cerr << "No 'streams' field in Start response" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
std::cerr << "Failed to extract streams: " << e.what() << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (node_id == 0) {
|
|
|
|
|
std::cerr << "Invalid node_id" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::cout << "Got Pipewire node ID: " << node_id << std::endl;
|
|
|
|
|
|
|
|
|
|
// Open PipeWire remote
|
|
|
|
|
int pw_fd = openPipeWireRemote(session_path_);
|
|
|
|
|
if (pw_fd < 0) {
|
|
|
|
|
std::cerr << "Failed to open PipeWire remote" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize Pipewire and connect to stream
|
|
|
|
|
if (!initPipewire(pw_fd, node_id)) {
|
|
|
|
|
std::cerr << "Failed to initialize Pipewire" << std::endl;
|
|
|
|
|
cleanupPortalConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
capturing_ = true;
|
|
|
|
|
std::cout << "Screen capture started successfully!" << std::endl;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
bool ScreenCapture::initPipewire(int fd, uint32_t node_id) {
|
|
|
|
|
pw_init(nullptr, nullptr);
|
2025-12-07 12:00:44 -07:00
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
std::cout << "Initializing PipeWire with fd: " << fd << ", node_id: " << node_id << std::endl;
|
2025-12-07 12:00:44 -07:00
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
pw_loop_ = pw_thread_loop_new("screen-capture", nullptr);
|
|
|
|
|
if (!pw_loop_) {
|
|
|
|
|
std::cerr << "Failed to create Pipewire loop" << std::endl;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-07 12:00:44 -07:00
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
pw_context_ = pw_context_new(
|
|
|
|
|
pw_thread_loop_get_loop(pw_loop_),
|
|
|
|
|
nullptr, 0
|
|
|
|
|
);
|
|
|
|
|
if (!pw_context_) {
|
|
|
|
|
std::cerr << "Failed to create Pipewire context" << std::endl;
|
|
|
|
|
pw_thread_loop_destroy(pw_loop_);
|
|
|
|
|
pw_loop_ = nullptr;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Connect to the PipeWire remote using the file descriptor
|
|
|
|
|
pw_core* core = pw_context_connect_fd(
|
|
|
|
|
pw_context_,
|
|
|
|
|
fd,
|
|
|
|
|
nullptr, 0
|
|
|
|
|
);
|
|
|
|
|
if (!core) {
|
|
|
|
|
std::cerr << "Failed to connect to PipeWire remote" << std::endl;
|
|
|
|
|
cleanupPipewire();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create stream properties
|
|
|
|
|
pw_properties* props = pw_properties_new(
|
|
|
|
|
PW_KEY_MEDIA_TYPE, "Video",
|
|
|
|
|
PW_KEY_MEDIA_CATEGORY, "Capture",
|
|
|
|
|
PW_KEY_MEDIA_ROLE, "Screen",
|
|
|
|
|
nullptr
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Create the stream
|
|
|
|
|
pw_stream_ = pw_stream_new(
|
|
|
|
|
core,
|
|
|
|
|
"screen-capture-stream",
|
|
|
|
|
props
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!pw_stream_) {
|
|
|
|
|
std::cerr << "Failed to create PipeWire stream" << std::endl;
|
|
|
|
|
pw_core_disconnect(core);
|
|
|
|
|
cleanupPipewire();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set up stream events
|
|
|
|
|
static const pw_stream_events stream_events = {
|
|
|
|
|
PW_VERSION_STREAM_EVENTS,
|
|
|
|
|
nullptr, // destroy
|
|
|
|
|
nullptr, // state_changed
|
|
|
|
|
nullptr, // control_info
|
|
|
|
|
nullptr, // io_changed
|
|
|
|
|
onStreamParamChanged, // param_changed
|
|
|
|
|
nullptr, // add_buffer
|
|
|
|
|
nullptr, // remove_buffer
|
|
|
|
|
onStreamProcess, // process
|
|
|
|
|
nullptr, // drained
|
|
|
|
|
nullptr, // command
|
|
|
|
|
nullptr, // trigger_done
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
pw_stream_add_listener(
|
|
|
|
|
pw_stream_,
|
|
|
|
|
&stream_listener_,
|
|
|
|
|
&stream_events,
|
|
|
|
|
this // userdata pointer
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
// Lock before connecting
|
|
|
|
|
pw_thread_loop_lock(pw_loop_);
|
|
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
// Connect to the node
|
|
|
|
|
uint8_t buffer[1024];
|
|
|
|
|
spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
|
|
|
|
|
|
|
|
|
// Request any video format (we'll adapt to what we get)
|
|
|
|
|
const struct spa_pod* params[1];
|
|
|
|
|
params[0] = reinterpret_cast<const struct spa_pod*>(spa_pod_builder_add_object(
|
|
|
|
|
&pod_builder,
|
|
|
|
|
SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
|
|
|
|
|
SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
|
|
|
|
|
SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw)
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
int result = pw_stream_connect(
|
|
|
|
|
pw_stream_,
|
|
|
|
|
PW_DIRECTION_INPUT,
|
|
|
|
|
node_id,
|
2025-12-07 14:03:08 -07:00
|
|
|
PW_STREAM_FLAG_AUTOCONNECT, // Removed MAP_BUFFERS flag
|
2025-12-07 12:30:16 -07:00
|
|
|
params, 1
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result < 0) {
|
|
|
|
|
std::cerr << "Failed to connect stream to node " << node_id << ": " << strerror(-result) << std::endl;
|
2025-12-07 14:03:08 -07:00
|
|
|
pw_thread_loop_unlock(pw_loop_);
|
2025-12-07 12:30:16 -07:00
|
|
|
pw_core_disconnect(core);
|
|
|
|
|
cleanupPipewire();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
// Unlock after connecting
|
|
|
|
|
pw_thread_loop_unlock(pw_loop_);
|
|
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
// Start the thread loop
|
|
|
|
|
if (pw_thread_loop_start(pw_loop_) < 0) {
|
|
|
|
|
std::cerr << "Failed to start PipeWire thread loop" << std::endl;
|
|
|
|
|
pw_core_disconnect(core);
|
|
|
|
|
cleanupPipewire();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::cout << "PipeWire stream connected and running" << std::endl;
|
2025-12-07 12:00:44 -07:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
void ScreenCapture::cleanupPipewire() {
|
|
|
|
|
if (pw_loop_) {
|
|
|
|
|
pw_thread_loop_stop(pw_loop_);
|
|
|
|
|
}
|
2025-12-07 12:00:44 -07:00
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
if (pw_stream_) {
|
|
|
|
|
pw_stream_destroy(pw_stream_);
|
|
|
|
|
pw_stream_ = nullptr;
|
|
|
|
|
}
|
2025-12-07 12:00:44 -07:00
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
if (pw_context_) {
|
|
|
|
|
pw_context_destroy(pw_context_);
|
|
|
|
|
pw_context_ = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pw_loop_) {
|
|
|
|
|
pw_thread_loop_destroy(pw_loop_);
|
|
|
|
|
pw_loop_ = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pw_deinit();
|
2025-12-07 12:00:44 -07:00
|
|
|
}
|
|
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
void ScreenCapture::onStreamProcess(void* userdata) {
|
2025-12-07 14:03:08 -07:00
|
|
|
std::cout << "onStreamProcess called" << std::endl;
|
2025-12-07 12:30:16 -07:00
|
|
|
auto* self = static_cast<ScreenCapture*>(userdata);
|
2025-12-07 14:03:08 -07:00
|
|
|
std::cout << " userdata cast successful" << std::endl;
|
2025-12-07 12:30:16 -07:00
|
|
|
|
2025-12-07 14:03:08 -07:00
|
|
|
try {
|
|
|
|
|
pw_buffer* buffer = pw_stream_dequeue_buffer(self->pw_stream_);
|
|
|
|
|
std::cout << " dequeue_buffer returned: " << (void*)buffer << std::endl;
|
|
|
|
|
if (!buffer) {
|
|
|
|
|
std::cerr << "No buffer available" << std::endl;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spa_buffer* spa_buf = buffer->buffer;
|
|
|
|
|
std::cout << " spa_buf: " << (void*)spa_buf << std::endl;
|
|
|
|
|
if (!spa_buf || !spa_buf->datas) {
|
|
|
|
|
std::cout << " Invalid spa_buf structure, requeueing" << std::endl;
|
|
|
|
|
pw_stream_queue_buffer(self->pw_stream_, buffer);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::cout << " spa_buf valid, getting frame data" << std::endl;
|
|
|
|
|
|
|
|
|
|
// Get frame data
|
|
|
|
|
spa_data& data = spa_buf->datas[0];
|
|
|
|
|
|
|
|
|
|
std::cout << " Buffer type: " << data.type << " (MemFd=" << SPA_DATA_MemFd
|
|
|
|
|
<< ", MemPtr=" << SPA_DATA_MemPtr << ", DmaBuf=" << SPA_DATA_DmaBuf << ")" << std::endl;
|
|
|
|
|
std::cout << " data.data: " << data.data << ", data.fd: " << data.fd
|
|
|
|
|
<< ", data.maxsize: " << data.maxsize << std::endl;
|
|
|
|
|
|
|
|
|
|
// ALWAYS use mmap for DMA-BUF and MemFd types (ignore data.data pointer)
|
|
|
|
|
void* frame_data = nullptr;
|
|
|
|
|
bool needs_unmap = false;
|
|
|
|
|
|
|
|
|
|
if (data.type == SPA_DATA_MemFd || data.type == SPA_DATA_DmaBuf) {
|
|
|
|
|
// Map the file descriptor
|
|
|
|
|
std::cout << " Mapping buffer from fd..." << std::endl;
|
|
|
|
|
frame_data = mmap(NULL, data.maxsize, PROT_READ, MAP_PRIVATE, data.fd, data.mapoffset);
|
|
|
|
|
if (frame_data == MAP_FAILED) {
|
|
|
|
|
std::cerr << " Failed to mmap buffer, errno: " << errno << std::endl;
|
|
|
|
|
pw_stream_queue_buffer(self->pw_stream_, buffer);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
needs_unmap = true;
|
|
|
|
|
std::cout << " Successfully mapped buffer at " << frame_data << std::endl;
|
|
|
|
|
} else if (data.type == SPA_DATA_MemPtr) {
|
|
|
|
|
// Use the pointer directly
|
|
|
|
|
frame_data = data.data;
|
|
|
|
|
std::cout << " Using direct pointer" << std::endl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!frame_data) {
|
|
|
|
|
std::cout << " No frame data available, requeueing" << std::endl;
|
|
|
|
|
pw_stream_queue_buffer(self->pw_stream_, buffer);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate chunk
|
|
|
|
|
if (!data.chunk) {
|
|
|
|
|
std::cerr << "No chunk in buffer" << std::endl;
|
|
|
|
|
if (needs_unmap) {
|
|
|
|
|
munmap(frame_data, data.maxsize);
|
|
|
|
|
}
|
|
|
|
|
pw_stream_queue_buffer(self->pw_stream_, buffer);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply chunk offset to get actual frame data
|
|
|
|
|
uint8_t* frame_ptr = static_cast<uint8_t*>(frame_data) + data.chunk->offset;
|
|
|
|
|
uint32_t size = data.chunk->size;
|
|
|
|
|
|
|
|
|
|
std::cout << " chunk->offset: " << data.chunk->offset << ", chunk->size: " << size << std::endl;
|
|
|
|
|
|
|
|
|
|
// Validate that chunk doesn't exceed mapped buffer
|
|
|
|
|
if (data.chunk->offset + size > data.maxsize) {
|
|
|
|
|
std::cerr << "Chunk exceeds buffer bounds: offset=" << data.chunk->offset
|
|
|
|
|
<< " size=" << size << " maxsize=" << data.maxsize << std::endl;
|
|
|
|
|
if (needs_unmap) {
|
|
|
|
|
munmap(frame_data, data.maxsize);
|
|
|
|
|
}
|
|
|
|
|
pw_stream_queue_buffer(self->pw_stream_, buffer);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate data pointer and size
|
|
|
|
|
if (!frame_ptr || size == 0 || size > 100 * 1024 * 1024) { // Max 100MB sanity check
|
|
|
|
|
std::cerr << "Invalid frame data: ptr=" << (void*)frame_ptr << " size=" << size << std::endl;
|
|
|
|
|
if (needs_unmap) {
|
|
|
|
|
munmap(frame_data, data.maxsize);
|
|
|
|
|
}
|
|
|
|
|
pw_stream_queue_buffer(self->pw_stream_, buffer);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int frame_count = 0;
|
|
|
|
|
if (++frame_count % 30 == 0) {
|
|
|
|
|
std::cout << "Frame received: " << frame_count << " (size: " << size << " bytes)" << std::endl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Encode and send frame if encoder is available
|
|
|
|
|
if (self->encoder_ && self->frameCallback_) {
|
|
|
|
|
try {
|
|
|
|
|
// Encode the raw frame to H.264
|
|
|
|
|
std::vector<uint8_t> encoded_frame = self->encoder_->encode(frame_ptr, size);
|
|
|
|
|
|
|
|
|
|
if (!encoded_frame.empty()) {
|
|
|
|
|
// Send encoded frame via callback
|
|
|
|
|
self->frameCallback_(encoded_frame, self->frame_width_, self->frame_height_);
|
|
|
|
|
|
|
|
|
|
if (frame_count % 30 == 0) {
|
|
|
|
|
std::cout << " Encoded frame size: " << encoded_frame.size() << " bytes (compression: "
|
|
|
|
|
<< (100.0f * encoded_frame.size() / size) << "%)" << std::endl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
std::cerr << "Exception encoding/sending frame: " << e.what() << std::endl;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (frame_count < 5) {
|
|
|
|
|
if (!self->encoder_) {
|
|
|
|
|
std::cout << " Encoder not initialized yet" << std::endl;
|
|
|
|
|
} else if (!self->frameCallback_) {
|
|
|
|
|
std::cout << " Frame callback not set" << std::endl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clean up mapped memory if we mapped it ourselves
|
|
|
|
|
if (needs_unmap) {
|
|
|
|
|
munmap(frame_data, data.maxsize);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return buffer to stream
|
2025-12-07 12:30:16 -07:00
|
|
|
pw_stream_queue_buffer(self->pw_stream_, buffer);
|
2025-12-07 14:03:08 -07:00
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
std::cerr << "Exception in onStreamProcess: " << e.what() << std::endl;
|
|
|
|
|
} catch (...) {
|
|
|
|
|
std::cerr << "Unknown exception in onStreamProcess" << std::endl;
|
2025-12-07 12:30:16 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ScreenCapture::onStreamParamChanged(void* userdata, uint32_t id, const struct spa_pod* param) {
|
|
|
|
|
auto* self = static_cast<ScreenCapture*>(userdata);
|
|
|
|
|
|
|
|
|
|
if (!param || id != SPA_PARAM_Format) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse video format
|
|
|
|
|
spa_video_info_raw video_info;
|
|
|
|
|
if (spa_format_video_raw_parse(param, &video_info) < 0) {
|
|
|
|
|
std::cerr << "Failed to parse video format" << std::endl;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update our stored dimensions
|
|
|
|
|
self->frame_width_ = video_info.size.width;
|
|
|
|
|
self->frame_height_ = video_info.size.height;
|
|
|
|
|
|
|
|
|
|
std::cout << "Stream format changed:" << std::endl;
|
|
|
|
|
std::cout << " Resolution: " << self->frame_width_ << "x" << self->frame_height_ << std::endl;
|
|
|
|
|
std::cout << " Format: " << video_info.format << std::endl;
|
|
|
|
|
std::cout << " Framerate: " << video_info.framerate.num << "/" << video_info.framerate.denom << std::endl;
|
2025-12-07 14:03:08 -07:00
|
|
|
|
|
|
|
|
// Initialize video encoder now that we know the resolution
|
|
|
|
|
int fps = (video_info.framerate.denom > 0) ? (video_info.framerate.num / video_info.framerate.denom) : 30;
|
|
|
|
|
if (fps <= 0) fps = 30;
|
|
|
|
|
|
|
|
|
|
self->encoder_ = std::make_unique<VideoEncoder>();
|
|
|
|
|
if (!self->encoder_->initialize(self->frame_width_, self->frame_height_, fps)) {
|
|
|
|
|
std::cerr << "Failed to initialize video encoder" << std::endl;
|
|
|
|
|
self->encoder_.reset();
|
|
|
|
|
}
|
2025-12-07 12:00:44 -07:00
|
|
|
}
|
|
|
|
|
|
2025-12-07 12:30:16 -07:00
|
|
|
#else
|
|
|
|
|
// Stub implementations for non-Linux platforms
|
|
|
|
|
bool ScreenCapture::initPortalConnection() { return false; }
|
|
|
|
|
void ScreenCapture::cleanupPortalConnection() {}
|
|
|
|
|
std::string ScreenCapture::createPortalSession() { return ""; }
|
|
|
|
|
bool ScreenCapture::selectPortalSources(const std::string&) { return false; }
|
|
|
|
|
bool ScreenCapture::startPortalSession(const std::string&, uint32_t&) { return false; }
|
|
|
|
|
int ScreenCapture::openPipeWireRemote(const std::string&) { return -1; }
|
|
|
|
|
uint32_t ScreenCapture::getStreamsNodeId(const std::string&) { return 0; }
|
2025-12-07 14:03:08 -07:00
|
|
|
void ScreenCapture::setupResponseSignal(const std::string&, std::function<void(uint32_t, const std::map<std::string, sdbus::Variant>&)>) {}
|
|
|
|
|
void ScreenCapture::onCreateSessionResponse(uint32_t, const std::map<std::string, sdbus::Variant>&) {}
|
|
|
|
|
void ScreenCapture::onSelectSourcesResponse(uint32_t, const std::map<std::string, sdbus::Variant>&) {}
|
|
|
|
|
void ScreenCapture::onStartSessionResponse(uint32_t, const std::map<std::string, sdbus::Variant>&) {}
|
2025-12-07 12:30:16 -07:00
|
|
|
bool ScreenCapture::initPipewire(int, uint32_t) { return false; }
|
|
|
|
|
void ScreenCapture::cleanupPipewire() {}
|
|
|
|
|
void ScreenCapture::onStreamProcess(void*) {}
|
|
|
|
|
void ScreenCapture::onStreamParamChanged(void*, uint32_t, const struct spa_pod*) {}
|
|
|
|
|
#endif
|
|
|
|
|
|
2025-12-07 12:00:44 -07:00
|
|
|
} // namespace scar
|