Compare commits

...
4 Commits
36 changed files with 890 additions and 175 deletions
@@ -1,5 +1,11 @@
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _APPLICATION_STATE_H_
#define _APPLICATION_STATE_H_
#include <atomic>
#include <chrono>
@@ -192,4 +198,4 @@ struct ApplicationState : MainWindowState,
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
#endif
@@ -1,3 +1,9 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _GUI_APPLICATION_H_
#define _GUI_APPLICATION_H_
@@ -94,4 +100,4 @@ private:
} // namespace crossdesk
#endif
#endif
@@ -1,4 +1,11 @@
#pragma once
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SERVER_WINDOW_STATE_H_
#define _SERVER_WINDOW_STATE_H_
#include <algorithm>
#include <iterator>
@@ -24,5 +31,6 @@ inline int ReconcileSelectedController(const std::vector<std::string>& ids,
}
return static_cast<int>(std::distance(ids.begin(), selected));
}
} // namespace crossdesk::server_window_state
} // namespace crossdesk::server_window_state
#endif
@@ -1,4 +1,11 @@
#pragma once
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOW_GEOMETRY_H_
#define _WINDOW_GEOMETRY_H_
#include <algorithm>
@@ -38,3 +45,5 @@ constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
}
} // namespace crossdesk::window_geometry
#endif
@@ -74,7 +74,8 @@ int SessionDeviceManager::InitializeScreenCapturer() {
const int init_ret = screen_capturer_->Init(
fps, [this, fps](unsigned char *data, int size, int width, int height,
const char *display_name) {
const char *display_name,
const XNativeVideoFrame *native_frame) {
const auto now_time = std::chrono::steady_clock::now();
if (!ShouldSendCapturedFrame(now_time, fps)) {
return;
@@ -142,6 +143,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
frame.width = width;
frame.height = height;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
frame.native_frame = native_frame;
for (const std::string &remote_id : connected_remote_ids) {
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
remote_id.data(), remote_id.size());
+9 -3
View File
@@ -1,5 +1,11 @@
#ifndef CROSSDESK_GUI_RENDER_H_
#define CROSSDESK_GUI_RENDER_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _RENDER_H_
#define _RENDER_H_
#include <memory>
@@ -25,4 +31,4 @@ private:
} // namespace crossdesk
#endif // CROSSDESK_GUI_RENDER_H_
#endif
@@ -1,5 +1,11 @@
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SLINT_VIDEO_PRESENTER_H_
#define _SLINT_VIDEO_PRESENTER_H_
#include <cstddef>
#include <cstdint>
@@ -72,4 +78,4 @@ private:
} // namespace crossdesk
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
#endif
@@ -1,3 +1,9 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_POSITION_H_
#define _CURSOR_POSITION_H_
@@ -64,4 +70,4 @@ inline bool NormalizeCursorPosition(
} // namespace crossdesk
#endif
#endif
@@ -1,3 +1,9 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_STATE_PROVIDER_H_
#define _CURSOR_STATE_PROVIDER_H_
@@ -30,4 +36,4 @@ class CursorStateProvider {
} // namespace crossdesk
#endif
#endif
@@ -1,11 +1,11 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-28
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#ifndef _DEVICE_PRESENCE_CACHE_H_
#define _DEVICE_PRESENCE_CACHE_H_
#include <mutex>
#include <string>
@@ -38,4 +38,4 @@ private:
} // namespace crossdesk
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#endif
+4 -3
View File
@@ -141,9 +141,10 @@ int GuiRuntime::CreateConnectionPeer() {
sizeof(params_.log_path) - 1);
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
#if defined(_WIN32)
// Windows renderers retain pooled CPU NV12 frames or CUDA device frames and
// fall back to a packed CPU copy when native upload is unavailable.
#if defined(_WIN32) || defined(__APPLE__)
// Windows renderers retain pooled CPU NV12 frames or CUDA device frames;
// macOS retains VideoToolbox CVPixelBuffers for direct Metal sampling. Both
// platforms fall back to a packed CPU copy when native upload is unavailable.
params_.native_video_output = true;
#else
params_.native_video_output = false;
+9 -3
View File
@@ -1,5 +1,11 @@
#ifndef CROSSDESK_GUI_RUNTIME_H_
#define CROSSDESK_GUI_RUNTIME_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _GUI_RUNTIME_H_
#define _GUI_RUNTIME_H_
#include <atomic>
#include <memory>
@@ -98,4 +104,4 @@ class GuiRuntime : protected gui_detail::GuiState {
} // namespace crossdesk
#endif // CROSSDESK_GUI_RUNTIME_H_
#endif
+6 -8
View File
@@ -1,13 +1,11 @@
/*
* Aggregate state visible to GuiRuntime and GuiApplication.
*
* Definitions live beside their owners: SDL/window state in application and
* connection/session state in runtime. This header is intentionally only the
* composition point.
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_GUI_STATE_H_
#define CROSSDESK_GUI_STATE_H_
#ifndef _GUI_STATE_H_
#define _GUI_STATE_H_
#include "application/application_state.h"
#include "runtime/runtime_state.h"
@@ -18,4 +16,4 @@ struct GuiState : ApplicationState, RuntimeState {};
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_STATE_H_
#endif
@@ -1,5 +1,11 @@
#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PEER_EVENT_HANDLER_H_
#define _PEER_EVENT_HANDLER_H_
#include <cstddef>
#include <string>
@@ -47,4 +53,4 @@ private:
} // namespace crossdesk
#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#endif
@@ -1,5 +1,11 @@
#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _REMOTE_ACTION_CODEC_H_
#define _REMOTE_ACTION_CODEC_H_
#include <cstddef>
#include <vector>
@@ -14,4 +20,4 @@ void Free(RemoteAction &action);
} // namespace crossdesk::remote_action_codec
#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#endif
@@ -1,3 +1,9 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _REMOTE_SESSION_H_
#define _REMOTE_SESSION_H_
@@ -174,4 +180,4 @@ using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
} // namespace crossdesk::gui_detail
#endif
#endif
+7 -1
View File
@@ -1,3 +1,9 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _RUNTIME_STATE_H_
#define _RUNTIME_STATE_H_
@@ -212,4 +218,4 @@ struct RuntimeState : InfrastructureState,
} // namespace crossdesk::gui_detail
#endif
#endif
+10 -3
View File
@@ -1,5 +1,11 @@
#ifndef CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
#define CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _UI_LOCALIZATION_H_
#define _UI_LOCALIZATION_H_
#include <slint.h>
@@ -162,4 +168,5 @@ inline int ApplyStreamWindowStrings(
} // namespace crossdesk::ui_localization
#endif // CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
#endif
@@ -429,7 +429,7 @@ bool ScreenCapturerDrm::CaptureOutputFrame(const DrmOutput& output,
if (emit_callback && callback_) {
callback_(nv12.data(), static_cast<int>(nv12.size()), capture_width,
capture_height, output.stream_id.c_str());
capture_height, output.stream_id.c_str(), nullptr);
}
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
@@ -50,7 +50,8 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
callback_orig_ = std::move(cb);
callback_ = [this](unsigned char* data, int size, int width, int height,
const char* reported_stream_id) {
const char* reported_stream_id,
const XNativeVideoFrame* native_frame) {
const std::string mapped_stream_id = MapStreamId(reported_stream_id);
if (mapped_stream_id.empty()) {
if (!invalid_stream_id_logged_.exchange(true,
@@ -64,7 +65,8 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
}
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
if (callback_orig_) {
callback_orig_(data, size, width, height, mapped_stream_id.c_str());
callback_orig_(data, size, width, height, mapped_stream_id.c_str(),
native_frame);
}
};
@@ -1047,7 +1047,7 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
if (callback_) {
const std::string stream_id = MakeDisplayStreamId(0);
callback_(nv12.data(), static_cast<int>(nv12.size()), even_width,
even_height, stream_id.c_str());
even_height, stream_id.c_str(), nullptr);
}
pipewire_last_frame_ms_.store(NowMs());
@@ -321,7 +321,7 @@ void ScreenCapturerX11::OnFrame() {
if (callback_) {
const std::string stream_id = MakeDisplayStreamId(monitor_index);
callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_,
stream_id.c_str());
stream_id.c_str(), nullptr);
}
XDestroyImage(image);
@@ -12,11 +12,11 @@
namespace crossdesk {
// macOS video compositor used by CrossDesk only. MiniRTC continues to expose
// tightly packed CPU NV12 frames; this renderer performs the single required
// copy into shared Metal buffers and converts NV12 to RGB in the fragment
// shader. AppKit attachment and rendering are main-thread operations, while
// SubmitNv12() is safe to call from MiniRTC's decode callback thread.
// macOS video compositor used by CrossDesk only. VideoToolbox CVPixelBuffers
// are sampled directly through CVMetalTextureCache; packed CPU NV12 remains a
// fallback for software decoding and cached snapshots. AppKit attachment and
// rendering are main-thread operations, while frame submission is safe from
// MiniRTC's decode callback thread.
class MacMetalVideoRenderer final : public VideoRenderer {
public:
MacMetalVideoRenderer();
@@ -37,6 +37,11 @@ class MacMetalVideoRenderer final : public VideoRenderer {
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t* data,
size_t size, int width, int height) override;
// Retains an IOSurface-backed VideoToolbox CVPixelBuffer until the Metal
// command buffer using it has completed.
SubmitResult SubmitNativeFrame(std::string_view remote_id,
const XNativeVideoFrame& frame) override;
// Seeds a newly selected stream from its retained CPU snapshot without
// replacing a newer decoded frame that is already queued or rendering.
SubmitResult SubmitCachedNv12(std::string_view remote_id,
@@ -1,6 +1,7 @@
#include "platform/macos/gui/metal_video_renderer.h"
#import <AppKit/AppKit.h>
#import <CoreVideo/CVMetalTextureCache.h>
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
@@ -194,6 +195,74 @@ namespace {
constexpr size_t kFrameSlotCount = 3;
class NativeVideoFrameRef {
public:
NativeVideoFrameRef() = default;
explicit NativeVideoFrameRef(const XNativeVideoFrame* frame) {
if (frame && frame->owner && frame->retain && frame->release) {
frame_ = *frame;
frame_.struct_size = sizeof(frame_);
frame_.retain(frame_.owner);
valid_ = true;
}
}
NativeVideoFrameRef(const NativeVideoFrameRef& other)
: NativeVideoFrameRef(other.Get()) {}
NativeVideoFrameRef(NativeVideoFrameRef&& other) noexcept
: frame_(other.frame_), valid_(std::exchange(other.valid_, false)) {
other.frame_ = {};
}
NativeVideoFrameRef& operator=(NativeVideoFrameRef other) noexcept {
Swap(other);
return *this;
}
~NativeVideoFrameRef() { Reset(); }
void Reset() {
if (valid_ && frame_.owner && frame_.release) {
frame_.release(frame_.owner);
}
frame_ = {};
valid_ = false;
}
void Swap(NativeVideoFrameRef& other) noexcept {
std::swap(frame_, other.frame_);
std::swap(valid_, other.valid_);
}
const XNativeVideoFrame* Get() const {
return valid_ ? &frame_ : nullptr;
}
explicit operator bool() const { return valid_; }
private:
XNativeVideoFrame frame_{};
bool valid_ = false;
};
const XNativeVideoFrame* GetMacNativeFrame(
const XNativeVideoFrame& frame) {
const XNativeVideoFrame* native = &frame;
if (native->struct_size < static_cast<uint32_t>(sizeof(*native)) ||
native->type != XNativeVideoFrameCVPixelBuffer ||
!native->payload.cv_pixel_buffer || !native->owner || !native->retain ||
!native->release || !native->copy_to_nv12 || native->width == 0 ||
native->height == 0 || (native->width & 1U) != 0 ||
(native->height & 1U) != 0 ||
native->width > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
native->height >
static_cast<uint32_t>(std::numeric_limits<int>::max())) {
return nullptr;
}
return native;
}
// Slint's winit backend creates AppKit windows with transparent backing so
// transparent Slint items can be composited. Keep the content out of the
// titlebar and choose whether AppKit's material or the semantic window color
@@ -262,6 +331,9 @@ struct FrameSlot {
id<MTLBuffer> uv_buffer = nil;
id<MTLTexture> y_texture = nil;
id<MTLTexture> uv_texture = nil;
CVMetalTextureRef y_cv_texture = nullptr;
CVMetalTextureRef uv_cv_texture = nullptr;
NativeVideoFrameRef native_frame;
size_t y_stride = 0;
size_t uv_stride = 0;
int width = 0;
@@ -274,6 +346,15 @@ struct FrameSlot {
void ReleaseResources() {
y_texture = nil;
uv_texture = nil;
if (y_cv_texture) {
CFRelease(y_cv_texture);
y_cv_texture = nullptr;
}
if (uv_cv_texture) {
CFRelease(uv_cv_texture);
uv_cv_texture = nullptr;
}
native_frame.Reset();
y_buffer = nil;
uv_buffer = nil;
y_stride = 0;
@@ -406,6 +487,7 @@ struct MacMetalVideoRenderer::Impl {
id<MTLDevice> device = nil;
id<MTLCommandQueue> command_queue = nil;
id<MTLRenderPipelineState> pipeline = nil;
CVMetalTextureCacheRef texture_cache = nullptr;
std::shared_ptr<SharedFrameState> frames =
std::make_shared<SharedFrameState>();
NSView* slint_view = nil;
@@ -417,6 +499,8 @@ struct MacMetalVideoRenderer::Impl {
uint64_t rendered_content_generation = 0;
std::shared_ptr<std::atomic<bool>> native_surface_needs_redraw =
std::make_shared<std::atomic<bool>>(true);
std::atomic<bool> native_path_logged{false};
std::atomic<bool> native_fallback_logged{false};
Impl() {
@autoreleasepool {
@@ -430,6 +514,12 @@ struct MacMetalVideoRenderer::Impl {
LOG_ERROR("Metal video renderer unavailable: command queue creation failed");
return;
}
const CVReturn cache_status = CVMetalTextureCacheCreate(
kCFAllocatorDefault, nullptr, device, nullptr, &texture_cache);
if (cache_status != kCVReturnSuccess || !texture_cache) {
LOG_WARN("Metal video renderer could not create CV texture cache: {}",
cache_status);
}
NSError* library_error = nil;
id<MTLLibrary> library =
@@ -466,6 +556,11 @@ struct MacMetalVideoRenderer::Impl {
~Impl() {
DetachViews();
frames.reset();
if (texture_cache) {
CVMetalTextureCacheFlush(texture_cache, 0);
CFRelease(texture_cache);
texture_cache = nullptr;
}
}
bool Ready() const {
@@ -655,6 +750,142 @@ MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNv12(
return SubmitNv12Internal(remote_id, data, size, width, height, true);
}
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNativeFrame(
std::string_view remote_id, const XNativeVideoFrame& frame) {
if (!IsReady()) {
return SubmitResult::failed;
}
const XNativeVideoFrame* native = GetMacNativeFrame(frame);
if (!native) {
return SubmitResult::failed;
}
{
std::lock_guard lock(impl_->frames->mutex);
if (impl_->frames->selected_stream != remote_id) {
return SubmitResult::not_selected;
}
}
const int width = static_cast<int>(native->width);
const int height = static_cast<int>(native->height);
const auto submit_cpu_fallback = [&]() {
if (!impl_->native_fallback_logged.exchange(true,
std::memory_order_acq_rel)) {
LOG_WARN("macOS native CVPixelBuffer upload unavailable; using CPU NV12 "
"fallback");
}
const size_t y_size = static_cast<size_t>(width) * height;
std::vector<unsigned char> packed_nv12(y_size + y_size / 2);
if (native->copy_to_nv12(native->owner, packed_nv12.data(),
packed_nv12.size()) != 0) {
return SubmitResult::failed;
}
return SubmitNv12(remote_id, packed_nv12.data(), packed_nv12.size(), width,
height);
};
CVPixelBufferRef pixel_buffer =
static_cast<CVPixelBufferRef>(native->payload.cv_pixel_buffer);
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixel_buffer);
if (!CVPixelBufferIsPlanar(pixel_buffer) ||
CVPixelBufferGetPlaneCount(pixel_buffer) < 2 ||
CVPixelBufferGetWidth(pixel_buffer) != native->width ||
CVPixelBufferGetHeight(pixel_buffer) != native->height ||
(pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)) {
return submit_cpu_fallback();
}
NativeVideoFrameRef retained(native);
if (!retained || !impl_->texture_cache) {
return submit_cpu_fallback();
}
CVMetalTextureRef y_cv_texture = nullptr;
CVReturn y_status = CVMetalTextureCacheCreateTextureFromImage(
kCFAllocatorDefault, impl_->texture_cache, pixel_buffer, nullptr,
MTLPixelFormatR8Unorm, native->width, native->height, 0,
&y_cv_texture);
if (y_status != kCVReturnSuccess || !y_cv_texture) {
return submit_cpu_fallback();
}
CVMetalTextureRef uv_cv_texture = nullptr;
CVReturn uv_status = CVMetalTextureCacheCreateTextureFromImage(
kCFAllocatorDefault, impl_->texture_cache, pixel_buffer, nullptr,
MTLPixelFormatRG8Unorm, native->width / 2U, native->height / 2U, 1,
&uv_cv_texture);
if (uv_status != kCVReturnSuccess || !uv_cv_texture) {
CFRelease(y_cv_texture);
return submit_cpu_fallback();
}
id<MTLTexture> y_texture = CVMetalTextureGetTexture(y_cv_texture);
id<MTLTexture> uv_texture = CVMetalTextureGetTexture(uv_cv_texture);
if (y_texture == nil || uv_texture == nil) {
CFRelease(y_cv_texture);
CFRelease(uv_cv_texture);
return submit_cpu_fallback();
}
auto& frames = *impl_->frames;
std::lock_guard lock(frames.mutex);
if (frames.selected_stream != remote_id) {
CFRelease(y_cv_texture);
CFRelease(uv_cv_texture);
return SubmitResult::not_selected;
}
// Latest-frame-only queue: replace an unconsumed decoded frame to bound
// latency, while never recycling a frame still referenced by the GPU.
FrameSlot* target = nullptr;
for (auto& slot : frames.slots) {
if (slot.use == SlotUse::pending) {
target = &slot;
break;
}
}
if (!target) {
uint64_t oldest_sequence = std::numeric_limits<uint64_t>::max();
for (auto& slot : frames.slots) {
if (slot.use != SlotUse::available) {
continue;
}
if (!slot.valid) {
target = &slot;
break;
}
if (slot.sequence < oldest_sequence) {
oldest_sequence = slot.sequence;
target = &slot;
}
}
}
if (!target) {
CFRelease(y_cv_texture);
CFRelease(uv_cv_texture);
return SubmitResult::dropped;
}
target->ReleaseResources();
target->y_cv_texture = y_cv_texture;
target->uv_cv_texture = uv_cv_texture;
target->y_texture = y_texture;
target->uv_texture = uv_texture;
target->native_frame = std::move(retained);
target->width = width;
target->height = height;
target->remote_id.assign(remote_id);
target->sequence = frames.next_sequence++;
target->use = SlotUse::pending;
target->valid = true;
if (!impl_->native_path_logged.exchange(true, std::memory_order_acq_rel)) {
LOG_INFO("macOS VideoToolbox-to-Metal native NV12 path enabled");
}
return SubmitResult::submitted;
}
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitCachedNv12(
std::string_view remote_id, const uint8_t* data, size_t size, int width,
int height) {
@@ -1045,40 +1276,57 @@ bool MacMetalVideoRenderer::CopyLatestNv12(
return false;
}
std::lock_guard lock(impl_->frames->mutex);
const FrameSlot* newest = nullptr;
for (const auto& slot : impl_->frames->slots) {
if (!slot.valid || slot.remote_id != remote_id || slot.y_buffer == nil ||
slot.uv_buffer == nil || slot.sequence == 0) {
continue;
NativeVideoFrameRef native_frame;
{
std::lock_guard lock(impl_->frames->mutex);
const FrameSlot* newest = nullptr;
for (const auto& slot : impl_->frames->slots) {
if (!slot.valid || slot.remote_id != remote_id || slot.sequence == 0 ||
(!slot.native_frame &&
(slot.y_buffer == nil || slot.uv_buffer == nil))) {
continue;
}
if (newest == nullptr || slot.sequence > newest->sequence) {
newest = &slot;
}
}
if (newest == nullptr || slot.sequence > newest->sequence) {
newest = &slot;
if (newest == nullptr || newest->width <= 0 || newest->height <= 0) {
return false;
}
*width = newest->width;
*height = newest->height;
native_frame = newest->native_frame;
if (!native_frame) {
const size_t y_size =
static_cast<size_t>(newest->width) * newest->height;
output->resize(y_size + y_size / 2);
auto* y_source =
static_cast<const uint8_t*>(newest->y_buffer.contents);
auto* uv_source =
static_cast<const uint8_t*>(newest->uv_buffer.contents);
for (int row = 0; row < newest->height; ++row) {
std::memcpy(output->data() +
static_cast<size_t>(row) * newest->width,
y_source + static_cast<size_t>(row) * newest->y_stride,
newest->width);
}
uint8_t* uv_destination = output->data() + y_size;
for (int row = 0; row < newest->height / 2; ++row) {
std::memcpy(uv_destination +
static_cast<size_t>(row) * newest->width,
uv_source + static_cast<size_t>(row) * newest->uv_stride,
newest->width);
}
return true;
}
}
if (newest == nullptr || newest->width <= 0 || newest->height <= 0) {
return false;
}
const size_t y_size =
static_cast<size_t>(newest->width) * newest->height;
const XNativeVideoFrame* native = native_frame.Get();
const size_t y_size = static_cast<size_t>(native->width) * native->height;
output->resize(y_size + y_size / 2);
auto* y_source = static_cast<const uint8_t*>(newest->y_buffer.contents);
auto* uv_source = static_cast<const uint8_t*>(newest->uv_buffer.contents);
for (int row = 0; row < newest->height; ++row) {
std::memcpy(output->data() + static_cast<size_t>(row) * newest->width,
y_source + static_cast<size_t>(row) * newest->y_stride,
newest->width);
}
uint8_t* uv_destination = output->data() + y_size;
for (int row = 0; row < newest->height / 2; ++row) {
std::memcpy(uv_destination + static_cast<size_t>(row) * newest->width,
uv_source + static_cast<size_t>(row) * newest->uv_stride,
newest->width);
}
*width = newest->width;
*height = newest->height;
return true;
return native->copy_to_nv12(native->owner, output->data(), output->size()) ==
0;
}
} // namespace crossdesk
@@ -42,6 +42,65 @@ static std::string NSErrorToString(NSError *error) {
return description ? description : "";
}
static void RetainCapturedPixelBuffer(void *owner) {
if (owner) {
CVPixelBufferRetain(static_cast<CVPixelBufferRef>(owner));
}
}
static void ReleaseCapturedPixelBuffer(void *owner) {
if (owner) {
CVPixelBufferRelease(static_cast<CVPixelBufferRef>(owner));
}
}
static int CopyCapturedPixelBufferToNv12(void *owner, uint8_t *destination,
size_t destination_size) {
auto pixel_buffer = static_cast<CVPixelBufferRef>(owner);
if (!pixel_buffer || !destination ||
!CVPixelBufferIsPlanar(pixel_buffer) ||
CVPixelBufferGetPlaneCount(pixel_buffer) < 2) {
return -1;
}
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixel_buffer);
if (pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
return -1;
}
const size_t width = CVPixelBufferGetWidth(pixel_buffer);
const size_t height = CVPixelBufferGetHeight(pixel_buffer);
const size_t required_size = width * height * 3U / 2U;
if (destination_size < required_size ||
CVPixelBufferLockBaseAddress(pixel_buffer,
kCVPixelBufferLock_ReadOnly) !=
kCVReturnSuccess) {
return -1;
}
const auto *y_plane = static_cast<const uint8_t *>(
CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 0));
const auto *uv_plane = static_cast<const uint8_t *>(
CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 1));
const size_t y_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 0);
const size_t uv_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 1);
int result = 0;
if (!y_plane || !uv_plane || y_stride < width || uv_stride < width) {
result = -1;
} else {
for (size_t row = 0; row < height; ++row) {
std::memcpy(destination + row * width, y_plane + row * y_stride, width);
}
uint8_t *uv_destination = destination + width * height;
for (size_t row = 0; row < height / 2U; ++row) {
std::memcpy(uv_destination + row * width,
uv_plane + row * uv_stride, width);
}
}
CVPixelBufferUnlockBaseAddress(pixel_buffer, kCVPixelBufferLock_ReadOnly);
return result;
}
// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
// was reported to be broken before macOS 13 - see http://crbug.com/40234870.
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
@@ -92,14 +151,11 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
private:
std::vector<DisplayInfo> display_info_list_;
std::map<int, CGDirectDisplayID> display_id_map_;
unsigned char *nv12_frame_ = nullptr;
size_t nv12_frame_size_ = 0;
int width_ = 0;
int height_ = 0;
int fps_ = 60;
bool show_cursor_ = false;
bool capture_requested_ = false;
bool invalid_stream_id_logged_ = false;
bool native_output_logged_ = false;
public:
// Called by SckHelper when shareable content is returned by ScreenCaptureKit. `content` will be
@@ -119,8 +175,8 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
// object. The helper may outlive this C++ instance, if a completion-handler is passed to
// ScreenCaptureKit APIs and the C++ object is deleted before the handler executes.
SckHelper *__strong helper_ = nil;
// Callback for returning captured frames, or errors, to the caller. Only used on the caller's
// thread.
// Callback for returning captured frames to the caller. ScreenCaptureKit may
// invoke it on its sample-handler queue.
cb_desktop_data _on_data = nullptr;
// Signals that a permanent error occurred. This may be set on any thread, and is read by
// CaptureFrame() which runs on the caller's thread.
@@ -244,12 +300,6 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
display_info_list_.clear();
display_id_map_.clear();
if (nv12_frame_) {
delete[] nv12_frame_;
nv12_frame_ = nullptr;
nv12_frame_size_ = 0;
}
}
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
@@ -262,6 +312,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
fps_ = fps > 0 ? fps : 60;
display_info_list_.clear();
display_id_map_.clear();
native_output_logged_ = false;
if (@available(macOS 10.15, *)) {
bool has_permission = CGPreflightScreenCaptureAccess();
@@ -596,81 +647,69 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
return;
}
size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);
if (width == 0 || height == 0 || CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, planes={}", width, height,
const size_t width = CVPixelBufferGetWidth(pixelBuffer);
const size_t height = CVPixelBufferGetHeight(pixelBuffer);
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixelBuffer);
if (width == 0 || height == 0 || (width & 1U) != 0 ||
(height & 1U) != 0 || !CVPixelBufferIsPlanar(pixelBuffer) ||
CVPixelBufferGetPlaneCount(pixelBuffer) < 2 ||
(pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)) {
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, format={}, planes={}",
width, height, pixel_format,
CVPixelBufferGetPlaneCount(pixelBuffer));
return;
}
CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
if (status != kCVReturnSuccess) {
LOG_ERROR("Failed to lock CVPixelBuffer base address: %d", status);
return;
}
size_t required_size = width * height * 3 / 2;
const size_t required_size = width * height * 3U / 2U;
if (required_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
LOG_ERROR("Captured frame is too large: {} bytes", required_size);
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
std::lock_guard<std::mutex> lock(lock_);
if (!_on_data || !capture_requested_) {
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
if (!nv12_frame_ || nv12_frame_size_ < required_size) {
delete[] nv12_frame_;
nv12_frame_ = new unsigned char[required_size];
nv12_frame_size_ = required_size;
}
width_ = static_cast<int>(width);
height_ = static_cast<int>(height);
void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
if (!base_y || !base_uv || stride_y < width || stride_uv < width) {
LOG_ERROR("Invalid CVPixelBuffer planes: base_y={}, base_uv={}, stride_y={}, stride_uv={}",
base_y != nullptr, base_uv != nullptr, stride_y, stride_uv);
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
unsigned char *dst_y = nv12_frame_;
for (size_t row = 0; row < height; ++row) {
memcpy(dst_y + row * width, static_cast<unsigned char *>(base_y) + row * stride_y, width);
}
unsigned char *dst_uv = nv12_frame_ + width * height;
for (size_t row = 0; row < height / 2; ++row) {
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
}
const std::string stream_id = ResolveDisplayStreamId(
current_stream_id_.c_str(), display_info_list_.size(),
current_monitor_index_);
if (stream_id.empty()) {
if (!invalid_stream_id_logged_) {
LOG_ERROR("Dropping macOS frames without a registered stream id, "
"display_id={}",
current_display_);
invalid_stream_id_logged_ = true;
cb_desktop_data callback;
std::string stream_id;
bool log_native_output = false;
{
std::lock_guard<std::mutex> lock(lock_);
if (!_on_data || !capture_requested_) {
return;
}
stream_id = ResolveDisplayStreamId(current_stream_id_.c_str(),
display_info_list_.size(),
current_monitor_index_);
if (stream_id.empty()) {
if (!invalid_stream_id_logged_) {
LOG_ERROR("Dropping macOS frames without a registered stream id, "
"display_id={}",
current_display_);
invalid_stream_id_logged_ = true;
}
return;
}
invalid_stream_id_logged_ = false;
callback = _on_data;
if (!native_output_logged_) {
native_output_logged_ = true;
log_native_output = true;
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
invalid_stream_id_logged_ = false;
_on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
static_cast<int>(height), stream_id.c_str());
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
XNativeVideoFrame native_frame{};
native_frame.struct_size = sizeof(native_frame);
native_frame.type = XNativeVideoFrameCVPixelBuffer;
native_frame.width = static_cast<uint32_t>(width);
native_frame.height = static_cast<uint32_t>(height);
native_frame.payload.cv_pixel_buffer = pixelBuffer;
native_frame.owner = pixelBuffer;
native_frame.retain = &RetainCapturedPixelBuffer;
native_frame.release = &ReleaseCapturedPixelBuffer;
native_frame.copy_to_nv12 = &CopyCapturedPixelBufferToNv12;
if (log_native_output) {
LOG_INFO("macOS ScreenCaptureKit native CVPixelBuffer output enabled");
}
callback(nullptr, static_cast<int>(required_size), static_cast<int>(width),
static_cast<int>(height), stream_id.c_str(), &native_frame);
}
void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
@@ -1,3 +1,12 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SYSTEM_INFO_H_
#define _SYSTEM_INFO_H_
#include "platform.h"
#include <ifaddrs.h>
@@ -53,3 +62,6 @@ std::string GetHostName() {
bool IsWaylandSession() { return false; }
} // namespace crossdesk
#endif
@@ -384,7 +384,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
const std::string stream_id = MakeDisplayStreamId(idx);
callback_(nv12_frame_, nv12_size, even_width, even_height,
stream_id.c_str());
stream_id.c_str(), nullptr);
} else {
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
@@ -212,7 +212,8 @@ void ScreenCapturerGdi::CaptureLoop() {
if (callback_) {
const std::string stream_id = MakeDisplayStreamId(idx);
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str());
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str(),
nullptr);
}
SelectObject(mem_dc, old);
@@ -406,7 +406,7 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
const std::string stream_id = MakeDisplayStreamId(id);
on_data_(nv12_frame_, nv12_size, even_width, even_height,
stream_id.c_str());
stream_id.c_str(), nullptr);
}
}
@@ -15,6 +15,7 @@
#include <vector>
#include <display_stream_id.h>
#include "captured_nv12_frame.h"
#include "interactive_state.h"
#include "rd_log.h"
#include "screen_capturer_dxgi.h"
@@ -403,8 +404,17 @@ ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
cb_orig_ = cb;
native_output_logged_.store(false, std::memory_order_relaxed);
native_output_error_logged_.store(false, std::memory_order_relaxed);
try {
native_frame_pool_ = CapturedNv12FramePool::Create();
} catch (...) {
LOG_ERROR("Windows capturer: failed to create native NV12 frame pool");
return -1;
}
cb_ = [this](unsigned char* data, int size, int w, int h,
const char* reported_stream_id) {
const char* reported_stream_id,
const XNativeVideoFrame* native_frame) {
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
return;
}
@@ -450,7 +460,8 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
"elapsed_ms={}",
raw_stream_id, mapped_stream_id, w, h, size, elapsed_ms);
}
if (cb_orig_) cb_orig_(data, size, w, h, mapped_stream_id.c_str());
EmitCapturedFrame(data, size, w, h, mapped_stream_id.c_str(),
native_frame);
};
int ret = -1;
@@ -513,9 +524,52 @@ int ScreenCapturerWin::Destroy() {
stream_id_alias_.clear();
handle_to_canonical_index_.clear();
}
native_frame_pool_.reset();
return 0;
}
void ScreenCapturerWin::EmitCapturedFrame(
unsigned char* data, int size, int width, int height,
const char* stream_id, const XNativeVideoFrame* native_frame) {
if (!cb_orig_) {
return;
}
if (native_frame) {
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
LOG_INFO("Windows capturer native frame output enabled (type={})",
static_cast<uint32_t>(native_frame->type));
}
cb_orig_(data, size, width, height, stream_id, native_frame);
return;
}
CapturedNv12Frame* owned_frame = nullptr;
if (native_frame_pool_ && data && size > 0 && width > 0 && height > 0) {
owned_frame = native_frame_pool_->CopyFrom(
data, static_cast<size_t>(size), static_cast<uint32_t>(width),
static_cast<uint32_t>(height));
}
if (!owned_frame) {
if (!native_output_error_logged_.exchange(true,
std::memory_order_relaxed)) {
LOG_WARN(
"Windows capturer could not retain a native NV12 frame; falling "
"back to copied CPU input (size={}x{}, bytes={})",
width, height, size);
}
cb_orig_(data, size, width, height, stream_id, nullptr);
return;
}
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
LOG_INFO("Windows capturer native NV12 output enabled");
}
cb_orig_(nullptr, static_cast<int>(owned_frame->Size()), width, height,
stream_id, owned_frame->Descriptor());
owned_frame->Release();
}
int ScreenCapturerWin::Start(bool show_cursor) {
if (!impl_) return -1;
if (running_.load(std::memory_order_relaxed)) {
@@ -1261,9 +1315,11 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
ReadSecureDesktopSharedFrame(
static_cast<DWORD>(frame_interval_ms + 20), &secure_frame,
&captured_width, &captured_height, &error_message)) {
if (cb_orig_ && !secure_frame.empty()) {
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
captured_width, captured_height, display_name.c_str());
if (!secure_frame.empty()) {
EmitCapturedFrame(secure_frame.data(),
static_cast<int>(secure_frame.size()),
captured_width, captured_height,
display_name.c_str());
}
frame_delivered = true;
}
@@ -1275,9 +1331,11 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
status.interactive_desktop,
&secure_frame, &captured_width,
&captured_height, &error_message)) {
if (cb_orig_ && !secure_frame.empty()) {
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
captured_width, captured_height, display_name.c_str());
if (!secure_frame.empty()) {
EmitCapturedFrame(secure_frame.data(),
static_cast<int>(secure_frame.size()),
captured_width, captured_height,
display_name.c_str());
}
frame_delivered = true;
}
@@ -14,6 +14,7 @@
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
@@ -22,6 +23,8 @@
namespace crossdesk {
class CapturedNv12FramePool;
class ScreenCapturerWin : public ScreenCapturer {
public:
ScreenCapturerWin();
@@ -53,6 +56,8 @@ class ScreenCapturerWin : public ScreenCapturer {
std::mutex alias_mutex_;
std::vector<DisplayInfo> canonical_displays_;
std::atomic<bool> invalid_stream_id_logged_{false};
std::atomic<bool> native_output_logged_{false};
std::atomic<bool> native_output_error_logged_{false};
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<bool> show_cursor_{true};
@@ -77,9 +82,13 @@ class ScreenCapturerWin : public ScreenCapturer {
std::string secure_shared_stage_;
std::string secure_shared_desktop_;
bool secure_shared_capture_started_ = false;
std::shared_ptr<CapturedNv12FramePool> native_frame_pool_;
void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl();
void EmitCapturedFrame(unsigned char* data, int size, int width, int height,
const char* stream_id,
const XNativeVideoFrame* native_frame = nullptr);
void StopSecureCaptureThread();
bool RestartCaptureBackendAfterSecureDesktop();
void SecureDesktopCaptureLoop();
@@ -0,0 +1,165 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include "captured_nv12_frame.h"
#include <algorithm>
#include <cstring>
#include <limits>
#include <new>
#include <utility>
namespace crossdesk {
namespace {
bool GetNv12Size(uint32_t width, uint32_t height, size_t* size) {
if (!size || width == 0 || height == 0 || (width & 1U) != 0 ||
(height & 1U) != 0 ||
static_cast<size_t>(width) >
std::numeric_limits<size_t>::max() / height) {
return false;
}
const size_t pixels = static_cast<size_t>(width) * height;
if (pixels > std::numeric_limits<size_t>::max() / 3U * 2U) {
return false;
}
*size = pixels * 3U / 2U;
return true;
}
} // namespace
std::shared_ptr<CapturedNv12FramePool> CapturedNv12FramePool::Create(
size_t max_cached_buffers) {
return std::shared_ptr<CapturedNv12FramePool>(
new CapturedNv12FramePool(max_cached_buffers));
}
CapturedNv12FramePool::CapturedNv12FramePool(size_t max_cached_buffers)
: max_cached_buffers_(max_cached_buffers) {}
CapturedNv12Frame* CapturedNv12FramePool::CopyFrom(const uint8_t* data,
size_t size,
uint32_t width,
uint32_t height) {
size_t required_size = 0;
if (!data || !GetNv12Size(width, height, &required_size) ||
size < required_size) {
return nullptr;
}
std::vector<uint8_t> storage;
{
std::lock_guard<std::mutex> lock(mutex_);
auto best = free_buffers_.end();
for (auto it = free_buffers_.begin(); it != free_buffers_.end(); ++it) {
if (it->capacity() < required_size) {
continue;
}
if (best == free_buffers_.end() || it->capacity() < best->capacity()) {
best = it;
}
}
if (best == free_buffers_.end() && !free_buffers_.empty()) {
best = std::max_element(
free_buffers_.begin(), free_buffers_.end(),
[](const auto& lhs, const auto& rhs) {
return lhs.capacity() < rhs.capacity();
});
}
if (best != free_buffers_.end()) {
storage = std::move(*best);
free_buffers_.erase(best);
}
}
try {
storage.resize(required_size);
std::memcpy(storage.data(), data, required_size);
return new CapturedNv12Frame(shared_from_this(), std::move(storage), width,
height);
} catch (...) {
return nullptr;
}
}
void CapturedNv12FramePool::Recycle(std::vector<uint8_t> storage) {
if (max_cached_buffers_ == 0 || storage.empty()) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (free_buffers_.size() < max_cached_buffers_) {
free_buffers_.push_back(std::move(storage));
return;
}
auto smallest = std::min_element(
free_buffers_.begin(), free_buffers_.end(),
[](const auto& lhs, const auto& rhs) {
return lhs.capacity() < rhs.capacity();
});
if (smallest != free_buffers_.end() &&
smallest->capacity() < storage.capacity()) {
*smallest = std::move(storage);
}
}
CapturedNv12Frame::CapturedNv12Frame(
std::shared_ptr<CapturedNv12FramePool> pool,
std::vector<uint8_t> storage, uint32_t width, uint32_t height)
: pool_(std::move(pool)),
storage_(std::move(storage)),
width_(width),
height_(height) {
descriptor_.struct_size = sizeof(descriptor_);
descriptor_.type = XNativeVideoFrameCpuNv12;
descriptor_.width = width_;
descriptor_.height = height_;
descriptor_.payload.cpu_nv12 = {
storage_.data(), storage_.data() + static_cast<size_t>(width_) * height_,
width_, width_};
descriptor_.owner = this;
descriptor_.retain = &RetainOwner;
descriptor_.release = &ReleaseOwner;
descriptor_.copy_to_nv12 = &CopyToNv12;
}
CapturedNv12Frame::~CapturedNv12Frame() {
if (pool_) {
pool_->Recycle(std::move(storage_));
}
}
void CapturedNv12Frame::AddRef() {
references_.fetch_add(1, std::memory_order_relaxed);
}
void CapturedNv12Frame::Release() {
if (references_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete this;
}
}
void CapturedNv12Frame::RetainOwner(void* owner) {
static_cast<CapturedNv12Frame*>(owner)->AddRef();
}
void CapturedNv12Frame::ReleaseOwner(void* owner) {
static_cast<CapturedNv12Frame*>(owner)->Release();
}
int CapturedNv12Frame::CopyToNv12(void* owner, uint8_t* destination,
size_t destination_size) {
auto* frame = static_cast<CapturedNv12Frame*>(owner);
if (!frame || !destination || destination_size < frame->Size()) {
return -1;
}
std::memcpy(destination, frame->storage_.data(), frame->Size());
return 0;
}
} // namespace crossdesk
@@ -0,0 +1,78 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CAPTURED_NV12_FRAME_H_
#define _CAPTURED_NV12_FRAME_H_
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <vector>
#include "minirtc.h"
namespace crossdesk {
class CapturedNv12Frame;
// Owns the pixels behind captured native frames. A frame may outlive the
// capture callback while MiniRTC waits for its asynchronous encoder, so capture
// buffers cannot be borrowed directly. Released allocations are cached to keep
// that ownership transfer inexpensive after the pipeline is warm.
class CapturedNv12FramePool final
: public std::enable_shared_from_this<CapturedNv12FramePool> {
public:
static std::shared_ptr<CapturedNv12FramePool> Create(
size_t max_cached_buffers = 4);
CapturedNv12Frame* CopyFrom(const uint8_t* data, size_t size,
uint32_t width, uint32_t height);
private:
explicit CapturedNv12FramePool(size_t max_cached_buffers);
void Recycle(std::vector<uint8_t> storage);
friend class CapturedNv12Frame;
const size_t max_cached_buffers_;
std::mutex mutex_;
std::vector<std::vector<uint8_t>> free_buffers_;
};
class CapturedNv12Frame final {
public:
const XNativeVideoFrame* Descriptor() const { return &descriptor_; }
size_t Size() const { return storage_.size(); }
void AddRef();
void Release();
private:
friend class CapturedNv12FramePool;
CapturedNv12Frame(std::shared_ptr<CapturedNv12FramePool> pool,
std::vector<uint8_t> storage, uint32_t width,
uint32_t height);
~CapturedNv12Frame();
static void RetainOwner(void* owner);
static void ReleaseOwner(void* owner);
static int CopyToNv12(void* owner, uint8_t* destination,
size_t destination_size);
std::atomic<uint32_t> references_{1};
std::shared_ptr<CapturedNv12FramePool> pool_;
std::vector<uint8_t> storage_;
uint32_t width_ = 0;
uint32_t height_ = 0;
XNativeVideoFrame descriptor_{};
};
} // namespace crossdesk
#endif
@@ -10,14 +10,18 @@
#include <functional>
#include "display_info.h"
#include "minirtc.h"
namespace crossdesk {
class ScreenCapturer {
public:
// The final callback argument is a logical MiniRTC stream ID (DisplayN),
// not a platform display name or physical handle.
typedef std::function<void(unsigned char*, int, int, int, const char*)>
// |stream_id| is a logical MiniRTC stream ID (DisplayN), not a platform
// display name or physical handle. |native_frame| is borrowed for the
// duration of the callback; retain its owner before using it asynchronously.
typedef std::function<void(unsigned char* data, int size, int width,
int height, const char* stream_id,
const XNativeVideoFrame* native_frame)>
cb_desktop_data;
public:
+4 -1
View File
@@ -159,8 +159,10 @@ function setup_targets()
set_kind("object")
add_deps("rd_log", "common", "crossdesk_wire")
add_includedirs("apps/desktop/src/screen_capturer", {public = true})
add_includedirs("deps/submodules/minirtc/src/api", {public = true})
if is_os("windows") then
add_packages("libyuv")
add_files("apps/desktop/src/screen_capturer/captured_nv12_frame.cpp")
add_files("apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp",
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp",
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp",
@@ -367,7 +369,8 @@ function setup_targets()
"apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_entry.cpp")
add_files("apps/desktop/resources/windows/wgc_plugin.rc")
add_includedirs("apps/desktop/src/common", "apps/desktop/src/screen_capturer",
"apps/desktop/src/platform/windows/screen_capturer")
"apps/desktop/src/platform/windows/screen_capturer",
"deps/submodules/minirtc/src/api")
target("crossdesk_service")
set_kind("binary")