mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-23 13:06:45 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
010064d5b7 | ||
|
|
db3b26efe7 | ||
|
|
cd1b53ee65 | ||
|
|
b031a3953e | ||
|
|
3f537ae63c |
@@ -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());
|
||||
|
||||
@@ -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
|
||||
@@ -141,9 +141,14 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
sizeof(params_.log_path) - 1);
|
||||
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
|
||||
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
|
||||
// The Slint desktop renderer currently consumes packed CPU frames. Native
|
||||
// renderers can opt into platform-native frame output independently.
|
||||
#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;
|
||||
#endif
|
||||
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
|
||||
? true
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -14,6 +14,18 @@ namespace {
|
||||
|
||||
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
|
||||
|
||||
const XNativeVideoFrame* GetNativeVideoFrame(const XVideoFrame* frame) {
|
||||
if (!frame || !frame->native_frame) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto* native = frame->native_frame;
|
||||
return native->struct_size >=
|
||||
static_cast<uint32_t>(sizeof(XNativeVideoFrame)) &&
|
||||
native->owner && native->copy_to_nv12
|
||||
? native
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
@@ -38,9 +50,17 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
bool background_snapshot_only = false;
|
||||
auto* native_renderer = runtime->video_renderer_.get();
|
||||
if (native_renderer && native_renderer->IsActive()) {
|
||||
const auto submit_result = native_renderer->SubmitNv12(
|
||||
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||
video_frame->size, video_frame->width, video_frame->height);
|
||||
VideoRenderer::SubmitResult submit_result =
|
||||
VideoRenderer::SubmitResult::failed;
|
||||
if (GetNativeVideoFrame(video_frame)) {
|
||||
submit_result = native_renderer->SubmitNativeFrame(
|
||||
remote_id, *video_frame->native_frame);
|
||||
} else
|
||||
{
|
||||
submit_result = native_renderer->SubmitNv12(
|
||||
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||
video_frame->size, video_frame->width, video_frame->height);
|
||||
}
|
||||
if (submit_result == VideoRenderer::SubmitResult::submitted) {
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
@@ -88,17 +108,39 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
return;
|
||||
}
|
||||
|
||||
size_t frame_size = video_frame->size;
|
||||
const XNativeVideoFrame* native_frame =
|
||||
GetNativeVideoFrame(video_frame);
|
||||
if (native_frame) {
|
||||
frame_size = static_cast<size_t>(native_frame->width) *
|
||||
native_frame->height * 3U / 2U;
|
||||
}
|
||||
if (frame_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate a third buffer only while the UI still owns the old snapshot.
|
||||
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
|
||||
props->back_frame_ =
|
||||
std::make_shared<std::vector<unsigned char>>(video_frame->size);
|
||||
std::make_shared<std::vector<unsigned char>>(frame_size);
|
||||
}
|
||||
if (props->back_frame_->size() != video_frame->size) {
|
||||
props->back_frame_->resize(video_frame->size);
|
||||
if (props->back_frame_->size() != frame_size) {
|
||||
props->back_frame_->resize(frame_size);
|
||||
}
|
||||
|
||||
std::memcpy(props->back_frame_->data(), video_frame->data,
|
||||
video_frame->size);
|
||||
if (native_frame) {
|
||||
if (native_frame->copy_to_nv12(
|
||||
native_frame->owner, props->back_frame_->data(),
|
||||
props->back_frame_->size()) != 0) {
|
||||
return;
|
||||
}
|
||||
} else
|
||||
{
|
||||
if (!video_frame->data || video_frame->size < frame_size) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(props->back_frame_->data(), video_frame->data, frame_size);
|
||||
}
|
||||
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
(props->video_height_ != video_frame->height);
|
||||
@@ -108,7 +150,7 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
props->video_size_ = frame_size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
props->thumbnail_frame_ = props->front_frame_;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -26,11 +26,97 @@
|
||||
#include <utility>
|
||||
|
||||
#include "rd_log.h"
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
#include "nvcodec_api.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kFrameSlotCount = 3;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
constexpr size_t kCudaUploadSlotCount = 3;
|
||||
#endif
|
||||
|
||||
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 *GetOpenGlNativeFrame(
|
||||
const XNativeVideoFrame &frame) {
|
||||
const XNativeVideoFrame *native = &frame;
|
||||
if (native->struct_size < static_cast<uint32_t>(sizeof(XNativeVideoFrame)) ||
|
||||
!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;
|
||||
}
|
||||
if (native->type == XNativeVideoFrameCpuNv12) {
|
||||
return native->payload.cpu_nv12.y_plane &&
|
||||
native->payload.cpu_nv12.uv_plane &&
|
||||
native->payload.cpu_nv12.y_stride >= native->width &&
|
||||
native->payload.cpu_nv12.uv_stride >= native->width
|
||||
? native
|
||||
: nullptr;
|
||||
}
|
||||
if (native->type == XNativeVideoFrameCudaNv12) {
|
||||
return native->payload.cuda_nv12.y_device_pointer != 0 &&
|
||||
native->payload.cuda_nv12.uv_device_pointer != 0 &&
|
||||
native->payload.cuda_nv12.y_stride >= native->width &&
|
||||
native->payload.cuda_nv12.uv_stride >= native->width &&
|
||||
native->payload.cuda_nv12.context
|
||||
? native
|
||||
: nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
enum class SlotUse {
|
||||
available,
|
||||
@@ -40,6 +126,7 @@ enum class SlotUse {
|
||||
|
||||
struct FrameSlot {
|
||||
std::vector<unsigned char> bytes;
|
||||
NativeVideoFrameRef native_frame;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::string remote_id;
|
||||
@@ -107,6 +194,11 @@ struct OpenGlFunctions {
|
||||
PFNGLUNIFORM4FPROC uniform_4f = nullptr;
|
||||
PFNGLUSEPROGRAMPROC use_program = nullptr;
|
||||
PFNGLVERTEXATTRIBPOINTERPROC vertex_attrib_pointer = nullptr;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
PFNGLFENCESYNCPROC fence_sync = nullptr;
|
||||
PFNGLCLIENTWAITSYNCPROC client_wait_sync = nullptr;
|
||||
PFNGLDELETESYNCPROC delete_sync = nullptr;
|
||||
#endif
|
||||
|
||||
bool Load() {
|
||||
return LoadOpenGlFunction(&active_texture, "glActiveTexture") &&
|
||||
@@ -142,6 +234,14 @@ struct OpenGlFunctions {
|
||||
LoadOpenGlFunction(&use_program, "glUseProgram") &&
|
||||
LoadOpenGlFunction(&vertex_attrib_pointer, "glVertexAttribPointer");
|
||||
}
|
||||
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
bool LoadSyncFunctions() {
|
||||
return LoadOpenGlFunction(&fence_sync, "glFenceSync") &&
|
||||
LoadOpenGlFunction(&client_wait_sync, "glClientWaitSync") &&
|
||||
LoadOpenGlFunction(&delete_sync, "glDeleteSync");
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
GLuint CompileShader(const OpenGlFunctions &gl, GLenum type,
|
||||
@@ -249,6 +349,316 @@ struct OpenGlVideoRenderer::Impl {
|
||||
std::string uploaded_stream;
|
||||
uint64_t uploaded_sequence = 0;
|
||||
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
enum class CudaUploadResult {
|
||||
ready,
|
||||
busy,
|
||||
unavailable,
|
||||
};
|
||||
|
||||
struct CudaUploadSlot {
|
||||
GLuint buffer = 0;
|
||||
GLuint y_texture = 0;
|
||||
GLuint uv_texture = 0;
|
||||
size_t capacity = 0;
|
||||
int texture_width = 0;
|
||||
int texture_height = 0;
|
||||
CUgraphicsResource resource = nullptr;
|
||||
GLsync fence = nullptr;
|
||||
};
|
||||
|
||||
std::array<CudaUploadSlot, kCudaUploadSlotCount> cuda_upload_slots;
|
||||
size_t next_cuda_upload_slot = 0;
|
||||
size_t displayed_cuda_upload_slot = kCudaUploadSlotCount;
|
||||
CUcontext cuda_context = nullptr;
|
||||
CUstream cuda_upload_stream = nullptr;
|
||||
NativeVideoFrameRef cuda_context_owner;
|
||||
bool cuda_sync_functions_available = false;
|
||||
bool cuda_interop_disabled = false;
|
||||
std::atomic<bool> cuda_cpu_fallback{false};
|
||||
bool cuda_native_logged = false;
|
||||
bool cuda_fallback_logged = false;
|
||||
uint64_t cuda_busy_drop_count = 0;
|
||||
|
||||
void DisableCudaInterop() {
|
||||
cuda_interop_disabled = true;
|
||||
cuda_cpu_fallback.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool IsCudaSlotAvailable(CudaUploadSlot &slot) {
|
||||
if (!slot.fence) {
|
||||
return true;
|
||||
}
|
||||
const GLenum wait_result =
|
||||
gl.client_wait_sync(slot.fence, 0, 0);
|
||||
if (wait_result == GL_WAIT_FAILED) {
|
||||
DisableCudaInterop();
|
||||
return false;
|
||||
}
|
||||
if (wait_result != GL_ALREADY_SIGNALED &&
|
||||
wait_result != GL_CONDITION_SATISFIED) {
|
||||
return false;
|
||||
}
|
||||
gl.delete_sync(slot.fence);
|
||||
slot.fence = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ReleaseCudaInterop() {
|
||||
bool has_gl_resources = false;
|
||||
for (const auto &slot : cuda_upload_slots) {
|
||||
has_gl_resources = has_gl_resources || slot.buffer != 0 || slot.fence;
|
||||
}
|
||||
if (has_gl_resources) {
|
||||
glFinish();
|
||||
}
|
||||
for (auto &slot : cuda_upload_slots) {
|
||||
if (slot.fence && gl.delete_sync) {
|
||||
gl.delete_sync(slot.fence);
|
||||
slot.fence = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (cuda_context && minirtc::cuCtxPushCurrent_ld &&
|
||||
minirtc::cuCtxPushCurrent_ld(cuda_context) == CUDA_SUCCESS) {
|
||||
if (cuda_upload_stream && minirtc::cuStreamSynchronize_ld) {
|
||||
minirtc::cuStreamSynchronize_ld(cuda_upload_stream);
|
||||
}
|
||||
for (auto &slot : cuda_upload_slots) {
|
||||
if (slot.resource && minirtc::cuGraphicsUnregisterResource_ld) {
|
||||
minirtc::cuGraphicsUnregisterResource_ld(slot.resource);
|
||||
slot.resource = nullptr;
|
||||
}
|
||||
}
|
||||
if (cuda_upload_stream && minirtc::cuStreamDestroy_ld) {
|
||||
minirtc::cuStreamDestroy_ld(cuda_upload_stream);
|
||||
}
|
||||
CUcontext popped_context = nullptr;
|
||||
minirtc::cuCtxPopCurrent_ld(&popped_context);
|
||||
}
|
||||
cuda_upload_stream = nullptr;
|
||||
cuda_context = nullptr;
|
||||
cuda_context_owner.Reset();
|
||||
|
||||
for (auto &slot : cuda_upload_slots) {
|
||||
if (slot.buffer != 0 && gl.delete_buffers) {
|
||||
gl.delete_buffers(1, &slot.buffer);
|
||||
}
|
||||
if (slot.y_texture != 0) {
|
||||
glDeleteTextures(1, &slot.y_texture);
|
||||
}
|
||||
if (slot.uv_texture != 0) {
|
||||
glDeleteTextures(1, &slot.uv_texture);
|
||||
}
|
||||
slot = {};
|
||||
}
|
||||
next_cuda_upload_slot = 0;
|
||||
displayed_cuda_upload_slot = kCudaUploadSlotCount;
|
||||
}
|
||||
|
||||
bool PrepareCudaContext(const NativeVideoFrameRef &native_frame) {
|
||||
const XNativeVideoFrame *frame = native_frame.Get();
|
||||
if (!frame || !frame->payload.cuda_nv12.context ||
|
||||
cuda_interop_disabled ||
|
||||
!cuda_sync_functions_available ||
|
||||
minirtc::LoadCudaGraphicsInterop() != 0) {
|
||||
DisableCudaInterop();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto frame_context =
|
||||
static_cast<CUcontext>(frame->payload.cuda_nv12.context);
|
||||
if (cuda_context && cuda_context != frame_context) {
|
||||
ReleaseCudaInterop();
|
||||
}
|
||||
if (!cuda_context) {
|
||||
cuda_context_owner = native_frame;
|
||||
cuda_context = frame_context;
|
||||
if (minirtc::cuCtxPushCurrent_ld(cuda_context) != CUDA_SUCCESS) {
|
||||
ReleaseCudaInterop();
|
||||
DisableCudaInterop();
|
||||
return false;
|
||||
}
|
||||
const CUresult create_result = minirtc::cuStreamCreate_ld(
|
||||
&cuda_upload_stream, CU_STREAM_NON_BLOCKING);
|
||||
CUcontext popped_context = nullptr;
|
||||
const CUresult pop_result =
|
||||
minirtc::cuCtxPopCurrent_ld(&popped_context);
|
||||
if (create_result != CUDA_SUCCESS || pop_result != CUDA_SUCCESS) {
|
||||
ReleaseCudaInterop();
|
||||
DisableCudaInterop();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrepareCudaSlot(CudaUploadSlot &slot, size_t required_size) {
|
||||
if (slot.buffer == 0) {
|
||||
gl.gen_buffers(1, &slot.buffer);
|
||||
}
|
||||
if (slot.buffer == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slot.capacity < required_size) {
|
||||
if (slot.resource) {
|
||||
if (minirtc::cuGraphicsUnregisterResource_ld(slot.resource) !=
|
||||
CUDA_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
slot.resource = nullptr;
|
||||
}
|
||||
GLint previous_unpack_buffer = 0;
|
||||
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previous_unpack_buffer);
|
||||
gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER, slot.buffer);
|
||||
gl.buffer_data(GL_PIXEL_UNPACK_BUFFER,
|
||||
static_cast<GLsizeiptr>(required_size), nullptr,
|
||||
GL_STREAM_DRAW);
|
||||
gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER,
|
||||
static_cast<GLuint>(previous_unpack_buffer));
|
||||
if (glGetError() != GL_NO_ERROR) {
|
||||
return false;
|
||||
}
|
||||
slot.capacity = required_size;
|
||||
}
|
||||
|
||||
if (!slot.resource) {
|
||||
if (minirtc::cuGraphicsGLRegisterBuffer_ld(
|
||||
&slot.resource, slot.buffer,
|
||||
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD) != CUDA_SUCCESS) {
|
||||
slot.resource = nullptr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrepareCudaTextures(CudaUploadSlot &slot) {
|
||||
const bool needs_configuration =
|
||||
slot.y_texture == 0 || slot.uv_texture == 0;
|
||||
if (slot.y_texture == 0) {
|
||||
glGenTextures(1, &slot.y_texture);
|
||||
}
|
||||
if (slot.uv_texture == 0) {
|
||||
glGenTextures(1, &slot.uv_texture);
|
||||
}
|
||||
if (slot.y_texture == 0 || slot.uv_texture == 0) {
|
||||
return false;
|
||||
}
|
||||
if (!needs_configuration) {
|
||||
return true;
|
||||
}
|
||||
for (const auto [unit, texture] :
|
||||
{std::pair{GL_TEXTURE0, slot.y_texture},
|
||||
std::pair{GL_TEXTURE1, slot.uv_texture}}) {
|
||||
gl.active_texture(unit);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
return glGetError() == GL_NO_ERROR;
|
||||
}
|
||||
|
||||
CudaUploadResult CopyCudaFrameToUploadBuffer(
|
||||
const NativeVideoFrameRef &native_frame, size_t *upload_slot_index) {
|
||||
const XNativeVideoFrame *frame = native_frame.Get();
|
||||
if (!upload_slot_index || !PrepareCudaContext(native_frame)) {
|
||||
return CudaUploadResult::unavailable;
|
||||
}
|
||||
|
||||
size_t selected_index = kCudaUploadSlotCount;
|
||||
for (size_t offset = 0; offset < cuda_upload_slots.size(); ++offset) {
|
||||
const size_t index =
|
||||
(next_cuda_upload_slot + offset) % cuda_upload_slots.size();
|
||||
if (IsCudaSlotAvailable(cuda_upload_slots[index])) {
|
||||
selected_index = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selected_index == kCudaUploadSlotCount) {
|
||||
++cuda_busy_drop_count;
|
||||
if (cuda_busy_drop_count == 1 || cuda_busy_drop_count % 300 == 0) {
|
||||
LOG_WARN("CUDA/OpenGL upload ring busy; dropped {} display frames",
|
||||
cuda_busy_drop_count);
|
||||
}
|
||||
return CudaUploadResult::busy;
|
||||
}
|
||||
|
||||
if (minirtc::cuCtxPushCurrent_ld(cuda_context) != CUDA_SUCCESS) {
|
||||
DisableCudaInterop();
|
||||
return CudaUploadResult::unavailable;
|
||||
}
|
||||
|
||||
CudaUploadSlot &slot = cuda_upload_slots[selected_index];
|
||||
const size_t packed_size =
|
||||
static_cast<size_t>(frame->width) * frame->height * 3U / 2U;
|
||||
CUresult result = PrepareCudaSlot(slot, packed_size)
|
||||
? CUDA_SUCCESS
|
||||
: CUDA_ERROR_INVALID_VALUE;
|
||||
CUgraphicsResource resource = slot.resource;
|
||||
bool mapped = false;
|
||||
CUdeviceptr destination = 0;
|
||||
size_t mapped_size = 0;
|
||||
if (result == CUDA_SUCCESS) {
|
||||
result = minirtc::cuGraphicsMapResources_ld(
|
||||
1, &resource, cuda_upload_stream);
|
||||
mapped = result == CUDA_SUCCESS;
|
||||
}
|
||||
if (result == CUDA_SUCCESS) {
|
||||
result = minirtc::cuGraphicsResourceGetMappedPointer_ld(
|
||||
&destination, &mapped_size, slot.resource);
|
||||
}
|
||||
if (result == CUDA_SUCCESS && mapped_size < packed_size) {
|
||||
result = CUDA_ERROR_INVALID_VALUE;
|
||||
}
|
||||
if (result == CUDA_SUCCESS) {
|
||||
CUDA_MEMCPY2D copy{};
|
||||
copy.srcMemoryType = CU_MEMORYTYPE_DEVICE;
|
||||
copy.srcDevice =
|
||||
static_cast<CUdeviceptr>(
|
||||
frame->payload.cuda_nv12.y_device_pointer);
|
||||
copy.srcPitch = frame->payload.cuda_nv12.y_stride;
|
||||
copy.dstMemoryType = CU_MEMORYTYPE_DEVICE;
|
||||
copy.dstDevice = destination;
|
||||
copy.dstPitch = frame->width;
|
||||
copy.WidthInBytes = frame->width;
|
||||
copy.Height = frame->height;
|
||||
result = minirtc::cuMemcpy2DAsync_ld(©, cuda_upload_stream);
|
||||
if (result == CUDA_SUCCESS) {
|
||||
copy.srcDevice =
|
||||
static_cast<CUdeviceptr>(
|
||||
frame->payload.cuda_nv12.uv_device_pointer);
|
||||
copy.srcPitch = frame->payload.cuda_nv12.uv_stride;
|
||||
copy.dstDevice = destination +
|
||||
static_cast<CUdeviceptr>(frame->width) * frame->height;
|
||||
copy.Height = frame->height / 2U;
|
||||
result = minirtc::cuMemcpy2DAsync_ld(©, cuda_upload_stream);
|
||||
}
|
||||
}
|
||||
if (mapped) {
|
||||
const CUresult unmap_result = minirtc::cuGraphicsUnmapResources_ld(
|
||||
1, &resource, cuda_upload_stream);
|
||||
if (result == CUDA_SUCCESS) {
|
||||
result = unmap_result;
|
||||
}
|
||||
}
|
||||
CUcontext popped_context = nullptr;
|
||||
const CUresult pop_result =
|
||||
minirtc::cuCtxPopCurrent_ld(&popped_context);
|
||||
if (result != CUDA_SUCCESS || pop_result != CUDA_SUCCESS) {
|
||||
DisableCudaInterop();
|
||||
return CudaUploadResult::unavailable;
|
||||
}
|
||||
|
||||
*upload_slot_index = selected_index;
|
||||
next_cuda_upload_slot = (selected_index + 1) % cuda_upload_slots.size();
|
||||
return CudaUploadResult::ready;
|
||||
}
|
||||
#endif
|
||||
|
||||
void ResetGlHandles() {
|
||||
program = 0;
|
||||
vertex_buffer = 0;
|
||||
@@ -266,6 +676,9 @@ struct OpenGlVideoRenderer::Impl {
|
||||
texture_height = 0;
|
||||
uploaded_stream.clear();
|
||||
uploaded_sequence = 0;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
displayed_cuda_upload_slot = kCudaUploadSlotCount;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
@@ -284,6 +697,15 @@ bool OpenGlVideoRenderer::Setup() {
|
||||
LOG_WARN("OpenGL NV12 underlay unavailable: required functions missing");
|
||||
return false;
|
||||
}
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
impl_->cuda_sync_functions_available = impl_->gl.LoadSyncFunctions();
|
||||
impl_->cuda_interop_disabled = !impl_->cuda_sync_functions_available;
|
||||
impl_->cuda_cpu_fallback.store(!impl_->cuda_sync_functions_available,
|
||||
std::memory_order_release);
|
||||
impl_->cuda_native_logged = false;
|
||||
impl_->cuda_fallback_logged = false;
|
||||
impl_->cuda_busy_drop_count = 0;
|
||||
#endif
|
||||
|
||||
static constexpr char kGlesVertexShader[] = R"glsl(
|
||||
#version 100
|
||||
@@ -524,6 +946,9 @@ void main() {
|
||||
|
||||
void OpenGlVideoRenderer::Teardown() {
|
||||
impl_->ready.store(false, std::memory_order_release);
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
impl_->ReleaseCudaInterop();
|
||||
#endif
|
||||
if (impl_->y_texture != 0)
|
||||
glDeleteTextures(1, &impl_->y_texture);
|
||||
if (impl_->uv_texture != 0)
|
||||
@@ -534,6 +959,14 @@ void OpenGlVideoRenderer::Teardown() {
|
||||
if (impl_->program != 0 && impl_->gl.delete_program) {
|
||||
impl_->gl.delete_program(impl_->program);
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
for (auto &slot : impl_->frames->slots) {
|
||||
slot.native_frame.Reset();
|
||||
slot.valid = false;
|
||||
slot.use = SlotUse::available;
|
||||
}
|
||||
}
|
||||
impl_->ResetGlHandles();
|
||||
}
|
||||
|
||||
@@ -553,6 +986,8 @@ bool OpenGlVideoRenderer::SetSelectedStream(std::string remote_id) {
|
||||
if (slot.use == SlotUse::pending &&
|
||||
slot.remote_id != impl_->frames->selected_stream) {
|
||||
slot.use = SlotUse::available;
|
||||
slot.valid = false;
|
||||
slot.native_frame.Reset();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -565,6 +1000,7 @@ void OpenGlVideoRenderer::DiscardStream(std::string_view remote_id) {
|
||||
slot.valid = false;
|
||||
slot.use = SlotUse::available;
|
||||
slot.remote_id.clear();
|
||||
slot.native_frame.Reset();
|
||||
}
|
||||
}
|
||||
if (impl_->frames->selected_stream == remote_id) {
|
||||
@@ -648,6 +1084,7 @@ OpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id,
|
||||
|
||||
target->bytes.resize(required_size);
|
||||
std::memcpy(target->bytes.data(), data, required_size);
|
||||
target->native_frame.Reset();
|
||||
target->width = width;
|
||||
target->height = height;
|
||||
target->remote_id.assign(remote_id);
|
||||
@@ -657,6 +1094,94 @@ OpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id,
|
||||
return SubmitResult::submitted;
|
||||
}
|
||||
|
||||
OpenGlVideoRenderer::SubmitResult
|
||||
OpenGlVideoRenderer::SubmitNativeFrame(std::string_view remote_id,
|
||||
const XNativeVideoFrame &frame) {
|
||||
if (!IsReady()) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
const XNativeVideoFrame *native = GetOpenGlNativeFrame(frame);
|
||||
if (!native) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
|
||||
auto &frames = *impl_->frames;
|
||||
std::lock_guard lock(frames.mutex);
|
||||
if (!IsReady()) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
if (frames.selected_stream != remote_id) {
|
||||
return SubmitResult::not_selected;
|
||||
}
|
||||
|
||||
// Latest-frame-only queue: replacing an unconsumed decoded frame bounds
|
||||
// latency without dropping compressed delta frames before decode.
|
||||
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) {
|
||||
return SubmitResult::dropped;
|
||||
}
|
||||
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
if (native->type == XNativeVideoFrameCudaNv12 &&
|
||||
impl_->cuda_cpu_fallback.load(std::memory_order_acquire)) {
|
||||
const size_t required_size =
|
||||
static_cast<size_t>(native->width) * native->height * 3U / 2U;
|
||||
target->bytes.resize(required_size);
|
||||
if (native->copy_to_nv12(native->owner, target->bytes.data(),
|
||||
target->bytes.size()) != 0) {
|
||||
target->bytes.clear();
|
||||
target->valid = false;
|
||||
target->use = SlotUse::available;
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
target->native_frame.Reset();
|
||||
target->width = static_cast<int>(native->width);
|
||||
target->height = static_cast<int>(native->height);
|
||||
target->remote_id.assign(remote_id);
|
||||
target->sequence = frames.next_sequence++;
|
||||
target->use = SlotUse::pending;
|
||||
target->valid = true;
|
||||
return SubmitResult::submitted;
|
||||
}
|
||||
#endif
|
||||
|
||||
NativeVideoFrameRef retained(native);
|
||||
if (!retained) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
target->bytes.clear();
|
||||
target->native_frame = std::move(retained);
|
||||
target->width = static_cast<int>(native->width);
|
||||
target->height = static_cast<int>(native->height);
|
||||
target->remote_id.assign(remote_id);
|
||||
target->sequence = frames.next_sequence++;
|
||||
target->use = SlotUse::pending;
|
||||
target->valid = true;
|
||||
return SubmitResult::submitted;
|
||||
}
|
||||
|
||||
OpenGlVideoRenderer::RenderOutcome
|
||||
OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
int target_width, int target_height,
|
||||
@@ -670,7 +1195,9 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
uint64_t sequence = 0;
|
||||
int source_width = 0;
|
||||
int source_height = 0;
|
||||
const unsigned char *frame_data = nullptr;
|
||||
const unsigned char *y_data = nullptr;
|
||||
const unsigned char *uv_data = nullptr;
|
||||
NativeVideoFrameRef active_native_frame;
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
for (size_t index = 0; index < impl_->frames->slots.size(); ++index) {
|
||||
@@ -687,12 +1214,19 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
selected.use = SlotUse::uploading;
|
||||
source_width = selected.width;
|
||||
source_height = selected.height;
|
||||
frame_data = selected.bytes.data();
|
||||
active_native_frame = selected.native_frame;
|
||||
if (!selected.bytes.empty()) {
|
||||
const size_t y_size = static_cast<size_t>(source_width) * source_height;
|
||||
y_data = selected.bytes.data();
|
||||
uv_data = selected.bytes.data() + y_size;
|
||||
}
|
||||
for (size_t index = 0; index < impl_->frames->slots.size(); ++index) {
|
||||
auto &slot = impl_->frames->slots[index];
|
||||
if (index != slot_index && slot.use == SlotUse::pending &&
|
||||
slot.remote_id == remote_id) {
|
||||
slot.use = SlotUse::available;
|
||||
slot.valid = false;
|
||||
slot.native_frame.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -701,6 +1235,9 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
GLint previous_program = 0;
|
||||
GLint previous_active_texture = GL_TEXTURE0;
|
||||
GLint previous_array_buffer = 0;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
GLint previous_unpack_buffer = 0;
|
||||
#endif
|
||||
GLint previous_unpack_alignment = 4;
|
||||
GLint previous_viewport[4] = {};
|
||||
GLint previous_scissor_box[4] = {};
|
||||
@@ -709,6 +1246,9 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
glGetIntegerv(GL_CURRENT_PROGRAM, &previous_program);
|
||||
glGetIntegerv(GL_ACTIVE_TEXTURE, &previous_active_texture);
|
||||
glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &previous_array_buffer);
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previous_unpack_buffer);
|
||||
#endif
|
||||
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
|
||||
glGetIntegerv(GL_VIEWPORT, previous_viewport);
|
||||
glGetIntegerv(GL_SCISSOR_BOX, previous_scissor_box);
|
||||
@@ -750,38 +1290,140 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
const size_t y_size = static_cast<size_t>(source_width) * source_height;
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->y_texture);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->uv_texture);
|
||||
if (impl_->texture_width != source_width ||
|
||||
impl_->texture_height != source_height) {
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, source_width, source_height,
|
||||
0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame_data);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, source_width / 2,
|
||||
source_height / 2, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE,
|
||||
frame_data + y_size);
|
||||
} else {
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, source_width, source_height,
|
||||
GL_LUMINANCE, GL_UNSIGNED_BYTE, frame_data);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, source_width / 2,
|
||||
source_height / 2, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE,
|
||||
frame_data + y_size);
|
||||
const size_t required_size = y_size + y_size / 2U;
|
||||
bool should_upload = true;
|
||||
bool use_pixel_unpack_buffer = false;
|
||||
size_t cuda_upload_slot = 0;
|
||||
std::vector<unsigned char> cpu_fallback;
|
||||
if (const XNativeVideoFrame *native = active_native_frame.Get()) {
|
||||
if (native->type == XNativeVideoFrameCpuNv12 &&
|
||||
native->payload.cpu_nv12.y_stride == native->width &&
|
||||
native->payload.cpu_nv12.uv_stride == native->width) {
|
||||
y_data = native->payload.cpu_nv12.y_plane;
|
||||
uv_data = native->payload.cpu_nv12.uv_plane;
|
||||
} else if (native->type == XNativeVideoFrameCudaNv12) {
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
const auto cuda_result = impl_->CopyCudaFrameToUploadBuffer(
|
||||
active_native_frame, &cuda_upload_slot);
|
||||
use_pixel_unpack_buffer =
|
||||
cuda_result == OpenGlVideoRenderer::Impl::CudaUploadResult::ready;
|
||||
should_upload =
|
||||
cuda_result != OpenGlVideoRenderer::Impl::CudaUploadResult::busy;
|
||||
if (use_pixel_unpack_buffer && !impl_->cuda_native_logged) {
|
||||
LOG_INFO("Windows native NV12 using asynchronous CUDA/OpenGL ring");
|
||||
impl_->cuda_native_logged = true;
|
||||
}
|
||||
if (should_upload && !use_pixel_unpack_buffer &&
|
||||
!impl_->cuda_fallback_logged) {
|
||||
LOG_WARN("CUDA/OpenGL native upload unavailable; using CPU fallback");
|
||||
impl_->cuda_fallback_logged = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (should_upload && !use_pixel_unpack_buffer && (!y_data || !uv_data)) {
|
||||
cpu_fallback.resize(required_size);
|
||||
if (native->copy_to_nv12(native->owner, cpu_fallback.data(),
|
||||
cpu_fallback.size()) == 0) {
|
||||
y_data = cpu_fallback.data();
|
||||
uv_data = cpu_fallback.data() + y_size;
|
||||
} else {
|
||||
upload_succeeded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
const GLenum upload_error = glGetError();
|
||||
upload_succeeded = upload_error == GL_NO_ERROR;
|
||||
if (upload_succeeded) {
|
||||
impl_->texture_width = source_width;
|
||||
impl_->texture_height = source_height;
|
||||
impl_->uploaded_stream.assign(remote_id);
|
||||
impl_->uploaded_sequence = sequence;
|
||||
} else {
|
||||
LOG_WARN("OpenGL NV12 texture upload failed, error={}", upload_error);
|
||||
if (should_upload && !use_pixel_unpack_buffer && (!y_data || !uv_data)) {
|
||||
upload_succeeded = false;
|
||||
}
|
||||
|
||||
GLuint upload_y_texture = impl_->y_texture;
|
||||
GLuint upload_uv_texture = impl_->uv_texture;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
if (should_upload && upload_succeeded && use_pixel_unpack_buffer) {
|
||||
auto &cuda_slot = impl_->cuda_upload_slots[cuda_upload_slot];
|
||||
upload_succeeded = impl_->PrepareCudaTextures(cuda_slot);
|
||||
upload_y_texture = cuda_slot.y_texture;
|
||||
upload_uv_texture = cuda_slot.uv_texture;
|
||||
if (!upload_succeeded) {
|
||||
impl_->DisableCudaInterop();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (should_upload && upload_succeeded) {
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, upload_y_texture);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, upload_uv_texture);
|
||||
if (use_pixel_unpack_buffer) {
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
impl_->gl.bind_buffer(
|
||||
GL_PIXEL_UNPACK_BUFFER,
|
||||
impl_->cuda_upload_slots[cuda_upload_slot].buffer);
|
||||
#endif
|
||||
}
|
||||
const void *y_pixels = use_pixel_unpack_buffer
|
||||
? nullptr
|
||||
: static_cast<const void *>(y_data);
|
||||
const void *uv_pixels =
|
||||
use_pixel_unpack_buffer
|
||||
? reinterpret_cast<const void *>(static_cast<uintptr_t>(y_size))
|
||||
: static_cast<const void *>(uv_data);
|
||||
int uploaded_texture_width = impl_->texture_width;
|
||||
int uploaded_texture_height = impl_->texture_height;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
if (use_pixel_unpack_buffer) {
|
||||
uploaded_texture_width =
|
||||
impl_->cuda_upload_slots[cuda_upload_slot].texture_width;
|
||||
uploaded_texture_height =
|
||||
impl_->cuda_upload_slots[cuda_upload_slot].texture_height;
|
||||
}
|
||||
#endif
|
||||
if (uploaded_texture_width != source_width ||
|
||||
uploaded_texture_height != source_height) {
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, source_width,
|
||||
source_height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE,
|
||||
y_pixels);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, source_width / 2,
|
||||
source_height / 2, 0, GL_LUMINANCE_ALPHA,
|
||||
GL_UNSIGNED_BYTE, uv_pixels);
|
||||
} else {
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, source_width, source_height,
|
||||
GL_LUMINANCE, GL_UNSIGNED_BYTE, y_pixels);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, source_width / 2,
|
||||
source_height / 2, GL_LUMINANCE_ALPHA,
|
||||
GL_UNSIGNED_BYTE, uv_pixels);
|
||||
}
|
||||
if (use_pixel_unpack_buffer) {
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
impl_->gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER,
|
||||
static_cast<GLuint>(previous_unpack_buffer));
|
||||
#endif
|
||||
}
|
||||
const GLenum upload_error = glGetError();
|
||||
upload_succeeded = upload_error == GL_NO_ERROR;
|
||||
if (upload_succeeded) {
|
||||
impl_->texture_width = source_width;
|
||||
impl_->texture_height = source_height;
|
||||
impl_->uploaded_stream.assign(remote_id);
|
||||
impl_->uploaded_sequence = sequence;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
if (use_pixel_unpack_buffer) {
|
||||
auto &cuda_slot = impl_->cuda_upload_slots[cuda_upload_slot];
|
||||
cuda_slot.texture_width = source_width;
|
||||
cuda_slot.texture_height = source_height;
|
||||
impl_->displayed_cuda_upload_slot = cuda_upload_slot;
|
||||
} else {
|
||||
impl_->displayed_cuda_upload_slot = kCudaUploadSlotCount;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
LOG_WARN("OpenGL NV12 texture upload failed, error={}", upload_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,12 +1468,22 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
GLuint displayed_y_texture = impl_->y_texture;
|
||||
GLuint displayed_uv_texture = impl_->uv_texture;
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
if (impl_->displayed_cuda_upload_slot < impl_->cuda_upload_slots.size()) {
|
||||
const auto &cuda_slot =
|
||||
impl_->cuda_upload_slots[impl_->displayed_cuda_upload_slot];
|
||||
displayed_y_texture = cuda_slot.y_texture;
|
||||
displayed_uv_texture = cuda_slot.uv_texture;
|
||||
}
|
||||
#endif
|
||||
impl_->gl.use_program(impl_->program);
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->y_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, displayed_y_texture);
|
||||
impl_->gl.uniform_1i(impl_->y_texture_location, 0);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->uv_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, displayed_uv_texture);
|
||||
impl_->gl.uniform_1i(impl_->uv_texture_location, 1);
|
||||
impl_->gl.uniform_2f(impl_->target_size_location,
|
||||
static_cast<GLfloat>(target_width),
|
||||
@@ -858,6 +1510,21 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
impl_->gl.enable_vertex_attrib_array(
|
||||
static_cast<GLuint>(impl_->texcoord_location));
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
if (impl_->displayed_cuda_upload_slot < impl_->cuda_upload_slots.size()) {
|
||||
auto &cuda_slot =
|
||||
impl_->cuda_upload_slots[impl_->displayed_cuda_upload_slot];
|
||||
if (cuda_slot.fence) {
|
||||
impl_->gl.delete_sync(cuda_slot.fence);
|
||||
}
|
||||
cuda_slot.fence =
|
||||
impl_->gl.fence_sync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
if (!cuda_slot.fence) {
|
||||
glFinish();
|
||||
impl_->DisableCudaInterop();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const GLenum draw_error = glGetError();
|
||||
RenderOutcome outcome{
|
||||
!upload_succeeded || draw_error != GL_NO_ERROR
|
||||
@@ -872,6 +1539,10 @@ OpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
texcoord_state);
|
||||
impl_->gl.bind_buffer(GL_ARRAY_BUFFER,
|
||||
static_cast<GLuint>(previous_array_buffer));
|
||||
#if defined(_WIN32) && USE_CUDA
|
||||
impl_->gl.bind_buffer(GL_PIXEL_UNPACK_BUFFER,
|
||||
static_cast<GLuint>(previous_unpack_buffer));
|
||||
#endif
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture_0));
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
@@ -908,21 +1579,38 @@ bool OpenGlVideoRenderer::CopyLatestNv12(
|
||||
if (!output || !width || !height) {
|
||||
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 &&
|
||||
(!newest || slot.sequence > newest->sequence)) {
|
||||
newest = &slot;
|
||||
std::vector<unsigned char> cpu_frame;
|
||||
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 &&
|
||||
(!newest || slot.sequence > newest->sequence)) {
|
||||
newest = &slot;
|
||||
}
|
||||
}
|
||||
if (!newest) {
|
||||
return false;
|
||||
}
|
||||
*width = newest->width;
|
||||
*height = newest->height;
|
||||
cpu_frame = newest->bytes;
|
||||
native_frame = newest->native_frame;
|
||||
}
|
||||
if (!newest || newest->bytes.empty()) {
|
||||
return false;
|
||||
|
||||
if (!cpu_frame.empty()) {
|
||||
*output = std::move(cpu_frame);
|
||||
return true;
|
||||
}
|
||||
*output = newest->bytes;
|
||||
*width = newest->width;
|
||||
*height = newest->height;
|
||||
return true;
|
||||
if (const XNativeVideoFrame *native = native_frame.Get()) {
|
||||
const size_t required_size =
|
||||
static_cast<size_t>(native->width) * native->height * 3U / 2U;
|
||||
output->resize(required_size);
|
||||
return native->copy_to_nv12(native->owner, output->data(),
|
||||
output->size()) == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
namespace crossdesk {
|
||||
|
||||
// NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux.
|
||||
// MiniRTC submits tightly packed CPU NV12 frames from its decode callback
|
||||
// thread. The Slint UI thread uploads the newest frame as Y and UV textures and
|
||||
// performs color conversion and scaling in a fragment shader before Slint
|
||||
// draws its overlay.
|
||||
// MiniRTC submits cross-platform native NV12 descriptors or tightly packed CPU
|
||||
// fallback frames from its decode callback thread. CUDA frames rotate through
|
||||
// fenced OpenGL upload resources; CPU descriptors expose their planes without
|
||||
// an intermediate queue copy. The UI thread always consumes the newest frame.
|
||||
class OpenGlVideoRenderer final : public VideoRenderer {
|
||||
public:
|
||||
OpenGlVideoRenderer();
|
||||
@@ -42,6 +42,8 @@ public:
|
||||
size_t size, int width, int height) override;
|
||||
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
|
||||
size_t size, int width, int height) override;
|
||||
SubmitResult SubmitNativeFrame(std::string_view remote_id,
|
||||
const XNativeVideoFrame &frame) override;
|
||||
|
||||
// Draws below Slint while its OpenGL context is current. All dimensions are
|
||||
// physical pixels in the window client area. corner_radius_pixels clips the
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Platform-independent contract for the native stream video renderer.
|
||||
@@ -56,6 +58,12 @@ public:
|
||||
virtual SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
||||
const uint8_t* data, size_t size,
|
||||
int width, int height) = 0;
|
||||
// Retains a platform-native decoded frame when the backend supports its
|
||||
// storage and handle types. Unsupported backends keep the default fallback.
|
||||
virtual SubmitResult SubmitNativeFrame(std::string_view,
|
||||
const XNativeVideoFrame&) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
|
||||
virtual bool CopyLatestNv12(std::string_view remote_id,
|
||||
std::vector<unsigned char>* output, int* width,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1124,12 +1124,17 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
static_cast<size_t>(_selectedDisplay.load()));
|
||||
if (source != expected) return;
|
||||
}
|
||||
const XNativeVideoFrame *native_frame = frame->native_frame;
|
||||
const bool has_native_pixel_buffer =
|
||||
frame->native_handle &&
|
||||
frame->native_handle_type == XVideoFrameNativeHandleCVPixelBuffer;
|
||||
native_frame &&
|
||||
native_frame->struct_size >=
|
||||
static_cast<uint32_t>(sizeof(XNativeVideoFrame)) &&
|
||||
native_frame->type == XNativeVideoFrameCVPixelBuffer &&
|
||||
native_frame->payload.cv_pixel_buffer;
|
||||
if (has_native_pixel_buffer) {
|
||||
CVPixelBufferRef pixel_buffer =
|
||||
static_cast<CVPixelBufferRef>(frame->native_handle);
|
||||
static_cast<CVPixelBufferRef>(
|
||||
native_frame->payload.cv_pixel_buffer);
|
||||
[self enqueueVideoPixelBuffer:pixel_buffer
|
||||
width:frame->width
|
||||
height:frame->height];
|
||||
|
||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: a1d221da96...454a0c544a
Reference in New Issue
Block a user