mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-23 21:23:12 +08:00
Compare commits
10
Commits
e38c9f2ae9
...
2186ebd7eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2186ebd7eb | ||
|
|
c11d48fc1f | ||
|
|
1690fc92a3 | ||
|
|
0d9d385d10 | ||
|
|
20f8137df2 | ||
|
|
29aae747c6 | ||
|
|
3f93dc05aa | ||
|
|
05885ecd48 | ||
|
|
ee92c4ce5f | ||
|
|
d94443330c |
@@ -117,7 +117,8 @@ sudo apt-get install -y \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
|
||||
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
|
||||
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
|
||||
libasound2-dev libsndio-dev libpulse-dev
|
||||
libasound2-dev libsndio-dev libpulse-dev \
|
||||
libgl1-mesa-dev
|
||||
```
|
||||
|
||||
Ubuntu 20.04 自带的 CMake 3.16 不满足要求时,xmake 会自动下载并使用
|
||||
|
||||
+2
-1
@@ -120,7 +120,8 @@ sudo apt-get install -y \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
|
||||
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
|
||||
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
|
||||
libasound2-dev libsndio-dev libpulse-dev
|
||||
libasound2-dev libsndio-dev libpulse-dev \
|
||||
libgl1-mesa-dev
|
||||
```
|
||||
|
||||
When Ubuntu 20.04's bundled CMake 3.16 is too old, xmake automatically
|
||||
|
||||
@@ -160,7 +160,7 @@ Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
|
||||
libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
|
||||
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
|
||||
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
|
||||
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
|
||||
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
|
||||
Recommends: $RECOMMENDS
|
||||
Priority: optional
|
||||
Section: utils
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "nv12_scaler.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
|
||||
#include "libyuv/planar_functions.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int ScaleNv12ViaI420(const uint8_t* src_y, int src_stride_y,
|
||||
const uint8_t* src_uv, int src_stride_uv, int src_width,
|
||||
int src_height, uint8_t* dst_y, int dst_stride_y,
|
||||
uint8_t* dst_uv, int dst_stride_uv, int dst_width,
|
||||
int dst_height, libyuv::FilterMode filtering,
|
||||
std::vector<uint8_t>* scratch_buffer) {
|
||||
if (!src_y || !src_uv || !dst_y || !dst_uv || !scratch_buffer ||
|
||||
src_width <= 0 || src_height <= 0 || dst_width <= 0 || dst_height <= 0 ||
|
||||
(src_width & 1) != 0 || (src_height & 1) != 0 || (dst_width & 1) != 0 ||
|
||||
(dst_height & 1) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int src_chroma_width = src_width / 2;
|
||||
const int src_chroma_height = src_height / 2;
|
||||
const int dst_chroma_width = dst_width / 2;
|
||||
const int dst_chroma_height = dst_height / 2;
|
||||
const size_t src_chroma_size =
|
||||
static_cast<size_t>(src_chroma_width) * src_chroma_height;
|
||||
const size_t dst_chroma_size =
|
||||
static_cast<size_t>(dst_chroma_width) * dst_chroma_height;
|
||||
const size_t max_size = std::numeric_limits<size_t>::max();
|
||||
if (src_chroma_size > max_size / 2 || dst_chroma_size > max_size / 2) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const size_t src_planes_size = 2 * src_chroma_size;
|
||||
const size_t dst_planes_size = 2 * dst_chroma_size;
|
||||
if (src_planes_size > max_size - dst_planes_size) {
|
||||
return -1;
|
||||
}
|
||||
scratch_buffer->resize(src_planes_size + dst_planes_size);
|
||||
uint8_t* src_u = scratch_buffer->data();
|
||||
uint8_t* src_v = src_u + src_chroma_size;
|
||||
uint8_t* dst_u = src_v + src_chroma_size;
|
||||
uint8_t* dst_v = dst_u + dst_chroma_size;
|
||||
|
||||
libyuv::SplitUVPlane(src_uv, src_stride_uv, src_u, src_chroma_width, src_v,
|
||||
src_chroma_width, src_chroma_width, src_chroma_height);
|
||||
const int result = libyuv::I420Scale(
|
||||
src_y, src_stride_y, src_u, src_chroma_width, src_v, src_chroma_width,
|
||||
src_width, src_height, dst_y, dst_stride_y, dst_u, dst_chroma_width,
|
||||
dst_v, dst_chroma_width, dst_width, dst_height, filtering);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
libyuv::MergeUVPlane(dst_u, dst_chroma_width, dst_v, dst_chroma_width, dst_uv,
|
||||
dst_stride_uv, dst_chroma_width, dst_chroma_height);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef CROSSDESK_COMMON_NV12_SCALER_H_
|
||||
#define CROSSDESK_COMMON_NV12_SCALER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "libyuv/scale.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Scales NV12 by separating the interleaved chroma plane before filtering.
|
||||
// This avoids the downward chroma rounding bias in libyuv::NV12Scale while
|
||||
// preserving the requested filtering quality.
|
||||
int ScaleNv12ViaI420(const uint8_t* src_y, int src_stride_y,
|
||||
const uint8_t* src_uv, int src_stride_uv, int src_width,
|
||||
int src_height, uint8_t* dst_y, int dst_stride_y,
|
||||
uint8_t* dst_uv, int dst_stride_uv, int dst_width,
|
||||
int dst_height, libyuv::FilterMode filtering,
|
||||
std::vector<uint8_t>* scratch_buffer);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_COMMON_NV12_SCALER_H_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -37,6 +38,17 @@ private:
|
||||
void SyncConnectionDialog();
|
||||
void SyncPlatformDialogs();
|
||||
void SyncStreamWindow();
|
||||
void SyncStreamVideoFrame();
|
||||
void ScheduleNextVideoFrame();
|
||||
void ConfigureStreamVideoRenderer();
|
||||
#if defined(_WIN32)
|
||||
void SubmitCachedFrameToOpenGl(
|
||||
const std::shared_ptr<RemoteSession>& session);
|
||||
#endif
|
||||
#if defined(__APPLE__)
|
||||
void SubmitCachedFrameToMetal(
|
||||
const std::shared_ptr<RemoteSession>& session);
|
||||
#endif
|
||||
void SyncStreamKeyboardFocus();
|
||||
void SetStreamKeyboardFocus(bool focused);
|
||||
void SyncServerWindow();
|
||||
@@ -66,6 +78,7 @@ private:
|
||||
bool OpenUrl(const std::string &url);
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
std::chrono::steady_clock::time_point next_video_frame_time_{};
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
bool use_xwayland_gui_ = false;
|
||||
bool use_x11_custom_titlebar_ = false;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk::server_window_state {
|
||||
|
||||
inline int ReconcileSelectedController(const std::vector<std::string>& ids,
|
||||
std::string* selected_id) {
|
||||
if (!selected_id) {
|
||||
return 0;
|
||||
}
|
||||
if (ids.empty()) {
|
||||
selected_id->clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto selected = std::find(ids.begin(), ids.end(), *selected_id);
|
||||
if (selected == ids.end()) {
|
||||
*selected_id = ids.front();
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int>(std::distance(ids.begin(), selected));
|
||||
}
|
||||
|
||||
} // namespace crossdesk::server_window_state
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace crossdesk::window_geometry {
|
||||
|
||||
struct PhysicalRect {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
struct PhysicalSize {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
struct PhysicalPosition {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
};
|
||||
|
||||
constexpr PhysicalPosition CenteredPosition(const PhysicalRect& bounds,
|
||||
const PhysicalSize& size) {
|
||||
return {
|
||||
bounds.x + (bounds.width - std::min(bounds.width, size.width)) / 2,
|
||||
bounds.y + (bounds.height - std::min(bounds.height, size.height)) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
|
||||
const PhysicalSize& size) {
|
||||
return {
|
||||
bounds.x + std::max(0, bounds.width - size.width),
|
||||
bounds.y + std::max(0, bounds.height - size.height),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace crossdesk::window_geometry
|
||||
@@ -13,11 +13,29 @@ namespace {
|
||||
|
||||
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
||||
constexpr size_t kMaxCapturedKeyboardInputs = 512;
|
||||
constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1);
|
||||
|
||||
} // namespace
|
||||
|
||||
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
bool SessionDeviceManager::ShouldSendCapturedFrame(
|
||||
std::chrono::steady_clock::time_point now, int fps) {
|
||||
const auto interval =
|
||||
std::chrono::nanoseconds(std::chrono::seconds(1)) / (fps > 0 ? fps : 1);
|
||||
if (next_frame_deadline_ == std::chrono::steady_clock::time_point{}) {
|
||||
next_frame_deadline_ = now;
|
||||
}
|
||||
if (now + kFrameDeadlineTolerance < next_frame_deadline_) {
|
||||
return false;
|
||||
}
|
||||
next_frame_deadline_ += interval;
|
||||
if (next_frame_deadline_ <= now) {
|
||||
next_frame_deadline_ = now + interval;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SessionDeviceManager::Initialize() {
|
||||
InitializeAudioOutput();
|
||||
screen_capturer_factory_ = new ScreenCapturerFactory();
|
||||
@@ -40,9 +58,8 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
|
||||
}
|
||||
|
||||
last_frame_time_ = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
last_frame_time_ = {};
|
||||
next_frame_deadline_ = {};
|
||||
const int fps = owner_.config_center_->GetVideoFrameRate() ==
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
@@ -56,15 +73,20 @@ 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 auto now_time = static_cast<uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count());
|
||||
const auto duration = now_time - last_frame_time_;
|
||||
if (duration * fps < 1000) {
|
||||
const auto now_time = std::chrono::steady_clock::now();
|
||||
if (!ShouldSendCapturedFrame(now_time, fps)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool has_previous_frame =
|
||||
last_frame_time_.time_since_epoch().count() != 0;
|
||||
const auto duration_ms =
|
||||
has_previous_frame
|
||||
? std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now_time - last_frame_time_)
|
||||
.count()
|
||||
: 0;
|
||||
|
||||
std::vector<std::string> connected_remote_ids;
|
||||
{
|
||||
std::shared_lock lock(owner_.connection_status_mutex_);
|
||||
@@ -101,14 +123,14 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
}
|
||||
invalid_video_stream_id_logged_ = false;
|
||||
const bool resumed_after_gap =
|
||||
last_frame_time_ != 0 && duration >= kCaptureResumeKeyFrameGapMs;
|
||||
has_previous_frame && duration_ms >= kCaptureResumeKeyFrameGapMs;
|
||||
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
|
||||
last_video_frame_stream_id_ != stream_id;
|
||||
if (resumed_after_gap || stream_changed) {
|
||||
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
|
||||
LOG_INFO("Request video key frame before sending captured frame, "
|
||||
"stream='{}', gap_ms={}, stream_changed={}",
|
||||
stream_id, duration, stream_changed);
|
||||
stream_id, duration_ms, stream_changed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
@@ -66,6 +67,8 @@ private:
|
||||
uint32_t scan_code, bool extended);
|
||||
void DrainCapturedKeyboardInput();
|
||||
void ClearCapturedKeyboardInput();
|
||||
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
|
||||
int fps);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
SDL_AudioStream *output_stream_ = nullptr;
|
||||
@@ -80,7 +83,8 @@ private:
|
||||
size_t registered_display_stream_count_ = 0;
|
||||
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
|
||||
std::mutex captured_keyboard_inputs_mutex_;
|
||||
uint64_t last_frame_time_ = 0;
|
||||
std::chrono::steady_clock::time_point last_frame_time_{};
|
||||
std::chrono::steady_clock::time_point next_frame_deadline_{};
|
||||
std::string last_video_frame_stream_id_;
|
||||
bool invalid_video_stream_id_logged_ = false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#ifndef CROSSDESK_GUI_PLATFORM_METAL_VIDEO_RENDERER_H_
|
||||
#define CROSSDESK_GUI_PLATFORM_METAL_VIDEO_RENDERER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
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.
|
||||
class MacMetalVideoRenderer {
|
||||
public:
|
||||
enum class SubmitResult {
|
||||
submitted,
|
||||
not_selected,
|
||||
dropped,
|
||||
failed,
|
||||
};
|
||||
|
||||
enum class RenderResult {
|
||||
rendered,
|
||||
idle,
|
||||
empty,
|
||||
failed,
|
||||
};
|
||||
|
||||
struct RenderOutcome {
|
||||
RenderResult result = RenderResult::failed;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
MacMetalVideoRenderer();
|
||||
~MacMetalVideoRenderer();
|
||||
|
||||
MacMetalVideoRenderer(const MacMetalVideoRenderer&) = delete;
|
||||
MacMetalVideoRenderer& operator=(const MacMetalVideoRenderer&) = delete;
|
||||
|
||||
bool IsReady() const;
|
||||
|
||||
// The selected stream is the only stream uploaded. Frames for background
|
||||
// tabs are intentionally dropped to avoid wasting memory bandwidth.
|
||||
bool SetSelectedStream(std::string remote_id);
|
||||
void DiscardStream(std::string_view remote_id);
|
||||
|
||||
// Copies one tightly packed NV12 frame into a free shared Metal slot.
|
||||
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t* data,
|
||||
size_t size, int width, int height);
|
||||
|
||||
// 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,
|
||||
const uint8_t* data, size_t size, int width,
|
||||
int height);
|
||||
|
||||
// Attaches a native CAMetalLayer-backed sibling below Slint's NSView and
|
||||
// restores the containing AppKit window's ordinary opaque titlebar.
|
||||
// |slint_view| is an NSView* kept opaque so this header remains valid C++.
|
||||
bool Attach(void* slint_view);
|
||||
void Detach();
|
||||
bool IsAttached() const;
|
||||
|
||||
// Cheap main-thread geometry check used by the UI frame timer. New decoded
|
||||
// frames are tracked by GuiRuntime's dirty flag; this covers native resize,
|
||||
// scale, stream selection, and retry work without a full session sync.
|
||||
bool NeedsSurfaceRedraw() const;
|
||||
|
||||
// Draws the newest queued frame for |remote_id|. top_inset_points reserves
|
||||
// Slint's tab strip above the native video surface. A rendered outcome
|
||||
// carries metadata from the exact slot submitted to the command buffer.
|
||||
RenderOutcome RenderLatest(std::string_view remote_id,
|
||||
double top_inset_points,
|
||||
bool native_titlebar_visible);
|
||||
|
||||
// Used only when a session closes, so thumbnail generation does not require
|
||||
// a second per-frame CPU copy during normal playback.
|
||||
bool CopyLatestNv12(std::string_view remote_id,
|
||||
std::vector<unsigned char>* output, int* width,
|
||||
int* height) const;
|
||||
|
||||
private:
|
||||
SubmitResult SubmitNv12Internal(std::string_view remote_id,
|
||||
const uint8_t* data, size_t size, int width,
|
||||
int height, bool replace_pending);
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_PLATFORM_METAL_VIDEO_RENDERER_H_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,14 @@ bool HideDisabledMainWindowZoomButton();
|
||||
|
||||
// Configures live resize and replaces the stream window's native Space
|
||||
// fullscreen action with an immediate, single-window fullscreen transition.
|
||||
bool ConfigureStreamWindowLiveResize();
|
||||
// |slint_view| is the current stream window's AppKit NSView*. Passing the
|
||||
// identity explicitly prevents a closing window with the same title from
|
||||
// being selected during a later connection.
|
||||
bool ConfigureStreamWindowLiveResize(void* slint_view);
|
||||
|
||||
// Drops the native stream-window reference if it belongs to |slint_view|.
|
||||
// This must run before the corresponding Slint component is destroyed.
|
||||
void UnregisterStreamWindow(void* slint_view);
|
||||
|
||||
// Returns whether the configured stream window is currently the active AppKit
|
||||
// key window.
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
NSWindow *stream_window = nil;
|
||||
__weak NSWindow *stream_window = nil;
|
||||
__weak NSView *stream_slint_view = nil;
|
||||
CrossDeskStreamFullscreenTarget *stream_fullscreen_target = nil;
|
||||
bool stream_fullscreen = false;
|
||||
NSRect stream_restore_frame = NSZeroRect;
|
||||
@@ -101,48 +102,63 @@ bool HideDisabledMainWindowZoomButton() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigureStreamWindowLiveResize() {
|
||||
bool ConfigureStreamWindowLiveResize(void *opaque_slint_view) {
|
||||
@autoreleasepool {
|
||||
for (NSWindow *window in [NSApp windows]) {
|
||||
if (![window.title isEqualToString:@"CrossDesk"]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSButton *zoom_button =
|
||||
[window standardWindowButton:NSWindowZoomButton];
|
||||
// The stream window is resizable and therefore keeps an enabled native
|
||||
// zoom button. The fixed-size main window's zoom button is disabled.
|
||||
if (zoom_button == nil || !zoom_button.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSView *content_view = window.contentView;
|
||||
if (content_view == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// During an ordinary edge resize, scale one cached frame and redraw once
|
||||
// the new window size has settled.
|
||||
window.preservesContentDuringLiveResize = YES;
|
||||
content_view.layerContentsRedrawPolicy =
|
||||
NSViewLayerContentsRedrawBeforeViewResize;
|
||||
content_view.layerContentsPlacement =
|
||||
NSViewLayerContentsPlacementScaleProportionallyToFit;
|
||||
stream_window = window;
|
||||
// Native Space fullscreen cross-fades an AppKit source snapshot over a
|
||||
// separately rendered destination window. Disable that path for the
|
||||
// stream window and route the green button to the immediate transition.
|
||||
window.collectionBehavior =
|
||||
(window.collectionBehavior &
|
||||
~(NSWindowCollectionBehaviorFullScreenPrimary |
|
||||
NSWindowCollectionBehaviorFullScreenAuxiliary |
|
||||
NSWindowCollectionBehaviorFullScreenAllowsTiling)) |
|
||||
NSWindowCollectionBehaviorFullScreenNone;
|
||||
InstallStreamFullscreenButton(window);
|
||||
content_view.needsDisplay = YES;
|
||||
return true;
|
||||
NSView *slint_view = (__bridge NSView *)opaque_slint_view;
|
||||
NSWindow *window = slint_view.window;
|
||||
if (window == nil) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
NSButton *zoom_button =
|
||||
[window standardWindowButton:NSWindowZoomButton];
|
||||
NSView *content_view = window.contentView;
|
||||
if (zoom_button == nil || !zoom_button.enabled || content_view == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// During an ordinary edge resize, scale one cached frame and redraw once
|
||||
// the new window size has settled.
|
||||
window.preservesContentDuringLiveResize = YES;
|
||||
content_view.layerContentsRedrawPolicy =
|
||||
NSViewLayerContentsRedrawBeforeViewResize;
|
||||
content_view.layerContentsPlacement =
|
||||
NSViewLayerContentsPlacementScaleProportionallyToFit;
|
||||
stream_window = window;
|
||||
stream_slint_view = slint_view;
|
||||
// Native Space fullscreen cross-fades an AppKit source snapshot over a
|
||||
// separately rendered destination window. Disable that path for the
|
||||
// stream window and route the green button to the immediate transition.
|
||||
window.collectionBehavior =
|
||||
(window.collectionBehavior &
|
||||
~(NSWindowCollectionBehaviorFullScreenPrimary |
|
||||
NSWindowCollectionBehaviorFullScreenAuxiliary |
|
||||
NSWindowCollectionBehaviorFullScreenAllowsTiling)) |
|
||||
NSWindowCollectionBehaviorFullScreenNone;
|
||||
InstallStreamFullscreenButton(window);
|
||||
content_view.needsDisplay = YES;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void UnregisterStreamWindow(void *opaque_slint_view) {
|
||||
@autoreleasepool {
|
||||
NSView *slint_view = (__bridge NSView *)opaque_slint_view;
|
||||
if (slint_view == nil || slint_view != stream_slint_view) {
|
||||
return;
|
||||
}
|
||||
stream_window = nil;
|
||||
stream_slint_view = nil;
|
||||
stream_fullscreen = false;
|
||||
stream_restore_frame = NSZeroRect;
|
||||
stream_restore_style_mask = NSWindowStyleMaskTitled;
|
||||
stream_restore_title_visibility = NSWindowTitleVisible;
|
||||
stream_restore_titlebar_transparent = NO;
|
||||
stream_restore_movable = YES;
|
||||
stream_restore_close_hidden = NO;
|
||||
stream_restore_miniaturize_hidden = NO;
|
||||
stream_restore_zoom_hidden = NO;
|
||||
stream_restore_presentation_options = NSApplicationPresentationDefault;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,812 @@
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#error "WindowsOpenGlVideoRenderer is only available on Windows"
|
||||
#endif
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glext.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kFrameSlotCount = 3;
|
||||
|
||||
enum class SlotUse {
|
||||
available,
|
||||
pending,
|
||||
uploading,
|
||||
};
|
||||
|
||||
struct FrameSlot {
|
||||
std::vector<unsigned char> bytes;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::string remote_id;
|
||||
uint64_t sequence = 0;
|
||||
SlotUse use = SlotUse::available;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct SharedFrameState {
|
||||
mutable std::mutex mutex;
|
||||
std::array<FrameSlot, kFrameSlotCount> slots;
|
||||
std::string selected_stream;
|
||||
uint64_t next_sequence = 1;
|
||||
};
|
||||
|
||||
template <typename T> bool LoadOpenGlFunction(T *function, const char *name) {
|
||||
PROC address = wglGetProcAddress(name);
|
||||
if (address == nullptr || address == reinterpret_cast<PROC>(1) ||
|
||||
address == reinterpret_cast<PROC>(2) ||
|
||||
address == reinterpret_cast<PROC>(3) ||
|
||||
address == reinterpret_cast<PROC>(-1)) {
|
||||
HMODULE module = GetModuleHandleW(L"opengl32.dll");
|
||||
address = module ? GetProcAddress(module, name) : nullptr;
|
||||
}
|
||||
*function = reinterpret_cast<T>(address);
|
||||
return *function != nullptr;
|
||||
}
|
||||
|
||||
struct OpenGlFunctions {
|
||||
PFNGLACTIVETEXTUREPROC active_texture = nullptr;
|
||||
PFNGLATTACHSHADERPROC attach_shader = nullptr;
|
||||
PFNGLBINDBUFFERPROC bind_buffer = nullptr;
|
||||
PFNGLBUFFERDATAPROC buffer_data = nullptr;
|
||||
PFNGLCOMPILESHADERPROC compile_shader = nullptr;
|
||||
PFNGLCREATEPROGRAMPROC create_program = nullptr;
|
||||
PFNGLCREATESHADERPROC create_shader = nullptr;
|
||||
PFNGLDELETEBUFFERSPROC delete_buffers = nullptr;
|
||||
PFNGLDELETEPROGRAMPROC delete_program = nullptr;
|
||||
PFNGLDELETESHADERPROC delete_shader = nullptr;
|
||||
PFNGLDISABLEVERTEXATTRIBARRAYPROC disable_vertex_attrib_array = nullptr;
|
||||
PFNGLENABLEVERTEXATTRIBARRAYPROC enable_vertex_attrib_array = nullptr;
|
||||
PFNGLGENBUFFERSPROC gen_buffers = nullptr;
|
||||
PFNGLGETATTRIBLOCATIONPROC get_attrib_location = nullptr;
|
||||
PFNGLGETPROGRAMINFOLOGPROC get_program_info_log = nullptr;
|
||||
PFNGLGETPROGRAMIVPROC get_program_iv = nullptr;
|
||||
PFNGLGETSHADERINFOLOGPROC get_shader_info_log = nullptr;
|
||||
PFNGLGETSHADERIVPROC get_shader_iv = nullptr;
|
||||
PFNGLGETUNIFORMLOCATIONPROC get_uniform_location = nullptr;
|
||||
PFNGLGETVERTEXATTRIBIVPROC get_vertex_attrib_iv = nullptr;
|
||||
PFNGLGETVERTEXATTRIBPOINTERVPROC get_vertex_attrib_pointer_v = nullptr;
|
||||
PFNGLLINKPROGRAMPROC link_program = nullptr;
|
||||
PFNGLSHADERSOURCEPROC shader_source = nullptr;
|
||||
PFNGLUNIFORM1IPROC uniform_1i = nullptr;
|
||||
PFNGLUSEPROGRAMPROC use_program = nullptr;
|
||||
PFNGLVERTEXATTRIBPOINTERPROC vertex_attrib_pointer = nullptr;
|
||||
|
||||
bool Load() {
|
||||
return LoadOpenGlFunction(&active_texture, "glActiveTexture") &&
|
||||
LoadOpenGlFunction(&attach_shader, "glAttachShader") &&
|
||||
LoadOpenGlFunction(&bind_buffer, "glBindBuffer") &&
|
||||
LoadOpenGlFunction(&buffer_data, "glBufferData") &&
|
||||
LoadOpenGlFunction(&compile_shader, "glCompileShader") &&
|
||||
LoadOpenGlFunction(&create_program, "glCreateProgram") &&
|
||||
LoadOpenGlFunction(&create_shader, "glCreateShader") &&
|
||||
LoadOpenGlFunction(&delete_buffers, "glDeleteBuffers") &&
|
||||
LoadOpenGlFunction(&delete_program, "glDeleteProgram") &&
|
||||
LoadOpenGlFunction(&delete_shader, "glDeleteShader") &&
|
||||
LoadOpenGlFunction(&disable_vertex_attrib_array,
|
||||
"glDisableVertexAttribArray") &&
|
||||
LoadOpenGlFunction(&enable_vertex_attrib_array,
|
||||
"glEnableVertexAttribArray") &&
|
||||
LoadOpenGlFunction(&gen_buffers, "glGenBuffers") &&
|
||||
LoadOpenGlFunction(&get_attrib_location, "glGetAttribLocation") &&
|
||||
LoadOpenGlFunction(&get_program_info_log, "glGetProgramInfoLog") &&
|
||||
LoadOpenGlFunction(&get_program_iv, "glGetProgramiv") &&
|
||||
LoadOpenGlFunction(&get_shader_info_log, "glGetShaderInfoLog") &&
|
||||
LoadOpenGlFunction(&get_shader_iv, "glGetShaderiv") &&
|
||||
LoadOpenGlFunction(&get_uniform_location, "glGetUniformLocation") &&
|
||||
LoadOpenGlFunction(&get_vertex_attrib_iv, "glGetVertexAttribiv") &&
|
||||
LoadOpenGlFunction(&get_vertex_attrib_pointer_v,
|
||||
"glGetVertexAttribPointerv") &&
|
||||
LoadOpenGlFunction(&link_program, "glLinkProgram") &&
|
||||
LoadOpenGlFunction(&shader_source, "glShaderSource") &&
|
||||
LoadOpenGlFunction(&uniform_1i, "glUniform1i") &&
|
||||
LoadOpenGlFunction(&use_program, "glUseProgram") &&
|
||||
LoadOpenGlFunction(&vertex_attrib_pointer, "glVertexAttribPointer");
|
||||
}
|
||||
};
|
||||
|
||||
GLuint CompileShader(const OpenGlFunctions &gl, GLenum type,
|
||||
const char *source) {
|
||||
const GLuint shader = gl.create_shader(type);
|
||||
if (shader == 0) {
|
||||
return 0;
|
||||
}
|
||||
gl.shader_source(shader, 1, &source, nullptr);
|
||||
gl.compile_shader(shader);
|
||||
|
||||
GLint compiled = GL_FALSE;
|
||||
gl.get_shader_iv(shader, GL_COMPILE_STATUS, &compiled);
|
||||
if (compiled == GL_TRUE) {
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLint log_length = 0;
|
||||
gl.get_shader_iv(shader, GL_INFO_LOG_LENGTH, &log_length);
|
||||
std::string log(static_cast<size_t>(std::max(log_length, 1)), '\0');
|
||||
GLsizei written = 0;
|
||||
gl.get_shader_info_log(shader, static_cast<GLsizei>(log.size()), &written,
|
||||
log.data());
|
||||
if (written >= 0 && static_cast<size_t>(written) < log.size()) {
|
||||
log.resize(static_cast<size_t>(written));
|
||||
}
|
||||
LOG_ERROR("OpenGL NV12 {} shader compilation failed: {}",
|
||||
type == GL_VERTEX_SHADER ? "vertex" : "fragment", log);
|
||||
gl.delete_shader(shader);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool IsEnabled(GLenum capability) { return glIsEnabled(capability) == GL_TRUE; }
|
||||
|
||||
void RestoreCapability(GLenum capability, bool enabled) {
|
||||
if (enabled) {
|
||||
glEnable(capability);
|
||||
} else {
|
||||
glDisable(capability);
|
||||
}
|
||||
}
|
||||
|
||||
struct VertexAttribState {
|
||||
GLint enabled = GL_FALSE;
|
||||
GLint size = 4;
|
||||
GLint stride = 0;
|
||||
GLint type = GL_FLOAT;
|
||||
GLint normalized = GL_FALSE;
|
||||
GLint buffer = 0;
|
||||
void *pointer = nullptr;
|
||||
};
|
||||
|
||||
VertexAttribState CaptureVertexAttrib(const OpenGlFunctions &gl, GLuint index) {
|
||||
VertexAttribState state;
|
||||
gl.get_vertex_attrib_iv(index, GL_VERTEX_ATTRIB_ARRAY_ENABLED,
|
||||
&state.enabled);
|
||||
gl.get_vertex_attrib_iv(index, GL_VERTEX_ATTRIB_ARRAY_SIZE, &state.size);
|
||||
gl.get_vertex_attrib_iv(index, GL_VERTEX_ATTRIB_ARRAY_STRIDE, &state.stride);
|
||||
gl.get_vertex_attrib_iv(index, GL_VERTEX_ATTRIB_ARRAY_TYPE, &state.type);
|
||||
gl.get_vertex_attrib_iv(index, GL_VERTEX_ATTRIB_ARRAY_NORMALIZED,
|
||||
&state.normalized);
|
||||
gl.get_vertex_attrib_iv(index, GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING,
|
||||
&state.buffer);
|
||||
gl.get_vertex_attrib_pointer_v(index, GL_VERTEX_ATTRIB_ARRAY_POINTER,
|
||||
&state.pointer);
|
||||
return state;
|
||||
}
|
||||
|
||||
void RestoreVertexAttrib(const OpenGlFunctions &gl, GLuint index,
|
||||
const VertexAttribState &state) {
|
||||
if (state.buffer != 0 || state.pointer != nullptr) {
|
||||
gl.bind_buffer(GL_ARRAY_BUFFER, static_cast<GLuint>(state.buffer));
|
||||
gl.vertex_attrib_pointer(index, state.size, static_cast<GLenum>(state.type),
|
||||
static_cast<GLboolean>(state.normalized),
|
||||
state.stride, state.pointer);
|
||||
}
|
||||
if (state.enabled == GL_TRUE) {
|
||||
gl.enable_vertex_attrib_array(index);
|
||||
} else {
|
||||
gl.disable_vertex_attrib_array(index);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct WindowsOpenGlVideoRenderer::Impl {
|
||||
OpenGlFunctions gl;
|
||||
std::shared_ptr<SharedFrameState> frames =
|
||||
std::make_shared<SharedFrameState>();
|
||||
std::atomic<bool> ready{false};
|
||||
GLuint program = 0;
|
||||
GLuint vertex_buffer = 0;
|
||||
GLuint y_texture = 0;
|
||||
GLuint uv_texture = 0;
|
||||
GLint position_location = -1;
|
||||
GLint texcoord_location = -1;
|
||||
GLint y_texture_location = -1;
|
||||
GLint uv_texture_location = -1;
|
||||
int texture_width = 0;
|
||||
int texture_height = 0;
|
||||
std::string uploaded_stream;
|
||||
uint64_t uploaded_sequence = 0;
|
||||
|
||||
void ResetGlHandles() {
|
||||
program = 0;
|
||||
vertex_buffer = 0;
|
||||
y_texture = 0;
|
||||
uv_texture = 0;
|
||||
position_location = -1;
|
||||
texcoord_location = -1;
|
||||
y_texture_location = -1;
|
||||
uv_texture_location = -1;
|
||||
texture_width = 0;
|
||||
texture_height = 0;
|
||||
uploaded_stream.clear();
|
||||
uploaded_sequence = 0;
|
||||
}
|
||||
};
|
||||
|
||||
WindowsOpenGlVideoRenderer::WindowsOpenGlVideoRenderer()
|
||||
: impl_(std::make_unique<Impl>()) {}
|
||||
|
||||
WindowsOpenGlVideoRenderer::~WindowsOpenGlVideoRenderer() = default;
|
||||
|
||||
bool WindowsOpenGlVideoRenderer::Setup() {
|
||||
if (IsReady()) {
|
||||
return true;
|
||||
}
|
||||
impl_->ready.store(false, std::memory_order_release);
|
||||
impl_->ResetGlHandles();
|
||||
if (!impl_->gl.Load()) {
|
||||
LOG_WARN("OpenGL NV12 underlay unavailable: required functions missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
static constexpr char kGlesVertexShader[] = R"glsl(
|
||||
#version 100
|
||||
attribute vec2 position;
|
||||
attribute vec2 texcoord;
|
||||
varying vec2 video_texcoord;
|
||||
void main() {
|
||||
video_texcoord = texcoord;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
)glsl";
|
||||
static constexpr char kGlesFragmentShader[] = R"glsl(
|
||||
#version 100
|
||||
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
||||
precision highp float;
|
||||
#else
|
||||
precision mediump float;
|
||||
#endif
|
||||
uniform sampler2D y_texture;
|
||||
uniform sampler2D uv_texture;
|
||||
varying vec2 video_texcoord;
|
||||
void main() {
|
||||
float y = 1.16438356 *
|
||||
(texture2D(y_texture, video_texcoord).r - 16.0 / 255.0);
|
||||
vec2 uv = texture2D(uv_texture, video_texcoord).ra - vec2(0.5, 0.5);
|
||||
vec3 rgb = vec3(y + 1.59602678 * uv.y,
|
||||
y - 0.39176229 * uv.x - 0.81296764 * uv.y,
|
||||
y + 2.01723214 * uv.x);
|
||||
gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0);
|
||||
}
|
||||
)glsl";
|
||||
static constexpr char kDesktopVertexShader[] = R"glsl(
|
||||
#version 110
|
||||
attribute vec2 position;
|
||||
attribute vec2 texcoord;
|
||||
varying vec2 video_texcoord;
|
||||
void main() {
|
||||
video_texcoord = texcoord;
|
||||
gl_Position = vec4(position, 0.0, 1.0);
|
||||
}
|
||||
)glsl";
|
||||
static constexpr char kDesktopFragmentShader[] = R"glsl(
|
||||
#version 110
|
||||
uniform sampler2D y_texture;
|
||||
uniform sampler2D uv_texture;
|
||||
varying vec2 video_texcoord;
|
||||
void main() {
|
||||
float y = 1.16438356 *
|
||||
(texture2D(y_texture, video_texcoord).r - 16.0 / 255.0);
|
||||
vec2 uv = texture2D(uv_texture, video_texcoord).ra - vec2(0.5, 0.5);
|
||||
vec3 rgb = vec3(y + 1.59602678 * uv.y,
|
||||
y - 0.39176229 * uv.x - 0.81296764 * uv.y,
|
||||
y + 2.01723214 * uv.x);
|
||||
gl_FragColor = vec4(clamp(rgb, 0.0, 1.0), 1.0);
|
||||
}
|
||||
)glsl";
|
||||
|
||||
const auto *version = reinterpret_cast<const char *>(glGetString(GL_VERSION));
|
||||
const bool is_gles = version && std::strstr(version, "OpenGL ES") != nullptr;
|
||||
const char *vertex_source =
|
||||
is_gles ? kGlesVertexShader : kDesktopVertexShader;
|
||||
const char *fragment_source =
|
||||
is_gles ? kGlesFragmentShader : kDesktopFragmentShader;
|
||||
|
||||
const GLuint vertex_shader =
|
||||
CompileShader(impl_->gl, GL_VERTEX_SHADER, vertex_source);
|
||||
const GLuint fragment_shader =
|
||||
CompileShader(impl_->gl, GL_FRAGMENT_SHADER, fragment_source);
|
||||
if (vertex_shader == 0 || fragment_shader == 0) {
|
||||
if (vertex_shader != 0)
|
||||
impl_->gl.delete_shader(vertex_shader);
|
||||
if (fragment_shader != 0)
|
||||
impl_->gl.delete_shader(fragment_shader);
|
||||
return false;
|
||||
}
|
||||
|
||||
impl_->program = impl_->gl.create_program();
|
||||
impl_->gl.attach_shader(impl_->program, vertex_shader);
|
||||
impl_->gl.attach_shader(impl_->program, fragment_shader);
|
||||
impl_->gl.link_program(impl_->program);
|
||||
impl_->gl.delete_shader(vertex_shader);
|
||||
impl_->gl.delete_shader(fragment_shader);
|
||||
|
||||
GLint linked = GL_FALSE;
|
||||
impl_->gl.get_program_iv(impl_->program, GL_LINK_STATUS, &linked);
|
||||
if (linked != GL_TRUE) {
|
||||
GLint log_length = 0;
|
||||
impl_->gl.get_program_iv(impl_->program, GL_INFO_LOG_LENGTH, &log_length);
|
||||
std::string log(static_cast<size_t>(std::max(log_length, 1)), '\0');
|
||||
GLsizei written = 0;
|
||||
impl_->gl.get_program_info_log(
|
||||
impl_->program, static_cast<GLsizei>(log.size()), &written, log.data());
|
||||
if (written >= 0 && static_cast<size_t>(written) < log.size()) {
|
||||
log.resize(static_cast<size_t>(written));
|
||||
}
|
||||
LOG_ERROR("OpenGL NV12 program link failed: {}", log);
|
||||
impl_->gl.delete_program(impl_->program);
|
||||
impl_->ResetGlHandles();
|
||||
return false;
|
||||
}
|
||||
|
||||
impl_->position_location =
|
||||
impl_->gl.get_attrib_location(impl_->program, "position");
|
||||
impl_->texcoord_location =
|
||||
impl_->gl.get_attrib_location(impl_->program, "texcoord");
|
||||
impl_->y_texture_location =
|
||||
impl_->gl.get_uniform_location(impl_->program, "y_texture");
|
||||
impl_->uv_texture_location =
|
||||
impl_->gl.get_uniform_location(impl_->program, "uv_texture");
|
||||
if (impl_->position_location < 0 || impl_->texcoord_location < 0 ||
|
||||
impl_->y_texture_location < 0 || impl_->uv_texture_location < 0) {
|
||||
LOG_ERROR("OpenGL NV12 program is missing required shader bindings");
|
||||
impl_->gl.delete_program(impl_->program);
|
||||
impl_->ResetGlHandles();
|
||||
return false;
|
||||
}
|
||||
|
||||
static constexpr GLfloat kVertices[] = {
|
||||
-1.0f, 1.0f, 0.0f, 0.0f, // top-left
|
||||
-1.0f, -1.0f, 0.0f, 1.0f, // bottom-left
|
||||
1.0f, 1.0f, 1.0f, 0.0f, // top-right
|
||||
1.0f, -1.0f, 1.0f, 1.0f, // bottom-right
|
||||
};
|
||||
GLint previous_array_buffer = 0;
|
||||
glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &previous_array_buffer);
|
||||
impl_->gl.gen_buffers(1, &impl_->vertex_buffer);
|
||||
impl_->gl.bind_buffer(GL_ARRAY_BUFFER, impl_->vertex_buffer);
|
||||
impl_->gl.buffer_data(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices,
|
||||
GL_STATIC_DRAW);
|
||||
impl_->gl.bind_buffer(GL_ARRAY_BUFFER,
|
||||
static_cast<GLuint>(previous_array_buffer));
|
||||
|
||||
GLint previous_active_texture = GL_TEXTURE0;
|
||||
glGetIntegerv(GL_ACTIVE_TEXTURE, &previous_active_texture);
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
GLint previous_texture_0 = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture_0);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
GLint previous_texture_1 = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture_1);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
glGenTextures(1, &impl_->y_texture);
|
||||
glGenTextures(1, &impl_->uv_texture);
|
||||
for (const auto [unit, texture] :
|
||||
{std::pair{GL_TEXTURE0, impl_->y_texture},
|
||||
std::pair{GL_TEXTURE1, impl_->uv_texture}}) {
|
||||
impl_->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);
|
||||
}
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture_0));
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture_1));
|
||||
impl_->gl.active_texture(static_cast<GLenum>(previous_active_texture));
|
||||
|
||||
const GLenum setup_error = glGetError();
|
||||
if (impl_->program == 0 || impl_->vertex_buffer == 0 ||
|
||||
impl_->y_texture == 0 || impl_->uv_texture == 0 ||
|
||||
setup_error != GL_NO_ERROR) {
|
||||
LOG_ERROR("OpenGL NV12 resource setup failed, error={}", setup_error);
|
||||
Teardown();
|
||||
return false;
|
||||
}
|
||||
|
||||
impl_->ready.store(true, std::memory_order_release);
|
||||
LOG_INFO("OpenGL NV12 underlay initialized ({})",
|
||||
version ? version : "unknown OpenGL version");
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowsOpenGlVideoRenderer::Teardown() {
|
||||
impl_->ready.store(false, std::memory_order_release);
|
||||
if (impl_->y_texture != 0)
|
||||
glDeleteTextures(1, &impl_->y_texture);
|
||||
if (impl_->uv_texture != 0)
|
||||
glDeleteTextures(1, &impl_->uv_texture);
|
||||
if (impl_->vertex_buffer != 0 && impl_->gl.delete_buffers) {
|
||||
impl_->gl.delete_buffers(1, &impl_->vertex_buffer);
|
||||
}
|
||||
if (impl_->program != 0 && impl_->gl.delete_program) {
|
||||
impl_->gl.delete_program(impl_->program);
|
||||
}
|
||||
impl_->ResetGlHandles();
|
||||
}
|
||||
|
||||
bool WindowsOpenGlVideoRenderer::IsReady() const {
|
||||
return impl_->ready.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool WindowsOpenGlVideoRenderer::SetSelectedStream(std::string remote_id) {
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
if (impl_->frames->selected_stream == remote_id) {
|
||||
return false;
|
||||
}
|
||||
impl_->frames->selected_stream = std::move(remote_id);
|
||||
for (auto &slot : impl_->frames->slots) {
|
||||
if (slot.use == SlotUse::pending &&
|
||||
slot.remote_id != impl_->frames->selected_stream) {
|
||||
slot.use = SlotUse::available;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowsOpenGlVideoRenderer::DiscardStream(std::string_view remote_id) {
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
for (auto &slot : impl_->frames->slots) {
|
||||
if (slot.remote_id == remote_id && slot.use != SlotUse::uploading) {
|
||||
slot.valid = false;
|
||||
slot.use = SlotUse::available;
|
||||
slot.remote_id.clear();
|
||||
}
|
||||
}
|
||||
if (impl_->frames->selected_stream == remote_id) {
|
||||
impl_->frames->selected_stream.clear();
|
||||
}
|
||||
}
|
||||
|
||||
WindowsOpenGlVideoRenderer::SubmitResult
|
||||
WindowsOpenGlVideoRenderer::SubmitNv12(std::string_view remote_id,
|
||||
const uint8_t *data, size_t size,
|
||||
int width, int height) {
|
||||
return SubmitNv12Internal(remote_id, data, size, width, height, true);
|
||||
}
|
||||
|
||||
WindowsOpenGlVideoRenderer::SubmitResult
|
||||
WindowsOpenGlVideoRenderer::SubmitCachedNv12(std::string_view remote_id,
|
||||
const uint8_t *data, size_t size,
|
||||
int width, int height) {
|
||||
return SubmitNv12Internal(remote_id, data, size, width, height, false);
|
||||
}
|
||||
|
||||
WindowsOpenGlVideoRenderer::SubmitResult
|
||||
WindowsOpenGlVideoRenderer::SubmitNv12Internal(std::string_view remote_id,
|
||||
const uint8_t *data, size_t size,
|
||||
int width, int height,
|
||||
bool replace_pending) {
|
||||
if (!IsReady()) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
if (data == nullptr || width <= 0 || height <= 0 || (width & 1) != 0 ||
|
||||
(height & 1) != 0) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
const size_t y_size = static_cast<size_t>(width) * height;
|
||||
const size_t required_size = y_size + y_size / 2;
|
||||
if (size < required_size) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
|
||||
auto &frames = *impl_->frames;
|
||||
std::lock_guard lock(frames.mutex);
|
||||
if (frames.selected_stream != remote_id) {
|
||||
return SubmitResult::not_selected;
|
||||
}
|
||||
|
||||
FrameSlot *target = nullptr;
|
||||
if (replace_pending) {
|
||||
for (auto &slot : frames.slots) {
|
||||
if (slot.use == SlotUse::pending) {
|
||||
target = &slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const auto &slot : frames.slots) {
|
||||
if (slot.valid && slot.remote_id == remote_id &&
|
||||
(slot.use == SlotUse::pending || slot.use == SlotUse::uploading)) {
|
||||
return SubmitResult::dropped;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (target == nullptr) {
|
||||
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 == nullptr) {
|
||||
return SubmitResult::dropped;
|
||||
}
|
||||
|
||||
target->bytes.resize(required_size);
|
||||
std::memcpy(target->bytes.data(), data, required_size);
|
||||
target->width = width;
|
||||
target->height = height;
|
||||
target->remote_id.assign(remote_id);
|
||||
target->sequence = frames.next_sequence++;
|
||||
target->use = SlotUse::pending;
|
||||
target->valid = true;
|
||||
return SubmitResult::submitted;
|
||||
}
|
||||
|
||||
WindowsOpenGlVideoRenderer::RenderOutcome
|
||||
WindowsOpenGlVideoRenderer::RenderLatest(std::string_view remote_id,
|
||||
int target_width, int target_height,
|
||||
int top_inset_pixels) {
|
||||
if (!IsReady() || target_width <= 0 || target_height <= 0) {
|
||||
return {RenderResult::failed};
|
||||
}
|
||||
|
||||
size_t slot_index = kFrameSlotCount;
|
||||
uint64_t sequence = 0;
|
||||
int source_width = 0;
|
||||
int source_height = 0;
|
||||
const unsigned char *frame_data = nullptr;
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
for (size_t index = 0; index < impl_->frames->slots.size(); ++index) {
|
||||
const auto &slot = impl_->frames->slots[index];
|
||||
if (slot.valid && slot.use == SlotUse::pending &&
|
||||
slot.remote_id == remote_id &&
|
||||
(slot_index == kFrameSlotCount || slot.sequence > sequence)) {
|
||||
slot_index = index;
|
||||
sequence = slot.sequence;
|
||||
}
|
||||
}
|
||||
if (slot_index != kFrameSlotCount) {
|
||||
auto &selected = impl_->frames->slots[slot_index];
|
||||
selected.use = SlotUse::uploading;
|
||||
source_width = selected.width;
|
||||
source_height = selected.height;
|
||||
frame_data = selected.bytes.data();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GLint previous_program = 0;
|
||||
GLint previous_active_texture = GL_TEXTURE0;
|
||||
GLint previous_array_buffer = 0;
|
||||
GLint previous_unpack_alignment = 4;
|
||||
GLint previous_viewport[4] = {};
|
||||
GLint previous_scissor_box[4] = {};
|
||||
GLfloat previous_clear_color[4] = {};
|
||||
GLboolean previous_color_mask[4] = {};
|
||||
glGetIntegerv(GL_CURRENT_PROGRAM, &previous_program);
|
||||
glGetIntegerv(GL_ACTIVE_TEXTURE, &previous_active_texture);
|
||||
glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &previous_array_buffer);
|
||||
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
|
||||
glGetIntegerv(GL_VIEWPORT, previous_viewport);
|
||||
glGetIntegerv(GL_SCISSOR_BOX, previous_scissor_box);
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, previous_clear_color);
|
||||
glGetBooleanv(GL_COLOR_WRITEMASK, previous_color_mask);
|
||||
const bool blend_enabled = IsEnabled(GL_BLEND);
|
||||
const bool cull_enabled = IsEnabled(GL_CULL_FACE);
|
||||
const bool depth_enabled = IsEnabled(GL_DEPTH_TEST);
|
||||
const bool scissor_enabled = IsEnabled(GL_SCISSOR_TEST);
|
||||
const bool stencil_enabled = IsEnabled(GL_STENCIL_TEST);
|
||||
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
GLint previous_texture_0 = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture_0);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
GLint previous_texture_1 = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture_1);
|
||||
|
||||
const auto position_state = CaptureVertexAttrib(
|
||||
impl_->gl, static_cast<GLuint>(impl_->position_location));
|
||||
const auto texcoord_state = CaptureVertexAttrib(
|
||||
impl_->gl, static_cast<GLuint>(impl_->texcoord_location));
|
||||
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_STENCIL_TEST);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glViewport(0, 0, target_width, target_height);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
bool upload_succeeded = true;
|
||||
if (slot_index != kFrameSlotCount) {
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
if (slot_index != kFrameSlotCount) {
|
||||
auto &slot = impl_->frames->slots[slot_index];
|
||||
if (slot.sequence == sequence && slot.use == SlotUse::uploading) {
|
||||
slot.use = SlotUse::available;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderOutcome outcome{RenderResult::empty};
|
||||
if (upload_succeeded && impl_->uploaded_stream == remote_id &&
|
||||
impl_->texture_width > 0 && impl_->texture_height > 0) {
|
||||
source_width = impl_->texture_width;
|
||||
source_height = impl_->texture_height;
|
||||
sequence = impl_->uploaded_sequence;
|
||||
const int video_height = std::max(
|
||||
0, target_height - std::clamp(top_inset_pixels, 0, target_height));
|
||||
if (video_height > 0) {
|
||||
const double source_aspect =
|
||||
static_cast<double>(source_width) / source_height;
|
||||
const double target_aspect =
|
||||
static_cast<double>(target_width) / video_height;
|
||||
int viewport_width = target_width;
|
||||
int viewport_height = video_height;
|
||||
int viewport_x = 0;
|
||||
int viewport_y = 0;
|
||||
if (source_aspect > target_aspect) {
|
||||
viewport_height =
|
||||
std::max(1, static_cast<int>(target_width / source_aspect + 0.5));
|
||||
viewport_y = (video_height - viewport_height) / 2;
|
||||
} else {
|
||||
viewport_width =
|
||||
std::max(1, static_cast<int>(video_height * source_aspect + 0.5));
|
||||
viewport_x = (target_width - viewport_width) / 2;
|
||||
}
|
||||
|
||||
glViewport(viewport_x, viewport_y, viewport_width, viewport_height);
|
||||
impl_->gl.use_program(impl_->program);
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->y_texture);
|
||||
impl_->gl.uniform_1i(impl_->y_texture_location, 0);
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, impl_->uv_texture);
|
||||
impl_->gl.uniform_1i(impl_->uv_texture_location, 1);
|
||||
impl_->gl.bind_buffer(GL_ARRAY_BUFFER, impl_->vertex_buffer);
|
||||
impl_->gl.vertex_attrib_pointer(
|
||||
static_cast<GLuint>(impl_->position_location), 2, GL_FLOAT, GL_FALSE,
|
||||
4 * static_cast<GLsizei>(sizeof(GLfloat)), nullptr);
|
||||
impl_->gl.vertex_attrib_pointer(
|
||||
static_cast<GLuint>(impl_->texcoord_location), 2, GL_FLOAT, GL_FALSE,
|
||||
4 * static_cast<GLsizei>(sizeof(GLfloat)),
|
||||
reinterpret_cast<const void *>(2 * sizeof(GLfloat)));
|
||||
impl_->gl.enable_vertex_attrib_array(
|
||||
static_cast<GLuint>(impl_->position_location));
|
||||
impl_->gl.enable_vertex_attrib_array(
|
||||
static_cast<GLuint>(impl_->texcoord_location));
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
outcome = {glGetError() == GL_NO_ERROR ? RenderResult::rendered
|
||||
: RenderResult::failed,
|
||||
source_width, source_height, sequence};
|
||||
}
|
||||
}
|
||||
|
||||
RestoreVertexAttrib(impl_->gl, static_cast<GLuint>(impl_->position_location),
|
||||
position_state);
|
||||
RestoreVertexAttrib(impl_->gl, static_cast<GLuint>(impl_->texcoord_location),
|
||||
texcoord_state);
|
||||
impl_->gl.bind_buffer(GL_ARRAY_BUFFER,
|
||||
static_cast<GLuint>(previous_array_buffer));
|
||||
impl_->gl.active_texture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture_0));
|
||||
impl_->gl.active_texture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture_1));
|
||||
impl_->gl.active_texture(static_cast<GLenum>(previous_active_texture));
|
||||
impl_->gl.use_program(static_cast<GLuint>(previous_program));
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
|
||||
glViewport(previous_viewport[0], previous_viewport[1], previous_viewport[2],
|
||||
previous_viewport[3]);
|
||||
glScissor(previous_scissor_box[0], previous_scissor_box[1],
|
||||
previous_scissor_box[2], previous_scissor_box[3]);
|
||||
glClearColor(previous_clear_color[0], previous_clear_color[1],
|
||||
previous_clear_color[2], previous_clear_color[3]);
|
||||
glColorMask(previous_color_mask[0], previous_color_mask[1],
|
||||
previous_color_mask[2], previous_color_mask[3]);
|
||||
RestoreCapability(GL_BLEND, blend_enabled);
|
||||
RestoreCapability(GL_CULL_FACE, cull_enabled);
|
||||
RestoreCapability(GL_DEPTH_TEST, depth_enabled);
|
||||
RestoreCapability(GL_SCISSOR_TEST, scissor_enabled);
|
||||
RestoreCapability(GL_STENCIL_TEST, stencil_enabled);
|
||||
const GLenum restore_error = glGetError();
|
||||
if (restore_error != GL_NO_ERROR) {
|
||||
LOG_WARN("OpenGL NV12 state restoration failed, error={}", restore_error);
|
||||
if (outcome.result == RenderResult::rendered) {
|
||||
outcome.result = RenderResult::failed;
|
||||
}
|
||||
}
|
||||
return outcome;
|
||||
}
|
||||
|
||||
bool WindowsOpenGlVideoRenderer::CopyLatestNv12(
|
||||
std::string_view remote_id, std::vector<unsigned char> *output, int *width,
|
||||
int *height) const {
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (!newest || newest->bytes.empty()) {
|
||||
return false;
|
||||
}
|
||||
*output = newest->bytes;
|
||||
*width = newest->width;
|
||||
*height = newest->height;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,86 @@
|
||||
#ifndef CROSSDESK_GUI_PLATFORM_WINDOWS_OPENGL_VIDEO_RENDERER_H_
|
||||
#define CROSSDESK_GUI_PLATFORM_WINDOWS_OPENGL_VIDEO_RENDERER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Windows NV12 underlay for Slint's FemtoVG OpenGL renderer. 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.
|
||||
class WindowsOpenGlVideoRenderer {
|
||||
public:
|
||||
enum class SubmitResult {
|
||||
submitted,
|
||||
not_selected,
|
||||
dropped,
|
||||
failed,
|
||||
};
|
||||
|
||||
enum class RenderResult {
|
||||
rendered,
|
||||
idle,
|
||||
empty,
|
||||
failed,
|
||||
};
|
||||
|
||||
struct RenderOutcome {
|
||||
RenderResult result = RenderResult::failed;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
WindowsOpenGlVideoRenderer();
|
||||
~WindowsOpenGlVideoRenderer();
|
||||
|
||||
WindowsOpenGlVideoRenderer(const WindowsOpenGlVideoRenderer &) = delete;
|
||||
WindowsOpenGlVideoRenderer &
|
||||
operator=(const WindowsOpenGlVideoRenderer &) = delete;
|
||||
|
||||
// These methods must run while Slint's OpenGL context is current, from the
|
||||
// RenderingSetup and RenderingTeardown notifier states respectively.
|
||||
bool Setup();
|
||||
void Teardown();
|
||||
bool IsReady() const;
|
||||
|
||||
// Only the selected stream is queued for upload. Frames for other tabs are
|
||||
// retained by GuiRuntime at a throttled rate for thumbnail/tab restoration.
|
||||
bool SetSelectedStream(std::string remote_id);
|
||||
void DiscardStream(std::string_view remote_id);
|
||||
|
||||
// Thread-safe; called from MiniRTC's decode callback thread.
|
||||
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t *data,
|
||||
size_t size, int width, int height);
|
||||
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
|
||||
size_t size, int width, int height);
|
||||
|
||||
// Draws below Slint while its OpenGL context is current. target dimensions
|
||||
// and top_inset_pixels are physical pixels in the window client area.
|
||||
RenderOutcome RenderLatest(std::string_view remote_id, int target_width,
|
||||
int target_height, int top_inset_pixels);
|
||||
|
||||
// Used during disconnect cleanup so thumbnail generation does not require a
|
||||
// second per-frame CPU copy during normal playback.
|
||||
bool CopyLatestNv12(std::string_view remote_id,
|
||||
std::vector<unsigned char> *output, int *width,
|
||||
int *height) const;
|
||||
|
||||
private:
|
||||
SubmitResult SubmitNv12Internal(std::string_view remote_id,
|
||||
const uint8_t *data, size_t size, int width,
|
||||
int height, bool replace_pending);
|
||||
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_PLATFORM_WINDOWS_OPENGL_VIDEO_RENDERER_H_
|
||||
@@ -11,6 +11,12 @@
|
||||
#include "display_stream_id.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#if defined(_WIN32)
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
@@ -223,7 +229,30 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
if ((!frame_snapshot || frame_snapshot->empty()) &&
|
||||
props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) {
|
||||
frame_snapshot = props->thumbnail_frame_;
|
||||
video_width = props->thumbnail_width_;
|
||||
video_height = props->thumbnail_height_;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
#if defined(_WIN32)
|
||||
auto* native_renderer = windows_opengl_video_renderer_.get();
|
||||
#else
|
||||
auto* native_renderer = mac_metal_video_renderer_.get();
|
||||
#endif
|
||||
if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) {
|
||||
auto native_snapshot = std::make_shared<std::vector<unsigned char>>();
|
||||
if (native_renderer->CopyLatestNv12(props->remote_id_,
|
||||
native_snapshot.get(), &video_width,
|
||||
&video_height)) {
|
||||
frame_snapshot = std::move(native_snapshot);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||
video_height > 0) {
|
||||
@@ -247,6 +276,13 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
if (native_renderer) {
|
||||
native_renderer->DiscardStream(props->remote_id_);
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (props->peer_) {
|
||||
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
|
||||
LeaveConnection(props->peer_, props->remote_id_.c_str());
|
||||
@@ -307,6 +343,12 @@ void GuiRuntime::ResetRemoteSessionResources(
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
props->thumbnail_frame_.reset();
|
||||
props->thumbnail_width_ = 0;
|
||||
props->thumbnail_height_ = 0;
|
||||
props->background_snapshot_time_ = {};
|
||||
#endif
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include "localization.h"
|
||||
#if defined(_WIN32)
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -154,6 +160,8 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
: 60;
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainFrameRate;
|
||||
params_.on_receive_video_buffer = nullptr;
|
||||
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
|
||||
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef CROSSDESK_GUI_RUNTIME_H_
|
||||
#define CROSSDESK_GUI_RUNTIME_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -14,6 +15,13 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
#if defined(_WIN32)
|
||||
class WindowsOpenGlVideoRenderer;
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
class MacMetalVideoRenderer;
|
||||
#endif
|
||||
|
||||
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
|
||||
// state, but no window lifecycle, ImGui view, or transport callback methods.
|
||||
class GuiRuntime : protected gui_detail::GuiState {
|
||||
@@ -81,6 +89,13 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
SettingsManager settings_;
|
||||
KeyboardController keyboard_;
|
||||
PeerEventHandler peer_events_;
|
||||
std::atomic<bool> video_frame_dirty_{false};
|
||||
#if defined(_WIN32)
|
||||
std::unique_ptr<WindowsOpenGlVideoRenderer> windows_opengl_video_renderer_;
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
std::unique_ptr<MacMetalVideoRenderer> mac_metal_video_renderer_;
|
||||
#endif
|
||||
|
||||
private:
|
||||
friend class ClipboardController;
|
||||
|
||||
@@ -10,11 +10,18 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#if defined(_WIN32)
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "runtime/remote_action_codec.h"
|
||||
@@ -312,16 +319,62 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
props->enable_mouse_control_ = false;
|
||||
runtime->ResetRemoteServiceStatus(*props);
|
||||
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
|
||||
int native_snapshot_width = 0;
|
||||
int native_snapshot_height = 0;
|
||||
bool needs_native_snapshot = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
needs_native_snapshot = !props->thumbnail_frame_ ||
|
||||
props->thumbnail_frame_->empty();
|
||||
}
|
||||
if (needs_native_snapshot) {
|
||||
#if defined(_WIN32)
|
||||
auto* native_renderer =
|
||||
runtime->windows_opengl_video_renderer_.get();
|
||||
#else
|
||||
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
|
||||
#endif
|
||||
auto snapshot = std::make_shared<std::vector<unsigned char>>();
|
||||
if (native_renderer && native_renderer->CopyLatestNv12(
|
||||
remote_id, snapshot.get(),
|
||||
&native_snapshot_width,
|
||||
&native_snapshot_height)) {
|
||||
native_snapshot = std::move(snapshot);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
if (native_snapshot &&
|
||||
(!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) {
|
||||
props->thumbnail_frame_ = std::move(native_snapshot);
|
||||
props->thumbnail_width_ = native_snapshot_width;
|
||||
props->thumbnail_height_ = native_snapshot_height;
|
||||
}
|
||||
#endif
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
props->render_rect_dirty_ = true;
|
||||
props->stream_cleanup_pending_ = true;
|
||||
}
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
#if defined(_WIN32)
|
||||
auto* native_renderer = runtime->windows_opengl_video_renderer_.get();
|
||||
#else
|
||||
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
|
||||
#endif
|
||||
if (native_renderer) {
|
||||
native_renderer->DiscardStream(remote_id);
|
||||
runtime->video_frame_dirty_.store(true,
|
||||
std::memory_order_release);
|
||||
}
|
||||
#endif
|
||||
|
||||
runtime->focus_on_stream_window_ = false;
|
||||
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "runtime/remote_action_codec.h"
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
#include <chrono>
|
||||
|
||||
#if _WIN32
|
||||
#include "interactive_state.h"
|
||||
#include "service_host.h"
|
||||
#if defined(_WIN32)
|
||||
#include "platform/windows_opengl_video_renderer.h"
|
||||
#else
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
namespace {
|
||||
|
||||
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
const XVideoFrame *video_frame, const char *user_id, size_t user_id_size,
|
||||
@@ -45,10 +45,77 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
runtime->remote_sessions_.find(remote_id)->second.get();
|
||||
|
||||
if (props->connection_established_) {
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
bool background_snapshot_only = false;
|
||||
#if defined(_WIN32)
|
||||
auto* native_renderer = runtime->windows_opengl_video_renderer_.get();
|
||||
using NativeVideoRenderer = WindowsOpenGlVideoRenderer;
|
||||
#else
|
||||
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
|
||||
using NativeVideoRenderer = MacMetalVideoRenderer;
|
||||
#endif
|
||||
if (native_renderer && native_renderer->IsReady()
|
||||
#if defined(__APPLE__)
|
||||
&& native_renderer->IsAttached()
|
||||
#endif
|
||||
) {
|
||||
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);
|
||||
if (submit_result == NativeVideoRenderer::SubmitResult::submitted) {
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
const bool size_changed =
|
||||
(props->video_width_ != video_frame->width) ||
|
||||
(props->video_height_ != video_frame->height);
|
||||
if (size_changed) {
|
||||
props->render_rect_dirty_ = true;
|
||||
}
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
// Once the native renderer owns the current stream, do not leave an
|
||||
// older CPU frame ahead of the renderer's close snapshot.
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
props->thumbnail_frame_.reset();
|
||||
props->thumbnail_width_ = 0;
|
||||
props->thumbnail_height_ = 0;
|
||||
props->background_snapshot_time_ = {};
|
||||
++props->video_frame_sequence_;
|
||||
props->streaming_ = true;
|
||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
if (submit_result == NativeVideoRenderer::SubmitResult::dropped ||
|
||||
submit_result == NativeVideoRenderer::SubmitResult::failed) {
|
||||
// Keep presenting the last native frame. A later decoded frame can
|
||||
// reuse the renderer without changing ownership mid-window.
|
||||
props->streaming_ = true;
|
||||
return;
|
||||
}
|
||||
if (submit_result ==
|
||||
NativeVideoRenderer::SubmitResult::not_selected) {
|
||||
background_snapshot_only = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (background_snapshot_only && props->thumbnail_frame_ &&
|
||||
!props->thumbnail_frame_->empty() &&
|
||||
props->background_snapshot_time_ !=
|
||||
std::chrono::steady_clock::time_point{} &&
|
||||
now - props->background_snapshot_time_ <
|
||||
kBackgroundSnapshotInterval) {
|
||||
props->streaming_ = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!props->back_frame_) {
|
||||
// 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);
|
||||
}
|
||||
@@ -70,24 +137,24 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
props->video_size_ = video_frame->size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
props->thumbnail_frame_ = props->front_frame_;
|
||||
props->thumbnail_width_ = video_frame->width;
|
||||
props->thumbnail_height_ = video_frame->height;
|
||||
if (background_snapshot_only) {
|
||||
props->background_snapshot_time_ = now;
|
||||
}
|
||||
#endif
|
||||
++props->video_frame_sequence_;
|
||||
}
|
||||
|
||||
props->streaming_ = true;
|
||||
|
||||
if (props->net_traffic_stats_button_pressed_) {
|
||||
props->frame_count_++;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - props->last_time_)
|
||||
.count();
|
||||
|
||||
if (elapsed >= 1000) {
|
||||
props->fps_ = props->frame_count_ * 1000 / elapsed;
|
||||
props->frame_count_ = 0;
|
||||
props->last_time_ = now;
|
||||
}
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
if (background_snapshot_only) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,14 @@ struct RemoteSession {
|
||||
std::mutex video_frame_mutex_;
|
||||
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
||||
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
||||
#if defined(__APPLE__) || defined(_WIN32)
|
||||
// Retains the most recent background CPU frame across disconnect cleanup
|
||||
// so closing an unselected tab can still update its recent-item thumbnail.
|
||||
std::shared_ptr<std::vector<unsigned char>> thumbnail_frame_;
|
||||
int thumbnail_width_ = 0;
|
||||
int thumbnail_height_ = 0;
|
||||
std::chrono::steady_clock::time_point background_snapshot_time_;
|
||||
#endif
|
||||
bool render_rect_dirty_ = false;
|
||||
bool stream_cleanup_pending_ = false;
|
||||
float mouse_pos_x_ = 0;
|
||||
|
||||
@@ -116,10 +116,12 @@ export component StreamWindow inherits Window {
|
||||
min-width: 640px;
|
||||
min-height: 360px + (root.custom-titlebar ? 32px : 0px);
|
||||
no-frame: root.custom-titlebar;
|
||||
background: root.custom-titlebar ? transparent : black;
|
||||
background: root.native-video-enabled
|
||||
? transparent : root.custom-titlebar ? transparent : black;
|
||||
default-font-size: ImGuiFontStyle.base;
|
||||
|
||||
in property <bool> custom-titlebar: false;
|
||||
in property <bool> native-video-enabled: false;
|
||||
in property <bool> window-active: true;
|
||||
in property <bool> window-maximized: false;
|
||||
in property <[StreamTab]> tabs;
|
||||
@@ -263,7 +265,7 @@ export component StreamWindow inherits Window {
|
||||
window-surface := Rectangle {
|
||||
width: root.width;
|
||||
height: root.height;
|
||||
background: black;
|
||||
background: root.native-video-enabled ? transparent : black;
|
||||
border-radius:
|
||||
root.custom-titlebar && !root.fullscreen-enabled && !root.window-maximized
|
||||
? root.window-corner-radius : 0px;
|
||||
@@ -419,7 +421,7 @@ export component StreamWindow inherits Window {
|
||||
y: root.titlebar-height;
|
||||
width: parent.width;
|
||||
height: parent.height - self.y;
|
||||
background: black;
|
||||
background: root.native-video-enabled ? transparent : black;
|
||||
clip: true;
|
||||
|
||||
// Keep the ImGui tab strip behavior, including pointer-drag reordering.
|
||||
@@ -499,7 +501,7 @@ export component StreamWindow inherits Window {
|
||||
video := Rectangle {
|
||||
y: root.fullscreen-enabled ? 0px : (root.tabs.length > 1 ? 30px : 0px);
|
||||
height: parent.height - self.y;
|
||||
background: black;
|
||||
background: root.native-video-enabled ? transparent : black;
|
||||
clip: true;
|
||||
|
||||
frame-image := Image {
|
||||
@@ -507,7 +509,7 @@ export component StreamWindow inherits Window {
|
||||
height: parent.height;
|
||||
source: root.frame;
|
||||
image-fit: contain;
|
||||
visible: root.has-frame;
|
||||
visible: root.has-frame && !root.native-video-enabled;
|
||||
}
|
||||
|
||||
Text {
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#ifndef CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
#define CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
|
||||
#include <slint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "crossdesk_ui.h"
|
||||
#include "localization.h"
|
||||
|
||||
namespace crossdesk::ui_localization {
|
||||
|
||||
inline slint::SharedString Text(const std::string& value) {
|
||||
return slint::SharedString(value);
|
||||
}
|
||||
|
||||
inline int ApplyMainWindowStrings(
|
||||
const slint::ComponentHandle<ui::MainWindow>& window,
|
||||
int language_index) {
|
||||
const int language =
|
||||
localization::detail::ClampLanguageIndex(language_index);
|
||||
auto& strings = window->global<ui::UiStrings>();
|
||||
strings.set_local_desktop(Text(localization::local_desktop[language]));
|
||||
strings.set_remote_desktop(Text(localization::remote_desktop[language]));
|
||||
strings.set_recent_connections(
|
||||
Text(localization::recent_connections[language]));
|
||||
strings.set_local_id(Text(localization::local_id[language]));
|
||||
strings.set_device_name(Text(localization::device_name[language]));
|
||||
strings.set_remote_id(Text(localization::remote_id[language]));
|
||||
strings.set_online(Text(localization::online[language]));
|
||||
strings.set_offline(Text(localization::offline[language]));
|
||||
strings.set_password(Text(localization::password[language]));
|
||||
strings.set_copied(
|
||||
Text(localization::local_id_copied_to_clipboard[language]));
|
||||
strings.set_connect(Text(localization::connect[language]));
|
||||
strings.set_settings(Text(localization::settings[language]));
|
||||
strings.set_about(Text(localization::about[language]));
|
||||
strings.set_ok(Text(localization::ok[language]));
|
||||
strings.set_cancel(Text(localization::cancel[language]));
|
||||
strings.set_new_password(Text(localization::new_password[language]));
|
||||
strings.set_invalid_password(Text(localization::max_password_len[language]));
|
||||
strings.set_edit_alias(
|
||||
Text(localization::input_connection_alias[language]));
|
||||
strings.set_delete_connection(
|
||||
Text(localization::delete_connection[language]));
|
||||
strings.set_confirm_delete(
|
||||
Text(localization::confirm_delete_connection[language]));
|
||||
strings.set_language(Text(localization::language[language]));
|
||||
strings.set_video_quality(Text(localization::video_quality[language]));
|
||||
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
|
||||
strings.set_codec(Text(localization::video_encode_format[language]));
|
||||
strings.set_hardware_codec(
|
||||
Text(localization::enable_hardware_video_codec[language]));
|
||||
strings.set_turn_relay(Text(localization::enable_turn[language]));
|
||||
strings.set_srtp(Text(localization::enable_srtp[language]));
|
||||
strings.set_self_hosted(
|
||||
Text(localization::self_hosted_server_config[language]));
|
||||
strings.set_autostart(Text(localization::enable_autostart[language]));
|
||||
strings.set_daemon(Text(localization::enable_daemon[language]));
|
||||
strings.set_minimize_to_tray(
|
||||
Text(localization::minimize_to_tray[language]));
|
||||
strings.set_file_save_path(
|
||||
Text(localization::file_transfer_save_path[language]));
|
||||
strings.set_default_desktop(Text(localization::default_desktop[language]));
|
||||
strings.set_server_host(
|
||||
Text(localization::self_hosted_server_address[language]));
|
||||
strings.set_server_port(
|
||||
Text(localization::self_hosted_server_port[language]));
|
||||
strings.set_coturn_port(
|
||||
Text(localization::self_hosted_server_coturn_server_port[language]));
|
||||
strings.set_version(Text(localization::version[language]));
|
||||
strings.set_signal_connected(Text(localization::signal_connected[language]));
|
||||
strings.set_signal_disconnected(
|
||||
Text(localization::signal_disconnected[language]));
|
||||
strings.set_tls_error(Text(localization::signal_tls_cert_error[language]));
|
||||
strings.set_update_available(
|
||||
Text(localization::new_version_available[language]));
|
||||
strings.set_release_notes(Text(localization::release_notes[language]));
|
||||
strings.set_download(Text(localization::update[language]));
|
||||
strings.set_input_password(Text(localization::input_password[language]));
|
||||
strings.set_reinput_password(Text(localization::reinput_password[language]));
|
||||
strings.set_remember_password(
|
||||
Text(localization::remember_password[language]));
|
||||
strings.set_validate_password(
|
||||
Text(localization::validate_password[language]));
|
||||
strings.set_request_permissions(
|
||||
Text(localization::request_permissions[language]));
|
||||
strings.set_permission_required(
|
||||
Text(localization::permission_required_message[language]));
|
||||
strings.set_screen_recording_permission(
|
||||
Text(localization::screen_recording_permission[language]));
|
||||
strings.set_accessibility_permission(
|
||||
Text(localization::accessibility_permission[language]));
|
||||
strings.set_service_setup_title(
|
||||
Text(localization::windows_service_setup_title[language]));
|
||||
strings.set_service_setup_message(
|
||||
Text(localization::windows_service_setup_message[language]));
|
||||
strings.set_service_settings_label(
|
||||
Text(localization::windows_service_settings_label[language]));
|
||||
strings.set_install_service(
|
||||
Text(localization::install_windows_service[language]));
|
||||
strings.set_service_installed(
|
||||
Text(localization::windows_service_installed[language]));
|
||||
strings.set_do_not_remind(
|
||||
Text(localization::do_not_remind_again[language]));
|
||||
strings.set_notification(Text(localization::notification[language]));
|
||||
strings.set_service_suppressed_message(
|
||||
Text(localization::windows_service_prompt_suppressed_message[language]));
|
||||
strings.set_quality_low(Text(localization::video_quality_low[language]));
|
||||
strings.set_quality_medium(
|
||||
Text(localization::video_quality_medium[language]));
|
||||
strings.set_quality_high(Text(localization::video_quality_high[language]));
|
||||
strings.set_codec_h264(Text(localization::h264[language]));
|
||||
strings.set_codec_av1(Text(localization::av1[language]));
|
||||
strings.set_self_hosted_settings(
|
||||
Text(localization::self_hosted_server_settings[language]));
|
||||
strings.set_access_website(Text(localization::access_website[language]));
|
||||
strings.set_release_date_label(Text(localization::release_date[language]));
|
||||
strings.set_later(Text(localization::cancel[language]));
|
||||
return language;
|
||||
}
|
||||
|
||||
template <typename Window>
|
||||
inline int ApplyStreamWindowStrings(
|
||||
const slint::ComponentHandle<Window>& window, int language_index) {
|
||||
const int language =
|
||||
localization::detail::ClampLanguageIndex(language_index);
|
||||
auto& strings = window->template global<ui::StreamStrings>();
|
||||
strings.set_select_display(Text(localization::select_display[language]));
|
||||
strings.set_send_shortcut(Text(localization::send_shortcut[language]));
|
||||
strings.set_control_mouse(Text(localization::control_mouse[language]));
|
||||
strings.set_release_mouse(Text(localization::release_mouse[language]));
|
||||
strings.set_audio(Text(localization::audio_capture[language]));
|
||||
strings.set_mute(Text(localization::mute[language]));
|
||||
strings.set_select_file(Text(localization::select_file[language]));
|
||||
strings.set_show_stats(
|
||||
Text(localization::show_net_traffic_stats[language]));
|
||||
strings.set_hide_stats(
|
||||
Text(localization::hide_net_traffic_stats[language]));
|
||||
strings.set_fullscreen(Text(localization::fullscreen[language]));
|
||||
strings.set_exit_fullscreen(Text(localization::exit_fullscreen[language]));
|
||||
strings.set_disconnect(Text(localization::disconnect[language]));
|
||||
strings.set_file_transfer(
|
||||
Text(localization::file_transfer_progress[language]));
|
||||
strings.set_expand_control(Text(localization::expand_control_bar[language]));
|
||||
strings.set_collapse_control(
|
||||
Text(localization::collapse_control_bar[language]));
|
||||
strings.set_stats_in(Text(localization::in[language]));
|
||||
strings.set_stats_out(Text(localization::out[language]));
|
||||
strings.set_stats_loss_rate(Text(localization::loss_rate[language]));
|
||||
strings.set_stats_resolution(Text(localization::resolution[language]));
|
||||
strings.set_stats_connection_mode(
|
||||
Text(localization::connection_mode[language]));
|
||||
return language;
|
||||
}
|
||||
|
||||
} // namespace crossdesk::ui_localization
|
||||
|
||||
#endif // CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
+1
-1
Submodule submodules/minirtc updated: 7ea6e0e655...ad2614b3a1
@@ -1,11 +1,14 @@
|
||||
#include "crossdesk_ui.h"
|
||||
#include "fa_solid_900.h"
|
||||
#include "ui/ui_localization.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <charconv>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -36,9 +39,309 @@ bool RegisterFontAwesome(slint::Window &window) {
|
||||
.has_value();
|
||||
}
|
||||
|
||||
struct CaptureOptions {
|
||||
bool enabled = false;
|
||||
std::string page;
|
||||
int language = 0;
|
||||
std::string snapshot_path;
|
||||
};
|
||||
|
||||
// Compatibility entry point for the former application-level debug capture
|
||||
// mode. CROSSDESK_UI_CAPTURE keeps the interactive workflow, while
|
||||
// CROSSDESK_UI_CAPTURE_SNAPSHOT writes a deterministic PPM and exits so every
|
||||
// page can be exercised in automation. The legacy /tmp overrides still win.
|
||||
std::string ReadFirstLine(const char *path) {
|
||||
std::ifstream input(path);
|
||||
std::string value;
|
||||
std::getline(input, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
CaptureOptions LoadCaptureOptions() {
|
||||
CaptureOptions options;
|
||||
if (const char *capture = std::getenv("CROSSDESK_UI_CAPTURE")) {
|
||||
options.enabled = std::string_view(capture) != "0";
|
||||
}
|
||||
if (!options.enabled) {
|
||||
return options;
|
||||
}
|
||||
|
||||
if (const char *page = std::getenv("CROSSDESK_UI_CAPTURE_PAGE")) {
|
||||
options.page = page;
|
||||
}
|
||||
if (const std::string page_override =
|
||||
ReadFirstLine("/tmp/crossdesk-ui-capture-page");
|
||||
!page_override.empty()) {
|
||||
options.page = page_override;
|
||||
}
|
||||
|
||||
std::string language_value =
|
||||
ReadFirstLine("/tmp/crossdesk-ui-capture-language");
|
||||
if (language_value.empty()) {
|
||||
if (const char *language =
|
||||
std::getenv("CROSSDESK_UI_CAPTURE_LANGUAGE")) {
|
||||
language_value = language;
|
||||
}
|
||||
}
|
||||
if (!language_value.empty()) {
|
||||
int language = 0;
|
||||
const auto [end, error] = std::from_chars(
|
||||
language_value.data(), language_value.data() + language_value.size(),
|
||||
language);
|
||||
if (error == std::errc{} &&
|
||||
end == language_value.data() + language_value.size()) {
|
||||
options.language =
|
||||
crossdesk::localization::detail::ClampLanguageIndex(language);
|
||||
}
|
||||
}
|
||||
if (const char *snapshot =
|
||||
std::getenv("CROSSDESK_UI_CAPTURE_SNAPSHOT")) {
|
||||
options.snapshot_path = snapshot;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
bool HasNonEmptyEnvironmentVariable(const char *name) {
|
||||
const char *value = std::getenv(name);
|
||||
return value != nullptr && value[0] != '\0';
|
||||
}
|
||||
|
||||
void ResetMainCaptureState(
|
||||
const slint::ComponentHandle<crossdesk::ui::MainWindow> &window) {
|
||||
window->set_settings_open(false);
|
||||
window->set_self_host_settings_open(false);
|
||||
window->set_about_open(false);
|
||||
window->set_update_open(false);
|
||||
window->set_update_available(false);
|
||||
window->set_reset_password_open(false);
|
||||
window->set_reset_password_invalid(false);
|
||||
window->set_new_password_input("");
|
||||
window->set_alias_open(false);
|
||||
window->set_alias_input("");
|
||||
window->set_delete_open(false);
|
||||
window->set_offline_warning("");
|
||||
window->set_connection_dialog_open(false);
|
||||
window->set_connection_status_text("");
|
||||
window->set_connection_pending(false);
|
||||
window->set_connection_password_required(false);
|
||||
window->set_connection_validating(false);
|
||||
window->set_permission_dialog_open(false);
|
||||
window->set_screen_recording_granted(true);
|
||||
window->set_accessibility_granted(true);
|
||||
window->set_settings_session_active(false);
|
||||
window->set_portable_service_settings_visible(false);
|
||||
window->set_portable_service_dialog_open(false);
|
||||
window->set_portable_service_suppressed_notice_open(false);
|
||||
}
|
||||
|
||||
void ConfigureMainCapturePage(
|
||||
const slint::ComponentHandle<crossdesk::ui::MainWindow> &window,
|
||||
const CaptureOptions &options) {
|
||||
const int language = crossdesk::ui_localization::ApplyMainWindowStrings(
|
||||
window, options.language);
|
||||
crossdesk::ui_localization::ApplyStreamWindowStrings(window, language);
|
||||
window->set_language_index(language);
|
||||
ResetMainCaptureState(window);
|
||||
|
||||
#if _WIN32
|
||||
window->set_custom_titlebar(true);
|
||||
window->set_wayland_titlebar(false);
|
||||
#if CROSSDESK_PORTABLE
|
||||
window->set_portable_service_settings_visible(true);
|
||||
#endif
|
||||
#elif defined(__linux__)
|
||||
const bool has_x11 = HasNonEmptyEnvironmentVariable("DISPLAY");
|
||||
const bool use_xwayland =
|
||||
has_x11 && HasNonEmptyEnvironmentVariable("WAYLAND_DISPLAY");
|
||||
window->set_custom_titlebar(has_x11);
|
||||
window->set_wayland_titlebar(use_xwayland);
|
||||
#else
|
||||
window->set_custom_titlebar(false);
|
||||
window->set_wayland_titlebar(false);
|
||||
#endif
|
||||
|
||||
if (options.page == "settings") {
|
||||
window->set_settings_open(true);
|
||||
} else if (options.page == "self-hosted") {
|
||||
window->set_settings_open(true);
|
||||
window->set_self_host_settings_open(true);
|
||||
} else if (options.page == "about") {
|
||||
window->set_current_version("0.0.0");
|
||||
window->set_about_open(true);
|
||||
} else if (options.page == "update") {
|
||||
window->set_update_available(true);
|
||||
window->set_latest_version("v0.0.1");
|
||||
window->set_release_name("CrossDesk Preview");
|
||||
window->set_release_date("2026-08-25");
|
||||
window->set_update_open(true);
|
||||
} else if (options.page == "reset-password") {
|
||||
window->set_reset_password_open(true);
|
||||
} else if (options.page == "reset-password-invalid") {
|
||||
window->set_reset_password_open(true);
|
||||
window->set_reset_password_invalid(true);
|
||||
window->set_new_password_input("123");
|
||||
} else if (options.page == "alias") {
|
||||
window->set_alias_input("Office workstation");
|
||||
window->set_alias_open(true);
|
||||
} else if (options.page == "delete") {
|
||||
window->set_delete_open(true);
|
||||
} else if (options.page == "offline") {
|
||||
window->set_offline_warning(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::device_offline[language]));
|
||||
} else if (options.page == "connection") {
|
||||
window->set_connection_status_text(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::p2p_connecting[language]));
|
||||
window->set_connection_pending(true);
|
||||
window->set_connection_dialog_open(true);
|
||||
} else if (options.page == "connection-password") {
|
||||
window->set_connection_status_text(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::reinput_password[language]));
|
||||
window->set_connection_password_required(true);
|
||||
window->set_connection_dialog_open(true);
|
||||
} else if (options.page == "service") {
|
||||
window->set_portable_service_settings_visible(true);
|
||||
window->set_portable_service_dialog_open(true);
|
||||
} else if (options.page == "service-notice") {
|
||||
window->set_portable_service_settings_visible(true);
|
||||
window->set_portable_service_suppressed_notice_open(true);
|
||||
} else if (options.page == "permission") {
|
||||
window->set_permission_dialog_open(true);
|
||||
window->set_screen_recording_granted(false);
|
||||
window->set_accessibility_granted(false);
|
||||
}
|
||||
}
|
||||
|
||||
slint::Image CreateStreamPreviewImage() {
|
||||
constexpr int preview_width = 640;
|
||||
constexpr int preview_height = 360;
|
||||
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(preview_width,
|
||||
preview_height);
|
||||
auto *data = reinterpret_cast<uint8_t *>(pixels.begin());
|
||||
for (int y = 0; y < preview_height; ++y) {
|
||||
for (int x = 0; x < preview_width; ++x) {
|
||||
const bool border = x < 8 || y < 8 || x >= preview_width - 8 ||
|
||||
y >= preview_height - 8;
|
||||
const size_t offset = (static_cast<size_t>(y) * preview_width + x) * 3;
|
||||
data[offset] =
|
||||
border ? 240 : static_cast<uint8_t>(35 + 150 * x / preview_width);
|
||||
data[offset + 1] =
|
||||
border ? 70 : static_cast<uint8_t>(35 + 150 * y / preview_height);
|
||||
data[offset + 2] = border ? 70 : 90;
|
||||
}
|
||||
}
|
||||
return slint::Image(std::move(pixels));
|
||||
}
|
||||
|
||||
void ConfigureStreamCapturePage(
|
||||
const slint::ComponentHandle<crossdesk::ui::StreamWindow> &stream,
|
||||
int language) {
|
||||
language = crossdesk::ui_localization::ApplyStreamWindowStrings(
|
||||
stream, language);
|
||||
#if defined(__linux__)
|
||||
stream->set_custom_titlebar(
|
||||
HasNonEmptyEnvironmentVariable("DISPLAY") &&
|
||||
HasNonEmptyEnvironmentVariable("WAYLAND_DISPLAY"));
|
||||
#else
|
||||
stream->set_custom_titlebar(false);
|
||||
#endif
|
||||
stream->set_native_video_enabled(false);
|
||||
stream->set_window_maximized(false);
|
||||
stream->set_fullscreen_enabled(false);
|
||||
stream->set_stats_visible(false);
|
||||
stream->set_file_transfer_visible(false);
|
||||
stream->set_receiving_text("");
|
||||
stream->set_status_text("");
|
||||
|
||||
crossdesk::ui::StreamTab tab;
|
||||
tab.remote_id = "589173341";
|
||||
tab.title = "Mac";
|
||||
tab.connected = true;
|
||||
stream->set_tabs(
|
||||
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
|
||||
std::vector{tab}));
|
||||
stream->set_displays(
|
||||
std::make_shared<slint::VectorModel<slint::SharedString>>(
|
||||
std::vector{slint::SharedString("Display 1")}));
|
||||
stream->set_frame(CreateStreamPreviewImage());
|
||||
stream->set_has_frame(true);
|
||||
const float height = stream->get_custom_titlebar() ? 752.0f : 720.0f;
|
||||
stream->window().set_size(
|
||||
slint::LogicalSize(slint::Size<float>{1280.0f, height}));
|
||||
}
|
||||
|
||||
void ConfigureServerCapturePage(
|
||||
const slint::ComponentHandle<crossdesk::ui::ServerWindow> &server,
|
||||
int language) {
|
||||
language = crossdesk::localization::detail::ClampLanguageIndex(language);
|
||||
crossdesk::ui::ControllerEntry controller;
|
||||
controller.remote_id = "589173341";
|
||||
controller.display_name = "Mac";
|
||||
server->set_controllers(
|
||||
std::make_shared<slint::VectorModel<crossdesk::ui::ControllerEntry>>(
|
||||
std::vector{controller}));
|
||||
server->set_controller_names(
|
||||
std::make_shared<slint::VectorModel<slint::SharedString>>(
|
||||
std::vector{slint::SharedString("Mac")}));
|
||||
server->set_language_index(language);
|
||||
server->set_controller_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::controller[language]));
|
||||
server->set_connection_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::connection_status[language]));
|
||||
server->set_connection_status(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::p2p_connected[language]));
|
||||
server->set_file_transfer_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::file_transfer[language]));
|
||||
server->set_select_file_label(crossdesk::ui_localization::Text(
|
||||
crossdesk::localization::select_file[language]));
|
||||
server->set_file_transfer_visible(false);
|
||||
server->set_sending_file(false);
|
||||
const float width = language == 0 ? 250.0f : language == 1 ? 330.0f : 430.0f;
|
||||
server->window().set_size(
|
||||
slint::LogicalSize(slint::Size<float>{width, 150.0f}));
|
||||
}
|
||||
|
||||
int RunCaptureMode(
|
||||
const CaptureOptions &options,
|
||||
slint::ComponentHandle<crossdesk::ui::MainWindow> &window,
|
||||
slint::ComponentHandle<crossdesk::ui::StreamWindow> &stream,
|
||||
slint::ComponentHandle<crossdesk::ui::ServerWindow> &server) {
|
||||
ConfigureMainCapturePage(window, options);
|
||||
stream->hide();
|
||||
server->hide();
|
||||
|
||||
slint::Window *capture_window = &window->window();
|
||||
if (options.page == "stream") {
|
||||
ConfigureStreamCapturePage(stream, options.language);
|
||||
window->hide();
|
||||
stream->show();
|
||||
capture_window = &stream->window();
|
||||
} else if (options.page == "server") {
|
||||
ConfigureServerCapturePage(server, options.language);
|
||||
window->hide();
|
||||
server->show();
|
||||
capture_window = &server->window();
|
||||
} else {
|
||||
window->show();
|
||||
}
|
||||
|
||||
if (!options.snapshot_path.empty()) {
|
||||
return WriteWindowSnapshot(*capture_window, options.snapshot_path.c_str())
|
||||
? 0
|
||||
: 7;
|
||||
}
|
||||
|
||||
if (options.page == "stream" || options.page == "server") {
|
||||
slint::run_event_loop();
|
||||
} else {
|
||||
window->run();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const CaptureOptions capture_options = LoadCaptureOptions();
|
||||
auto window = crossdesk::ui::MainWindow::create();
|
||||
auto stream = crossdesk::ui::StreamWindow::create();
|
||||
auto server = crossdesk::ui::ServerWindow::create();
|
||||
@@ -143,10 +446,15 @@ int main() {
|
||||
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
|
||||
std::vector{tab}));
|
||||
stream->set_custom_titlebar(true);
|
||||
stream->set_native_video_enabled(true);
|
||||
stream->set_window_maximized(true);
|
||||
assert(stream->get_tabs()->row_count() == 1);
|
||||
assert(stream->get_custom_titlebar());
|
||||
assert(stream->get_native_video_enabled());
|
||||
assert(stream->get_window_maximized());
|
||||
// Keep snapshots on the CPU-rendered UI path; the native Metal video view
|
||||
// is intentionally outside Slint's component snapshot.
|
||||
stream->set_native_video_enabled(false);
|
||||
if (const char *snapshot_path =
|
||||
std::getenv("CROSSDESK_STREAM_UI_SNAPSHOT")) {
|
||||
stream->set_window_maximized(false);
|
||||
@@ -276,5 +584,9 @@ int main() {
|
||||
server->invoke_controller_selected(0);
|
||||
assert(controller_selected);
|
||||
|
||||
if (capture_options.enabled) {
|
||||
return RunCaptureMode(capture_options, window, stream, server);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Vendored
+4
-2
@@ -32,6 +32,7 @@ package("slint")
|
||||
break
|
||||
end
|
||||
end
|
||||
local use_macos_metal = package:is_plat("macosx")
|
||||
local configs = {
|
||||
"-DSLINT_BUILD_TESTING=OFF",
|
||||
"-DSLINT_BUILD_EXAMPLES=OFF",
|
||||
@@ -41,10 +42,11 @@ package("slint")
|
||||
"-DSLINT_FEATURE_SYSTEM_TESTING=OFF",
|
||||
"-DSLINT_FEATURE_MCP=OFF",
|
||||
"-DSLINT_FEATURE_BACKEND_QT=OFF",
|
||||
"-DSLINT_FEATURE_RENDERER_SKIA=OFF",
|
||||
"-DSLINT_FEATURE_RENDERER_SKIA=" .. (use_macos_metal and "ON" or "OFF"),
|
||||
"-DSLINT_FEATURE_RENDERER_SKIA_OPENGL=OFF",
|
||||
"-DSLINT_FEATURE_RENDERER_SKIA_VULKAN=OFF",
|
||||
"-DSLINT_FEATURE_RENDERER_FEMTOVG=ON",
|
||||
"-DSLINT_FEATURE_RENDERER_FEMTOVG=" .. (use_macos_metal and "OFF" or "ON"),
|
||||
"-DSLINT_FEATURE_RENDERER_FEMTOVG_WGPU=OFF",
|
||||
"-DSLINT_FEATURE_RENDERER_SOFTWARE=ON",
|
||||
"-DSLINT_STYLE=fluent",
|
||||
"-DBUILD_SHARED_LIBS=ON"
|
||||
|
||||
+3
-3
@@ -84,8 +84,8 @@ function setup_platform_settings()
|
||||
add_ldflags("-Wl,-ld_classic")
|
||||
end
|
||||
add_cxflags("-Wno-unused-variable")
|
||||
add_frameworks("Cocoa", "OpenGL", "IOSurface", "ScreenCaptureKit",
|
||||
"AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio",
|
||||
"AudioToolbox")
|
||||
add_frameworks("Cocoa", "Metal", "QuartzCore", "IOSurface",
|
||||
"ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo",
|
||||
"CoreAudio", "AudioToolbox")
|
||||
end
|
||||
end
|
||||
|
||||
+9
-3
@@ -33,6 +33,7 @@ function setup_targets()
|
||||
target("common")
|
||||
set_kind("object")
|
||||
add_deps("rd_log")
|
||||
add_packages("libyuv")
|
||||
add_files("src/common/*.cpp")
|
||||
remove_files("src/common/rounded_corner_button.cpp")
|
||||
if is_os("macosx") then
|
||||
@@ -112,7 +113,8 @@ function setup_targets()
|
||||
add_cxxflags("/bigobj")
|
||||
end
|
||||
add_packages("slint")
|
||||
add_includedirs("src/gui/assets/fonts")
|
||||
add_includedirs("src/gui", "src/gui/assets/fonts",
|
||||
"src/gui/assets/localization")
|
||||
add_rules("slint")
|
||||
add_files("src/gui/ui/crossdesk_ui.slint")
|
||||
add_files("tests/slint_ui_smoke_test.cpp")
|
||||
@@ -275,12 +277,16 @@ function setup_targets()
|
||||
add_includedirs("src/gui", {public = true})
|
||||
if is_os("windows") then
|
||||
add_cxxflags("/bigobj")
|
||||
add_files("src/gui/platform/tray/win_tray.cpp")
|
||||
add_links("opengl32")
|
||||
add_files("src/gui/platform/tray/win_tray.cpp",
|
||||
"src/gui/platform/windows_opengl_video_renderer.cpp")
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
elseif is_os("macosx") then
|
||||
add_files("src/gui/runtime/*.mm", "src/gui/platform/tray/*.mm",
|
||||
"src/gui/platform/window_drag_mac.mm")
|
||||
"src/gui/platform/window_drag_mac.mm",
|
||||
"src/gui/platform/metal_video_renderer.mm")
|
||||
elseif is_os("linux") then
|
||||
add_links("GL")
|
||||
add_files("src/gui/platform/tray/linux_tray.cpp")
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user