mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-26 13:29:31 +08:00
[refactor] modularize Slint video rendering backends
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
#include "application/gui_application.h"
|
#include "application/gui_application.h"
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <libyuv.h>
|
|
||||||
#include <slint.h>
|
#include <slint.h>
|
||||||
#include <tinyfiledialogs.h>
|
#include <tinyfiledialogs.h>
|
||||||
|
|
||||||
@@ -29,15 +28,14 @@
|
|||||||
#include "crossdesk_ui.h"
|
#include "crossdesk_ui.h"
|
||||||
#include "fa_solid_900.h"
|
#include "fa_solid_900.h"
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#include "nv12_scaler.h"
|
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
|
#include "platform/video_renderer.h"
|
||||||
|
#include "rendering/slint_video_presenter.h"
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <GL/gl.h>
|
|
||||||
|
|
||||||
#include "platform/tray/win_tray.h"
|
#include "platform/tray/win_tray.h"
|
||||||
#elif defined(__APPLE__)
|
#elif defined(__APPLE__)
|
||||||
#include "platform/metal_video_renderer.h"
|
|
||||||
#include "platform/tray/mac_tray.h"
|
#include "platform/tray/mac_tray.h"
|
||||||
#include "platform/window_drag.h"
|
#include "platform/window_drag.h"
|
||||||
#elif defined(__linux__)
|
#elif defined(__linux__)
|
||||||
@@ -48,9 +46,6 @@
|
|||||||
|
|
||||||
#include "platform/tray/linux_tray.h"
|
#include "platform/tray/linux_tray.h"
|
||||||
#endif
|
#endif
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
|
||||||
#include "platform/opengl_video_renderer.h"
|
|
||||||
#endif
|
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "server_window_state.h"
|
#include "server_window_state.h"
|
||||||
#include "ui/ui_localization.h"
|
#include "ui/ui_localization.h"
|
||||||
@@ -62,42 +57,6 @@ namespace {
|
|||||||
|
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
#if !defined(__APPLE__)
|
|
||||||
constexpr GLint kGlClampToEdge = 0x812F;
|
|
||||||
constexpr float kStreamWindowCornerRadius = 12.0f;
|
|
||||||
#else
|
|
||||||
constexpr int kMetalAttachmentAttemptLimit = 30;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct VideoRenderSize {
|
|
||||||
int width = 0;
|
|
||||||
int height = 0;
|
|
||||||
|
|
||||||
bool operator==(const VideoRenderSize&) const = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
VideoRenderSize FitVideoToRenderArea(int source_width, int source_height,
|
|
||||||
int area_width, int area_height) {
|
|
||||||
if (source_width <= 0 || source_height <= 0 || area_width <= 0 ||
|
|
||||||
area_height <= 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const double scale =
|
|
||||||
std::min({1.0, static_cast<double>(area_width) / source_width,
|
|
||||||
static_cast<double>(area_height) / source_height});
|
|
||||||
int width = std::max(2, static_cast<int>(std::floor(source_width * scale)));
|
|
||||||
int height =
|
|
||||||
std::max(2, static_cast<int>(std::floor(source_height * scale)));
|
|
||||||
|
|
||||||
// NV12 chroma samples cover 2x2 pixels. Keep the render buffer even-sized
|
|
||||||
// so libyuv never needs to read a partial chroma sample at the edge.
|
|
||||||
width &= ~1;
|
|
||||||
height &= ~1;
|
|
||||||
return {std::min(width, source_width & ~1),
|
|
||||||
std::min(height, source_height & ~1)};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string CreatePasswordChangeRequestId(uint64_t sequence) {
|
std::string CreatePasswordChangeRequestId(uint64_t sequence) {
|
||||||
const auto timestamp = std::chrono::system_clock::now()
|
const auto timestamp = std::chrono::system_clock::now()
|
||||||
.time_since_epoch()
|
.time_since_epoch()
|
||||||
@@ -933,19 +892,7 @@ struct GuiApplication::SlintUi {
|
|||||||
std::shared_ptr<slint::VectorModel<slint::SharedString>>
|
std::shared_ptr<slint::VectorModel<slint::SharedString>>
|
||||||
controller_name_model =
|
controller_name_model =
|
||||||
std::make_shared<slint::VectorModel<slint::SharedString>>();
|
std::make_shared<slint::VectorModel<slint::SharedString>>();
|
||||||
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
|
std::unique_ptr<SlintVideoPresenter> video_presenter;
|
||||||
std::vector<uint8_t> scaled_video_frame;
|
|
||||||
std::vector<uint8_t> scaled_video_frame_scratch;
|
|
||||||
#if !defined(__APPLE__)
|
|
||||||
std::mutex video_gl_mutex;
|
|
||||||
std::vector<uint8_t> video_gl_conversion_frame;
|
|
||||||
std::vector<uint8_t> video_gl_pending_frame;
|
|
||||||
uint32_t video_gl_texture = 0;
|
|
||||||
VideoRenderSize video_gl_texture_size;
|
|
||||||
VideoRenderSize video_gl_image_size;
|
|
||||||
VideoRenderSize video_gl_pending_size;
|
|
||||||
bool video_gl_pending_dirty = false;
|
|
||||||
#endif
|
|
||||||
std::vector<std::string> tab_order;
|
std::vector<std::string> tab_order;
|
||||||
std::vector<std::string> tab_ids;
|
std::vector<std::string> tab_ids;
|
||||||
std::string tab_model_signature;
|
std::string tab_model_signature;
|
||||||
@@ -960,9 +907,6 @@ struct GuiApplication::SlintUi {
|
|||||||
WindowDragState server_drag;
|
WindowDragState server_drag;
|
||||||
int main_native_titlebar_attempts = 30;
|
int main_native_titlebar_attempts = 30;
|
||||||
int stream_live_resize_configuration_attempts = 0;
|
int stream_live_resize_configuration_attempts = 0;
|
||||||
#if defined(__APPLE__)
|
|
||||||
int stream_video_attachment_attempts = 0;
|
|
||||||
#endif
|
|
||||||
int stream_initial_position_attempts = 0;
|
int stream_initial_position_attempts = 0;
|
||||||
int server_initial_position_attempts = 0;
|
int server_initial_position_attempts = 0;
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
@@ -1117,14 +1061,9 @@ void GuiApplication::InitializeModules() {
|
|||||||
if (modules_inited_) {
|
if (modules_inited_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
// Renderer construction can log device, shader, or pipeline failures.
|
||||||
opengl_video_renderer_ = std::make_unique<OpenGlVideoRenderer>();
|
// Initialize it after Run() has selected the application's log directory.
|
||||||
#elif defined(__APPLE__)
|
video_renderer_ = CreateVideoRenderer();
|
||||||
// Metal setup can log device, shader, or pipeline failures. Initialize it
|
|
||||||
// here, after Run() has selected the application's log directory, rather
|
|
||||||
// than from GuiRuntime's constructor before InitializeLogger().
|
|
||||||
mac_metal_video_renderer_ = std::make_unique<MacMetalVideoRenderer>();
|
|
||||||
#endif
|
|
||||||
devices_.Initialize();
|
devices_.Initialize();
|
||||||
CreateConnectionPeer();
|
CreateConnectionPeer();
|
||||||
modules_inited_ = true;
|
modules_inited_ = true;
|
||||||
@@ -1159,6 +1098,8 @@ void GuiApplication::InitializeUi() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
ui_ = std::make_unique<SlintUi>();
|
ui_ = std::make_unique<SlintUi>();
|
||||||
|
ui_->video_presenter =
|
||||||
|
std::make_unique<SlintVideoPresenter>(*video_renderer_);
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
ui_->main->set_custom_titlebar(true);
|
ui_->main->set_custom_titlebar(true);
|
||||||
#elif defined(__linux__)
|
#elif defined(__linux__)
|
||||||
@@ -2263,9 +2204,6 @@ void GuiApplication::SyncStreamKeyboardFocus() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::SyncStreamWindow() {
|
void GuiApplication::SyncStreamWindow() {
|
||||||
#if defined(__APPLE__)
|
|
||||||
bool stream_window_created_this_sync = false;
|
|
||||||
#endif
|
|
||||||
bool has_sessions = false;
|
bool has_sessions = false;
|
||||||
{
|
{
|
||||||
std::shared_lock lock(remote_sessions_mutex_);
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
@@ -2277,15 +2215,7 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
|
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
|
||||||
#endif
|
#endif
|
||||||
RegisterFontAwesome((*ui_->stream)->window());
|
RegisterFontAwesome((*ui_->stream)->window());
|
||||||
#if defined(__APPLE__)
|
|
||||||
// Commit the first visible AppKit window as an ordinary opaque window.
|
|
||||||
// The process-wide Slint event loop is already running here, so
|
|
||||||
// appkit_view() may exist before show(); that does not mean the native
|
|
||||||
// titlebar style has been committed yet.
|
|
||||||
(*ui_->stream)->set_native_video_enabled(false);
|
|
||||||
#else
|
|
||||||
ConfigureStreamVideoRenderer();
|
ConfigureStreamVideoRenderer();
|
||||||
#endif
|
|
||||||
// Slint globals belong to a component tree. Force the next localization
|
// Slint globals belong to a component tree. Force the next localization
|
||||||
// pass to initialize the newly-created, independent stream window tree.
|
// pass to initialize the newly-created, independent stream window tree.
|
||||||
ui_->localized_language = -1;
|
ui_->localized_language = -1;
|
||||||
@@ -2295,15 +2225,11 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
(*ui_->stream)->set_stats_rows(ui_->stats_model);
|
(*ui_->stream)->set_stats_rows(ui_->stats_model);
|
||||||
BindStreamCallbacks();
|
BindStreamCallbacks();
|
||||||
(*ui_->stream)->show();
|
(*ui_->stream)->show();
|
||||||
#if defined(__APPLE__)
|
|
||||||
stream_window_created_this_sync = true;
|
|
||||||
#endif
|
|
||||||
// Repeat after the next layout pass because the backend resolves the
|
// Repeat after the next layout pass because the backend resolves the
|
||||||
// initial DPI and preferred size asynchronously on some platforms.
|
// initial DPI and preferred size asynchronously on some platforms.
|
||||||
ui_->stream_initial_position_attempts = 2;
|
ui_->stream_initial_position_attempts = 2;
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
ui_->stream_live_resize_configuration_attempts = 30;
|
ui_->stream_live_resize_configuration_attempts = 30;
|
||||||
ui_->stream_video_attachment_attempts = kMetalAttachmentAttemptLimit;
|
|
||||||
#endif
|
#endif
|
||||||
stream_window_created_ = true;
|
stream_window_created_ = true;
|
||||||
stream_window_inited_ = true;
|
stream_window_inited_ = true;
|
||||||
@@ -2323,24 +2249,10 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
--ui_->stream_live_resize_configuration_attempts;
|
--ui_->stream_live_resize_configuration_attempts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ui_->stream_video_attachment_attempts > 0 &&
|
|
||||||
!stream_window_created_this_sync) {
|
|
||||||
ConfigureStreamVideoRenderer();
|
|
||||||
const bool metal_unavailable =
|
|
||||||
!mac_metal_video_renderer_ || !mac_metal_video_renderer_->IsReady();
|
|
||||||
const bool metal_attached =
|
|
||||||
mac_metal_video_renderer_ && mac_metal_video_renderer_->IsAttached();
|
|
||||||
if (metal_unavailable) {
|
|
||||||
ui_->stream_video_attachment_attempts = 0;
|
|
||||||
} else if (metal_attached) {
|
|
||||||
video_frame_dirty_.store(true, std::memory_order_release);
|
|
||||||
ui_->stream_video_attachment_attempts = 0;
|
|
||||||
} else if (--ui_->stream_video_attachment_attempts == 0) {
|
|
||||||
(*ui_->stream)->set_native_video_enabled(false);
|
|
||||||
LOG_WARN("Unable to attach the Metal video surface; using CPU rendering");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
if (ui_->video_presenter && ui_->video_presenter->EnsureAttached()) {
|
||||||
|
video_frame_dirty_.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
if (ui_->stream_initial_position_attempts > 0) {
|
if (ui_->stream_initial_position_attempts > 0) {
|
||||||
if (PositionWindowAtCenter(
|
if (PositionWindowAtCenter(
|
||||||
(*ui_->stream)->window(), ui_->main->window(),
|
(*ui_->stream)->window(), ui_->main->window(),
|
||||||
@@ -2353,29 +2265,20 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
}
|
}
|
||||||
if (!has_sessions) {
|
if (!has_sessions) {
|
||||||
SetStreamKeyboardFocus(false);
|
SetStreamKeyboardFocus(false);
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
|
||||||
if (opengl_video_renderer_) {
|
|
||||||
opengl_video_renderer_->SetSelectedStream({});
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
void* stream_view = (*ui_->stream)->window().appkit_view();
|
void* stream_view = (*ui_->stream)->window().appkit_view();
|
||||||
SetStreamWindowFullscreen(false);
|
SetStreamWindowFullscreen(false);
|
||||||
UnregisterStreamWindow(stream_view);
|
UnregisterStreamWindow(stream_view);
|
||||||
if (mac_metal_video_renderer_) {
|
|
||||||
mac_metal_video_renderer_->SetSelectedStream({});
|
|
||||||
mac_metal_video_renderer_->Detach();
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
if (ui_->video_presenter) {
|
||||||
|
ui_->video_presenter->Detach();
|
||||||
|
}
|
||||||
(*ui_->stream)->hide();
|
(*ui_->stream)->hide();
|
||||||
ui_->stream.reset();
|
ui_->stream.reset();
|
||||||
stream_window_created_ = false;
|
stream_window_created_ = false;
|
||||||
stream_window_inited_ = false;
|
stream_window_inited_ = false;
|
||||||
ui_->stream_initial_position_attempts = 0;
|
ui_->stream_initial_position_attempts = 0;
|
||||||
ui_->stream_live_resize_configuration_attempts = 0;
|
ui_->stream_live_resize_configuration_attempts = 0;
|
||||||
#if defined(__APPLE__)
|
|
||||||
ui_->stream_video_attachment_attempts = 0;
|
|
||||||
#endif
|
|
||||||
focused_remote_id_.clear();
|
focused_remote_id_.clear();
|
||||||
controlled_remote_id_.clear();
|
controlled_remote_id_.clear();
|
||||||
ui_->tab_order.clear();
|
ui_->tab_order.clear();
|
||||||
@@ -2592,7 +2495,7 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::SyncStreamVideoFrame() {
|
void GuiApplication::SyncStreamVideoFrame() {
|
||||||
if (!ui_ || !ui_->stream) {
|
if (!ui_ || !ui_->stream || !ui_->video_presenter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto props = SelectedSession();
|
auto props = SelectedSession();
|
||||||
@@ -2600,430 +2503,46 @@ void GuiApplication::SyncStreamVideoFrame() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
SlintVideoPresenter::Frame frame;
|
||||||
if (mac_metal_video_renderer_ && mac_metal_video_renderer_->IsReady() &&
|
frame.remote_id = props->remote_id_;
|
||||||
mac_metal_video_renderer_->IsAttached()) {
|
|
||||||
const double top_inset =
|
|
||||||
!fullscreen_button_pressed_ && ui_->tab_ids.size() > 1 ? 30.0 : 0.0;
|
|
||||||
const auto render_outcome = mac_metal_video_renderer_->RenderLatest(
|
|
||||||
props->remote_id_, top_inset, !fullscreen_button_pressed_);
|
|
||||||
if (render_outcome.result ==
|
|
||||||
MacMetalVideoRenderer::RenderResult::rendered) {
|
|
||||||
if (!(*ui_->stream)->get_has_frame()) {
|
|
||||||
(*ui_->stream)->set_has_frame(true);
|
|
||||||
}
|
|
||||||
if (!(*ui_->stream)->get_receiving_text().empty()) {
|
|
||||||
(*ui_->stream)->set_receiving_text("");
|
|
||||||
}
|
|
||||||
if (render_outcome.width > 0 && render_outcome.height > 0 &&
|
|
||||||
render_outcome.sequence > 0) {
|
|
||||||
(*ui_->stream)
|
|
||||||
->set_stats_resolution(
|
|
||||||
UiText(std::to_string(render_outcome.width) + "x" +
|
|
||||||
std::to_string(render_outcome.height)));
|
|
||||||
if (ui_->displayed_frame_sequence[props->remote_id_] !=
|
|
||||||
render_outcome.sequence) {
|
|
||||||
ui_->displayed_frame_sequence[props->remote_id_] =
|
|
||||||
render_outcome.sequence;
|
|
||||||
++props->frame_count_;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (render_outcome.result == MacMetalVideoRenderer::RenderResult::empty) {
|
|
||||||
if ((*ui_->stream)->get_has_frame()) {
|
|
||||||
(*ui_->stream)->set_has_frame(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (render_outcome.result == MacMetalVideoRenderer::RenderResult::idle) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// A transient Metal error must not change renderer ownership mid-window.
|
|
||||||
// Keep the last frame and retry on the next UI tick.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
|
||||||
if (opengl_video_renderer_ &&
|
|
||||||
opengl_video_renderer_->IsReady()) {
|
|
||||||
opengl_video_renderer_->SetSelectedStream(props->remote_id_);
|
|
||||||
SubmitCachedFrameToOpenGl(props);
|
|
||||||
if (!(*ui_->stream)->get_native_video_enabled()) {
|
|
||||||
(*ui_->stream)->set_native_video_enabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
int width = 0;
|
|
||||||
int height = 0;
|
|
||||||
uint64_t sequence = 0;
|
|
||||||
{
|
|
||||||
std::lock_guard lock(props->video_frame_mutex_);
|
|
||||||
width = props->video_width_;
|
|
||||||
height = props->video_height_;
|
|
||||||
sequence = props->video_frame_sequence_;
|
|
||||||
}
|
|
||||||
const bool has_frame = width > 0 && height > 0 && sequence > 0;
|
|
||||||
(*ui_->stream)->set_has_frame(has_frame);
|
|
||||||
if (has_frame) {
|
|
||||||
(*ui_->stream)->set_receiving_text("");
|
|
||||||
(*ui_->stream)
|
|
||||||
->set_stats_resolution(UiText(std::to_string(width) + "x" +
|
|
||||||
std::to_string(height)));
|
|
||||||
if (ui_->displayed_frame_sequence[props->remote_id_] != sequence) {
|
|
||||||
ui_->displayed_frame_sequence[props->remote_id_] = sequence;
|
|
||||||
++props->frame_count_;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(*ui_->stream)->window().request_redraw();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((*ui_->stream)->get_native_video_enabled()) {
|
|
||||||
(*ui_->stream)->set_native_video_enabled(false);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
std::shared_ptr<std::vector<unsigned char>> frame;
|
|
||||||
int width = 0;
|
|
||||||
int height = 0;
|
|
||||||
uint64_t sequence = 0;
|
|
||||||
{
|
{
|
||||||
std::lock_guard lock(props->video_frame_mutex_);
|
std::lock_guard lock(props->video_frame_mutex_);
|
||||||
frame = props->front_frame_;
|
frame.nv12 = props->front_frame_;
|
||||||
width = props->video_width_;
|
frame.width = props->video_width_;
|
||||||
height = props->video_height_;
|
frame.height = props->video_height_;
|
||||||
sequence = props->video_frame_sequence_;
|
frame.sequence = props->video_frame_sequence_;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t nv12_size = static_cast<size_t>(width) * height * 3 / 2;
|
const auto result = ui_->video_presenter->Present(frame);
|
||||||
if (!frame || width <= 0 || height <= 0 || frame->size() < nv12_size) {
|
if (result.width > 0 && result.height > 0) {
|
||||||
(*ui_->stream)->set_has_frame(false);
|
(*ui_->stream)
|
||||||
return;
|
->set_stats_resolution(UiText(std::to_string(result.width) + "x" +
|
||||||
|
std::to_string(result.height)));
|
||||||
}
|
}
|
||||||
if (ui_->displayed_frame_sequence[props->remote_id_] == sequence) {
|
if (result.new_frame) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto window_size = (*ui_->stream)->window().size();
|
|
||||||
const VideoRenderSize render_size = FitVideoToRenderArea(
|
|
||||||
width, height, static_cast<int>(window_size.width),
|
|
||||||
static_cast<int>(window_size.height));
|
|
||||||
const uint8_t* y_plane = frame->data();
|
|
||||||
const uint8_t* uv_plane =
|
|
||||||
frame->data() + static_cast<size_t>(width) * height;
|
|
||||||
int output_width = width;
|
|
||||||
int output_height = height;
|
|
||||||
|
|
||||||
if (render_size.width > 0 && render_size.height > 0 &&
|
|
||||||
(render_size.width != width || render_size.height != height)) {
|
|
||||||
const size_t scaled_nv12_size =
|
|
||||||
static_cast<size_t>(render_size.width) * render_size.height * 3 / 2;
|
|
||||||
ui_->scaled_video_frame.resize(scaled_nv12_size);
|
|
||||||
uint8_t* scaled_y = ui_->scaled_video_frame.data();
|
|
||||||
uint8_t* scaled_uv =
|
|
||||||
scaled_y +
|
|
||||||
static_cast<size_t>(render_size.width) * render_size.height;
|
|
||||||
if (ScaleNv12ViaI420(
|
|
||||||
y_plane, width, uv_plane, width, width, height, scaled_y,
|
|
||||||
render_size.width, scaled_uv, render_size.width, render_size.width,
|
|
||||||
render_size.height, libyuv::kFilterBox,
|
|
||||||
&ui_->scaled_video_frame_scratch) == 0) {
|
|
||||||
y_plane = scaled_y;
|
|
||||||
uv_plane = scaled_uv;
|
|
||||||
output_width = render_size.width;
|
|
||||||
output_height = render_size.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !defined(__APPLE__)
|
|
||||||
uint32_t texture_id = 0;
|
|
||||||
{
|
|
||||||
std::lock_guard lock(ui_->video_gl_mutex);
|
|
||||||
texture_id = ui_->video_gl_texture;
|
|
||||||
}
|
|
||||||
if (texture_id != 0) {
|
|
||||||
ui_->video_gl_conversion_frame.resize(
|
|
||||||
static_cast<size_t>(output_width) * output_height * 4);
|
|
||||||
if (libyuv::NV12ToABGR(
|
|
||||||
y_plane, output_width, uv_plane, output_width,
|
|
||||||
ui_->video_gl_conversion_frame.data(), output_width * 4,
|
|
||||||
output_width, output_height) != 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
std::lock_guard lock(ui_->video_gl_mutex);
|
|
||||||
if (ui_->video_gl_texture == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
texture_id = ui_->video_gl_texture;
|
|
||||||
ui_->video_gl_pending_frame.swap(ui_->video_gl_conversion_frame);
|
|
||||||
ui_->video_gl_pending_size = {output_width, output_height};
|
|
||||||
ui_->video_gl_pending_dirty = true;
|
|
||||||
}
|
|
||||||
const VideoRenderSize output_size{output_width, output_height};
|
|
||||||
if (ui_->video_gl_image_size != output_size) {
|
|
||||||
(*ui_->stream)
|
|
||||||
->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture(
|
|
||||||
texture_id,
|
|
||||||
slint::Size<uint32_t>{static_cast<uint32_t>(output_width),
|
|
||||||
static_cast<uint32_t>(output_height)}));
|
|
||||||
ui_->video_gl_image_size = output_size;
|
|
||||||
}
|
|
||||||
(*ui_->stream)->set_has_frame(true);
|
|
||||||
(*ui_->stream)->set_receiving_text("");
|
|
||||||
(*ui_->stream)->window().request_redraw();
|
|
||||||
ui_->displayed_frame_sequence[props->remote_id_] = sequence;
|
|
||||||
++props->frame_count_;
|
++props->frame_count_;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(output_width,
|
|
||||||
output_height);
|
|
||||||
if (libyuv::NV12ToRAW(
|
|
||||||
y_plane, output_width, uv_plane, output_width,
|
|
||||||
reinterpret_cast<uint8_t*>(pixels.begin()), output_width * 3,
|
|
||||||
output_width, output_height) != 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(*ui_->stream)->set_frame(slint::Image(std::move(pixels)));
|
|
||||||
(*ui_->stream)->set_has_frame(true);
|
|
||||||
(*ui_->stream)->set_receiving_text("");
|
|
||||||
ui_->displayed_frame_sequence[props->remote_id_] = sequence;
|
|
||||||
++props->frame_count_;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::ConfigureStreamVideoRenderer() {
|
void GuiApplication::ConfigureStreamVideoRenderer() {
|
||||||
#if defined(__APPLE__)
|
if (!ui_ || !ui_->stream || !ui_->video_presenter) {
|
||||||
if (!ui_ || !ui_->stream) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bool native_video_enabled =
|
|
||||||
mac_metal_video_renderer_ && mac_metal_video_renderer_->IsReady();
|
|
||||||
if (!native_video_enabled) {
|
|
||||||
(*ui_->stream)->set_native_video_enabled(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (void* view = (*ui_->stream)->window().appkit_view()) {
|
|
||||||
// Slint applies transparency through a property binding. Set it before
|
|
||||||
// restoring the AppKit titlebar so the later operation wins regardless of
|
|
||||||
// whether the backend commits the binding synchronously or next tick.
|
|
||||||
(*ui_->stream)->set_native_video_enabled(true);
|
|
||||||
if (!mac_metal_video_renderer_->Attach(view)) {
|
|
||||||
(*ui_->stream)->set_native_video_enabled(false);
|
|
||||||
} else if (auto selected_session = SelectedSession()) {
|
|
||||||
// Metal owns a separate monotonic sequence, so discard a CPU-renderer
|
|
||||||
// sequence recorded during the short window-attachment gap.
|
|
||||||
ui_->displayed_frame_sequence.erase(selected_session->remote_id_);
|
|
||||||
if (mac_metal_video_renderer_->SetSelectedStream(
|
|
||||||
selected_session->remote_id_)) {
|
|
||||||
video_frame_dirty_.store(true, std::memory_order_release);
|
|
||||||
}
|
|
||||||
SubmitCachedFrameToMetal(selected_session);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Keep the initial window opaque until winit has created its native
|
|
||||||
// NSWindow. Creating it transparent opts into a full-size titlebar whose
|
|
||||||
// title field sits underneath the standard traffic-light buttons.
|
|
||||||
(*ui_->stream)->set_native_video_enabled(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
#else
|
|
||||||
if (!ui_ || !ui_->stream) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto error = (*ui_->stream)->window().set_rendering_notifier(
|
|
||||||
[this](slint::RenderingState state, slint::GraphicsAPI graphics_api) {
|
|
||||||
if (!ui_ || graphics_api != slint::GraphicsAPI::NativeOpenGL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::lock_guard lock(ui_->video_gl_mutex);
|
ui_->video_presenter->PrepareWindow(
|
||||||
if (state == slint::RenderingState::RenderingSetup) {
|
*ui_->stream,
|
||||||
GLuint texture = 0;
|
[this] {
|
||||||
glGenTextures(1, &texture);
|
SlintVideoPresenter::SurfaceState state;
|
||||||
if (texture == 0) {
|
state.selected_stream = focused_remote_id_;
|
||||||
return;
|
state.fullscreen = fullscreen_button_pressed_;
|
||||||
}
|
if (ui_) {
|
||||||
GLint previous_texture = 0;
|
state.tab_count = ui_->tab_ids.size();
|
||||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
|
||||||
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, kGlClampToEdge);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge);
|
|
||||||
glBindTexture(GL_TEXTURE_2D,
|
|
||||||
static_cast<GLuint>(previous_texture));
|
|
||||||
ui_->video_gl_texture = texture;
|
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
|
||||||
if (opengl_video_renderer_ &&
|
|
||||||
opengl_video_renderer_->Setup()) {
|
|
||||||
opengl_video_renderer_->SetSelectedStream(
|
|
||||||
focused_remote_id_);
|
|
||||||
video_frame_dirty_.store(true, std::memory_order_release);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return state;
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
},
|
||||||
if (state == slint::RenderingState::BeforeRendering &&
|
[this] { video_frame_dirty_.store(true, std::memory_order_release); });
|
||||||
opengl_video_renderer_ &&
|
|
||||||
opengl_video_renderer_->IsReady() && ui_->stream &&
|
|
||||||
(*ui_->stream)->get_native_video_enabled()) {
|
|
||||||
const auto window_size = (*ui_->stream)->window().size();
|
|
||||||
const float scale_factor =
|
|
||||||
std::max(1.0f, (*ui_->stream)->window().scale_factor());
|
|
||||||
const int top_inset =
|
|
||||||
!fullscreen_button_pressed_ && ui_->tab_ids.size() > 1
|
|
||||||
? static_cast<int>(std::lround(30.0f * scale_factor))
|
|
||||||
: 0;
|
|
||||||
const bool rounded_window =
|
|
||||||
(*ui_->stream)->get_custom_titlebar() &&
|
|
||||||
!fullscreen_button_pressed_ &&
|
|
||||||
!(*ui_->stream)->window().is_maximized();
|
|
||||||
const int corner_radius =
|
|
||||||
rounded_window
|
|
||||||
? static_cast<int>(std::lround(
|
|
||||||
kStreamWindowCornerRadius * scale_factor))
|
|
||||||
: 0;
|
|
||||||
opengl_video_renderer_->RenderLatest(
|
|
||||||
focused_remote_id_, static_cast<int>(window_size.width),
|
|
||||||
static_cast<int>(window_size.height), top_inset, corner_radius);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (state == slint::RenderingState::BeforeRendering &&
|
|
||||||
ui_->video_gl_texture != 0 && ui_->video_gl_pending_dirty &&
|
|
||||||
!ui_->video_gl_pending_frame.empty()) {
|
|
||||||
GLint previous_texture = 0;
|
|
||||||
GLint previous_unpack_alignment = 0;
|
|
||||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
|
||||||
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, ui_->video_gl_texture);
|
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
||||||
if (ui_->video_gl_texture_size != ui_->video_gl_pending_size) {
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
|
|
||||||
ui_->video_gl_pending_size.width,
|
|
||||||
ui_->video_gl_pending_size.height, 0, GL_RGBA,
|
|
||||||
GL_UNSIGNED_BYTE, ui_->video_gl_pending_frame.data());
|
|
||||||
ui_->video_gl_texture_size = ui_->video_gl_pending_size;
|
|
||||||
} else {
|
|
||||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0,
|
|
||||||
ui_->video_gl_pending_size.width,
|
|
||||||
ui_->video_gl_pending_size.height, GL_RGBA,
|
|
||||||
GL_UNSIGNED_BYTE,
|
|
||||||
ui_->video_gl_pending_frame.data());
|
|
||||||
}
|
|
||||||
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
|
|
||||||
glBindTexture(GL_TEXTURE_2D,
|
|
||||||
static_cast<GLuint>(previous_texture));
|
|
||||||
ui_->video_gl_pending_dirty = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == slint::RenderingState::RenderingTeardown) {
|
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
|
||||||
if (opengl_video_renderer_) {
|
|
||||||
opengl_video_renderer_->Teardown();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (ui_->video_gl_texture != 0) {
|
|
||||||
const GLuint texture = ui_->video_gl_texture;
|
|
||||||
glDeleteTextures(1, &texture);
|
|
||||||
}
|
|
||||||
ui_->video_gl_texture = 0;
|
|
||||||
ui_->video_gl_texture_size = {};
|
|
||||||
ui_->video_gl_image_size = {};
|
|
||||||
ui_->video_gl_pending_size = {};
|
|
||||||
ui_->video_gl_pending_dirty = false;
|
|
||||||
ui_->video_gl_pending_frame.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (error.has_value()) {
|
|
||||||
LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers");
|
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
|
||||||
} else if (opengl_video_renderer_) {
|
|
||||||
opengl_video_renderer_->SetSelectedStream(focused_remote_id_);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
|
||||||
void GuiApplication::SubmitCachedFrameToOpenGl(
|
|
||||||
const std::shared_ptr<RemoteSession>& session) {
|
|
||||||
if (!session || !opengl_video_renderer_ ||
|
|
||||||
!opengl_video_renderer_->IsReady() ||
|
|
||||||
session->connection_status_.load() != ConnectionStatus::Connected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<std::vector<unsigned char>> cached_frame;
|
|
||||||
int cached_width = 0;
|
|
||||||
int cached_height = 0;
|
|
||||||
{
|
|
||||||
std::lock_guard lock(session->video_frame_mutex_);
|
|
||||||
if (session->front_frame_ && !session->front_frame_->empty()) {
|
|
||||||
cached_frame = session->front_frame_;
|
|
||||||
cached_width = session->video_width_;
|
|
||||||
cached_height = session->video_height_;
|
|
||||||
} else if (session->thumbnail_frame_ &&
|
|
||||||
!session->thumbnail_frame_->empty()) {
|
|
||||||
cached_frame = session->thumbnail_frame_;
|
|
||||||
cached_width = session->thumbnail_width_;
|
|
||||||
cached_height = session->thumbnail_height_;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cached_frame && cached_width > 0 && cached_height > 0 &&
|
|
||||||
opengl_video_renderer_->SubmitCachedNv12(
|
|
||||||
session->remote_id_, cached_frame->data(), cached_frame->size(),
|
|
||||||
cached_width, cached_height) ==
|
|
||||||
OpenGlVideoRenderer::SubmitResult::submitted) {
|
|
||||||
video_frame_dirty_.store(true, std::memory_order_release);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
void GuiApplication::SubmitCachedFrameToMetal(
|
|
||||||
const std::shared_ptr<RemoteSession>& session) {
|
|
||||||
if (!session || !mac_metal_video_renderer_ ||
|
|
||||||
!mac_metal_video_renderer_->IsReady() ||
|
|
||||||
session->connection_status_.load() != ConnectionStatus::Connected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<std::vector<unsigned char>> cached_frame;
|
|
||||||
int cached_width = 0;
|
|
||||||
int cached_height = 0;
|
|
||||||
{
|
|
||||||
std::lock_guard lock(session->video_frame_mutex_);
|
|
||||||
if (session->front_frame_ && !session->front_frame_->empty()) {
|
|
||||||
cached_frame = session->front_frame_;
|
|
||||||
cached_width = session->video_width_;
|
|
||||||
cached_height = session->video_height_;
|
|
||||||
} else if (session->thumbnail_frame_ &&
|
|
||||||
!session->thumbnail_frame_->empty()) {
|
|
||||||
cached_frame = session->thumbnail_frame_;
|
|
||||||
cached_width = session->thumbnail_width_;
|
|
||||||
cached_height = session->thumbnail_height_;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cached_frame && cached_width > 0 && cached_height > 0 &&
|
|
||||||
mac_metal_video_renderer_->SubmitCachedNv12(
|
|
||||||
session->remote_id_, cached_frame->data(), cached_frame->size(),
|
|
||||||
cached_width, cached_height) ==
|
|
||||||
MacMetalVideoRenderer::SubmitResult::submitted) {
|
|
||||||
video_frame_dirty_.store(true, std::memory_order_release);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void GuiApplication::ScheduleNextVideoFrame() {
|
void GuiApplication::ScheduleNextVideoFrame() {
|
||||||
if (!ui_) {
|
if (!ui_) {
|
||||||
return;
|
return;
|
||||||
@@ -3039,28 +2558,19 @@ void GuiApplication::ScheduleNextVideoFrame() {
|
|||||||
if (now >= next_video_frame_time_) {
|
if (now >= next_video_frame_time_) {
|
||||||
const bool frame_dirty =
|
const bool frame_dirty =
|
||||||
video_frame_dirty_.exchange(false, std::memory_order_acq_rel);
|
video_frame_dirty_.exchange(false, std::memory_order_acq_rel);
|
||||||
#if defined(__APPLE__)
|
const bool native_render_needed = ui_->stream && ui_->video_presenter &&
|
||||||
const bool native_render_needed =
|
ui_->video_presenter->NeedsRedraw();
|
||||||
mac_metal_video_renderer_ && mac_metal_video_renderer_->IsReady() &&
|
|
||||||
mac_metal_video_renderer_->IsAttached() && ui_->stream &&
|
|
||||||
mac_metal_video_renderer_->NeedsSurfaceRedraw();
|
|
||||||
if (frame_dirty || native_render_needed) {
|
if (frame_dirty || native_render_needed) {
|
||||||
SyncStreamVideoFrame();
|
SyncStreamVideoFrame();
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
if (frame_dirty) {
|
|
||||||
SyncStreamVideoFrame();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
do {
|
do {
|
||||||
next_video_frame_time_ += frame_interval;
|
next_video_frame_time_ += frame_interval;
|
||||||
} while (next_video_frame_time_ <= now);
|
} while (next_video_frame_time_ <= now);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto after_render = std::chrono::steady_clock::now();
|
const auto after_render = std::chrono::steady_clock::now();
|
||||||
const auto remaining =
|
const auto remaining = std::max(next_video_frame_time_ - after_render,
|
||||||
std::max(next_video_frame_time_ - after_render,
|
std::chrono::steady_clock::duration::zero());
|
||||||
std::chrono::steady_clock::duration::zero());
|
|
||||||
const auto delay_ns =
|
const auto delay_ns =
|
||||||
std::chrono::duration_cast<std::chrono::nanoseconds>(remaining).count();
|
std::chrono::duration_cast<std::chrono::nanoseconds>(remaining).count();
|
||||||
const auto delay_ms = std::max<int64_t>(1, (delay_ns + 999'999) / 1'000'000);
|
const auto delay_ms = std::max<int64_t>(1, (delay_ns + 999'999) / 1'000'000);
|
||||||
@@ -3332,20 +2842,17 @@ void GuiApplication::SelectStreamTab(int index) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const std::string selected_remote_id = ui_->tab_ids[index];
|
const std::string selected_remote_id = ui_->tab_ids[index];
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
const bool selection_changed = focused_remote_id_ != selected_remote_id;
|
const bool selection_changed = focused_remote_id_ != selected_remote_id;
|
||||||
bool renderer_selection_changed = false;
|
bool renderer_selection_changed = false;
|
||||||
#endif
|
|
||||||
if (!controlled_remote_id_.empty() &&
|
if (!controlled_remote_id_.empty() &&
|
||||||
controlled_remote_id_ != selected_remote_id) {
|
controlled_remote_id_ != selected_remote_id) {
|
||||||
keyboard_.ForceReleasePressedKeys();
|
keyboard_.ForceReleasePressedKeys();
|
||||||
}
|
}
|
||||||
focused_remote_id_ = selected_remote_id;
|
focused_remote_id_ = selected_remote_id;
|
||||||
controlled_remote_id_ = selected_remote_id;
|
controlled_remote_id_ = selected_remote_id;
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
if (ui_->video_presenter) {
|
||||||
if (opengl_video_renderer_) {
|
|
||||||
renderer_selection_changed =
|
renderer_selection_changed =
|
||||||
opengl_video_renderer_->SetSelectedStream(selected_remote_id);
|
ui_->video_presenter->SelectStream(selected_remote_id);
|
||||||
}
|
}
|
||||||
if (renderer_selection_changed) {
|
if (renderer_selection_changed) {
|
||||||
video_frame_dirty_.store(true, std::memory_order_release);
|
video_frame_dirty_.store(true, std::memory_order_release);
|
||||||
@@ -3353,18 +2860,6 @@ void GuiApplication::SelectStreamTab(int index) {
|
|||||||
if (selection_changed && ui_->stream) {
|
if (selection_changed && ui_->stream) {
|
||||||
(*ui_->stream)->set_has_frame(false);
|
(*ui_->stream)->set_has_frame(false);
|
||||||
}
|
}
|
||||||
#elif defined(__APPLE__)
|
|
||||||
if (mac_metal_video_renderer_) {
|
|
||||||
renderer_selection_changed =
|
|
||||||
mac_metal_video_renderer_->SetSelectedStream(selected_remote_id);
|
|
||||||
}
|
|
||||||
if (renderer_selection_changed) {
|
|
||||||
video_frame_dirty_.store(true, std::memory_order_release);
|
|
||||||
}
|
|
||||||
if (selection_changed && ui_->stream) {
|
|
||||||
(*ui_->stream)->set_has_frame(false);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
std::shared_ptr<RemoteSession> selected_session;
|
std::shared_ptr<RemoteSession> selected_session;
|
||||||
{
|
{
|
||||||
std::shared_lock lock(remote_sessions_mutex_);
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
@@ -3377,20 +2872,11 @@ void GuiApplication::SelectStreamTab(int index) {
|
|||||||
if (selected != remote_sessions_.end()) {
|
if (selected != remote_sessions_.end()) {
|
||||||
selected_session = selected->second;
|
selected_session = selected->second;
|
||||||
}
|
}
|
||||||
start_keyboard_capturer_ =
|
start_keyboard_capturer_ = selected_session &&
|
||||||
selected_session && selected_session->control_mouse_ &&
|
selected_session->control_mouse_ &&
|
||||||
selected_session->connection_status_.load() ==
|
selected_session->connection_status_.load() ==
|
||||||
ConnectionStatus::Connected;
|
ConnectionStatus::Connected;
|
||||||
}
|
}
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
|
||||||
if (renderer_selection_changed) {
|
|
||||||
SubmitCachedFrameToOpenGl(selected_session);
|
|
||||||
}
|
|
||||||
#elif defined(__APPLE__)
|
|
||||||
if (renderer_selection_changed) {
|
|
||||||
SubmitCachedFrameToMetal(selected_session);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) {
|
void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) {
|
||||||
@@ -3434,21 +2920,15 @@ void GuiApplication::CloseStreamTab(const std::string& remote_id) {
|
|||||||
std::unique_lock lock(remote_sessions_mutex_);
|
std::unique_lock lock(remote_sessions_mutex_);
|
||||||
remote_sessions_.erase(remote_id);
|
remote_sessions_.erase(remote_id);
|
||||||
}
|
}
|
||||||
ui_->displayed_frame_sequence.erase(remote_id);
|
if (ui_->video_presenter) {
|
||||||
|
ui_->video_presenter->ForgetStream(remote_id);
|
||||||
|
}
|
||||||
if (focused_remote_id_ == remote_id) {
|
if (focused_remote_id_ == remote_id) {
|
||||||
focused_remote_id_.clear();
|
focused_remote_id_.clear();
|
||||||
controlled_remote_id_.clear();
|
controlled_remote_id_.clear();
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
if (ui_->video_presenter && ui_->video_presenter->SelectStream({})) {
|
||||||
if (opengl_video_renderer_ &&
|
|
||||||
opengl_video_renderer_->SetSelectedStream({})) {
|
|
||||||
video_frame_dirty_.store(true, std::memory_order_release);
|
video_frame_dirty_.store(true, std::memory_order_release);
|
||||||
}
|
}
|
||||||
#elif defined(__APPLE__)
|
|
||||||
if (mac_metal_video_renderer_ &&
|
|
||||||
mac_metal_video_renderer_->SetSelectedStream({})) {
|
|
||||||
video_frame_dirty_.store(true, std::memory_order_release);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3642,20 +3122,14 @@ void GuiApplication::Cleanup() {
|
|||||||
WaitForThumbnailSaveTasks();
|
WaitForThumbnailSaveTasks();
|
||||||
devices_.DestroyAudioOutput();
|
devices_.DestroyAudioOutput();
|
||||||
if (ui_->stream) {
|
if (ui_->stream) {
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
|
||||||
if (opengl_video_renderer_) {
|
|
||||||
opengl_video_renderer_->SetSelectedStream({});
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
void* stream_view = (*ui_->stream)->window().appkit_view();
|
void* stream_view = (*ui_->stream)->window().appkit_view();
|
||||||
SetStreamWindowFullscreen(false);
|
SetStreamWindowFullscreen(false);
|
||||||
UnregisterStreamWindow(stream_view);
|
UnregisterStreamWindow(stream_view);
|
||||||
if (mac_metal_video_renderer_) {
|
|
||||||
mac_metal_video_renderer_->SetSelectedStream({});
|
|
||||||
mac_metal_video_renderer_->Detach();
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
if (ui_->video_presenter) {
|
||||||
|
ui_->video_presenter->Detach();
|
||||||
|
}
|
||||||
(*ui_->stream)->hide();
|
(*ui_->stream)->hide();
|
||||||
ui_->stream.reset();
|
ui_->stream.reset();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,14 +41,6 @@ private:
|
|||||||
void SyncStreamVideoFrame();
|
void SyncStreamVideoFrame();
|
||||||
void ScheduleNextVideoFrame();
|
void ScheduleNextVideoFrame();
|
||||||
void ConfigureStreamVideoRenderer();
|
void ConfigureStreamVideoRenderer();
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
|
||||||
void SubmitCachedFrameToOpenGl(
|
|
||||||
const std::shared_ptr<RemoteSession>& session);
|
|
||||||
#endif
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
void SubmitCachedFrameToMetal(
|
|
||||||
const std::shared_ptr<RemoteSession>& session);
|
|
||||||
#endif
|
|
||||||
void SyncStreamKeyboardFocus();
|
void SyncStreamKeyboardFocus();
|
||||||
void SetStreamKeyboardFocus(bool focused);
|
void SetStreamKeyboardFocus(bool focused);
|
||||||
void SyncServerWindow();
|
void SyncServerWindow();
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "platform/video_renderer.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
// macOS video compositor used by CrossDesk only. MiniRTC continues to expose
|
// macOS video compositor used by CrossDesk only. MiniRTC continues to expose
|
||||||
@@ -15,51 +17,31 @@ namespace crossdesk {
|
|||||||
// copy into shared Metal buffers and converts NV12 to RGB in the fragment
|
// copy into shared Metal buffers and converts NV12 to RGB in the fragment
|
||||||
// shader. AppKit attachment and rendering are main-thread operations, while
|
// shader. AppKit attachment and rendering are main-thread operations, while
|
||||||
// SubmitNv12() is safe to call from MiniRTC's decode callback thread.
|
// SubmitNv12() is safe to call from MiniRTC's decode callback thread.
|
||||||
class MacMetalVideoRenderer {
|
class MacMetalVideoRenderer final : public VideoRenderer {
|
||||||
public:
|
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();
|
~MacMetalVideoRenderer() override;
|
||||||
|
|
||||||
MacMetalVideoRenderer(const MacMetalVideoRenderer&) = delete;
|
MacMetalVideoRenderer(const MacMetalVideoRenderer&) = delete;
|
||||||
MacMetalVideoRenderer& operator=(const MacMetalVideoRenderer&) = delete;
|
MacMetalVideoRenderer& operator=(const MacMetalVideoRenderer&) = delete;
|
||||||
|
|
||||||
bool IsReady() const;
|
bool IsReady() const override;
|
||||||
|
bool IsActive() const override;
|
||||||
|
|
||||||
// The selected stream is the only stream uploaded. Frames for background
|
// The selected stream is the only stream uploaded. Frames for background
|
||||||
// tabs are intentionally dropped to avoid wasting memory bandwidth.
|
// tabs are intentionally dropped to avoid wasting memory bandwidth.
|
||||||
bool SetSelectedStream(std::string remote_id);
|
bool SetSelectedStream(std::string remote_id) override;
|
||||||
void DiscardStream(std::string_view remote_id);
|
void DiscardStream(std::string_view remote_id) override;
|
||||||
|
|
||||||
// Copies one tightly packed NV12 frame into a free shared Metal slot.
|
// Copies one tightly packed NV12 frame into a free shared Metal slot.
|
||||||
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t* data,
|
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t* data,
|
||||||
size_t size, int width, int height);
|
size_t size, int width, int height) override;
|
||||||
|
|
||||||
// Seeds a newly selected stream from its retained CPU snapshot without
|
// Seeds a newly selected stream from its retained CPU snapshot without
|
||||||
// replacing a newer decoded frame that is already queued or rendering.
|
// replacing a newer decoded frame that is already queued or rendering.
|
||||||
SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
||||||
const uint8_t* data, size_t size, int width,
|
const uint8_t* data, size_t size, int width,
|
||||||
int height);
|
int height) override;
|
||||||
|
|
||||||
// Attaches a native CAMetalLayer-backed sibling below Slint's NSView and
|
// Attaches a native CAMetalLayer-backed sibling below Slint's NSView and
|
||||||
// restores the containing AppKit window's ordinary opaque titlebar.
|
// restores the containing AppKit window's ordinary opaque titlebar.
|
||||||
@@ -84,7 +66,7 @@ class MacMetalVideoRenderer {
|
|||||||
// a second per-frame CPU copy during normal playback.
|
// a second per-frame CPU copy during normal playback.
|
||||||
bool CopyLatestNv12(std::string_view remote_id,
|
bool CopyLatestNv12(std::string_view remote_id,
|
||||||
std::vector<unsigned char>* output, int* width,
|
std::vector<unsigned char>* output, int* width,
|
||||||
int* height) const;
|
int* height) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SubmitResult SubmitNv12Internal(std::string_view remote_id,
|
SubmitResult SubmitNv12Internal(std::string_view remote_id,
|
||||||
|
|||||||
@@ -602,6 +602,10 @@ bool MacMetalVideoRenderer::IsReady() const {
|
|||||||
return impl_ && impl_->Ready();
|
return impl_ && impl_->Ready();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MacMetalVideoRenderer::IsActive() const {
|
||||||
|
return IsReady() && IsAttached();
|
||||||
|
}
|
||||||
|
|
||||||
bool MacMetalVideoRenderer::SetSelectedStream(std::string remote_id) {
|
bool MacMetalVideoRenderer::SetSelectedStream(std::string remote_id) {
|
||||||
if (!impl_) {
|
if (!impl_) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -541,6 +541,8 @@ bool OpenGlVideoRenderer::IsReady() const {
|
|||||||
return impl_->ready.load(std::memory_order_acquire);
|
return impl_->ready.load(std::memory_order_acquire);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool OpenGlVideoRenderer::IsActive() const { return IsReady(); }
|
||||||
|
|
||||||
bool OpenGlVideoRenderer::SetSelectedStream(std::string remote_id) {
|
bool OpenGlVideoRenderer::SetSelectedStream(std::string remote_id) {
|
||||||
std::lock_guard lock(impl_->frames->mutex);
|
std::lock_guard lock(impl_->frames->mutex);
|
||||||
if (impl_->frames->selected_stream == remote_id) {
|
if (impl_->frames->selected_stream == remote_id) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "platform/video_renderer.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
// NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux.
|
// NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux.
|
||||||
@@ -15,31 +17,10 @@ namespace crossdesk {
|
|||||||
// thread. The Slint UI thread uploads the newest frame as Y and UV textures and
|
// 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
|
// performs color conversion and scaling in a fragment shader before Slint
|
||||||
// draws its overlay.
|
// draws its overlay.
|
||||||
class OpenGlVideoRenderer {
|
class OpenGlVideoRenderer final : public VideoRenderer {
|
||||||
public:
|
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
OpenGlVideoRenderer();
|
OpenGlVideoRenderer();
|
||||||
~OpenGlVideoRenderer();
|
~OpenGlVideoRenderer() override;
|
||||||
|
|
||||||
OpenGlVideoRenderer(const OpenGlVideoRenderer &) = delete;
|
OpenGlVideoRenderer(const OpenGlVideoRenderer &) = delete;
|
||||||
OpenGlVideoRenderer &operator=(const OpenGlVideoRenderer &) = delete;
|
OpenGlVideoRenderer &operator=(const OpenGlVideoRenderer &) = delete;
|
||||||
@@ -48,18 +29,19 @@ public:
|
|||||||
// RenderingSetup and RenderingTeardown notifier states respectively.
|
// RenderingSetup and RenderingTeardown notifier states respectively.
|
||||||
bool Setup();
|
bool Setup();
|
||||||
void Teardown();
|
void Teardown();
|
||||||
bool IsReady() const;
|
bool IsReady() const override;
|
||||||
|
bool IsActive() const override;
|
||||||
|
|
||||||
// Only the selected stream is queued for upload. Frames for other tabs are
|
// Only the selected stream is queued for upload. Frames for other tabs are
|
||||||
// retained by GuiRuntime at a throttled rate for thumbnail/tab restoration.
|
// retained by GuiRuntime at a throttled rate for thumbnail/tab restoration.
|
||||||
bool SetSelectedStream(std::string remote_id);
|
bool SetSelectedStream(std::string remote_id) override;
|
||||||
void DiscardStream(std::string_view remote_id);
|
void DiscardStream(std::string_view remote_id) override;
|
||||||
|
|
||||||
// Thread-safe; called from MiniRTC's decode callback thread.
|
// Thread-safe; called from MiniRTC's decode callback thread.
|
||||||
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t *data,
|
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t *data,
|
||||||
size_t size, int width, int height);
|
size_t size, int width, int height) override;
|
||||||
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
|
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
|
||||||
size_t size, int width, int height);
|
size_t size, int width, int height) override;
|
||||||
|
|
||||||
// Draws below Slint while its OpenGL context is current. All dimensions are
|
// Draws below Slint while its OpenGL context is current. All dimensions are
|
||||||
// physical pixels in the window client area. corner_radius_pixels clips the
|
// physical pixels in the window client area. corner_radius_pixels clips the
|
||||||
@@ -72,7 +54,7 @@ public:
|
|||||||
// second per-frame CPU copy during normal playback.
|
// second per-frame CPU copy during normal playback.
|
||||||
bool CopyLatestNv12(std::string_view remote_id,
|
bool CopyLatestNv12(std::string_view remote_id,
|
||||||
std::vector<unsigned char> *output, int *width,
|
std::vector<unsigned char> *output, int *width,
|
||||||
int *height) const;
|
int *height) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SubmitResult SubmitNv12Internal(std::string_view remote_id,
|
SubmitResult SubmitNv12Internal(std::string_view remote_id,
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#ifndef CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_
|
||||||
|
#define CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// Platform-independent contract for the native stream video renderer.
|
||||||
|
//
|
||||||
|
// Frame submission and snapshot methods may be called from MiniRTC callback
|
||||||
|
// threads. Surface setup, rendering and teardown remain backend-specific and
|
||||||
|
// are driven by the Slint presentation module on the UI thread.
|
||||||
|
class VideoRenderer {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
virtual ~VideoRenderer() = default;
|
||||||
|
|
||||||
|
virtual bool IsReady() const = 0;
|
||||||
|
|
||||||
|
// True only while the renderer can accept and present native frames. Metal
|
||||||
|
// additionally requires attachment to an AppKit view; OpenGL requires an
|
||||||
|
// active Slint rendering context.
|
||||||
|
virtual bool IsActive() const = 0;
|
||||||
|
|
||||||
|
virtual bool SetSelectedStream(std::string remote_id) = 0;
|
||||||
|
virtual void DiscardStream(std::string_view remote_id) = 0;
|
||||||
|
|
||||||
|
virtual SubmitResult SubmitNv12(std::string_view remote_id,
|
||||||
|
const uint8_t* data, size_t size, int width,
|
||||||
|
int height) = 0;
|
||||||
|
virtual SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
||||||
|
const uint8_t* data, size_t size,
|
||||||
|
int width, int height) = 0;
|
||||||
|
|
||||||
|
virtual bool CopyLatestNv12(std::string_view remote_id,
|
||||||
|
std::vector<unsigned char>* output, int* width,
|
||||||
|
int* height) const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exactly one factory implementation is linked for the target platform.
|
||||||
|
std::unique_ptr<VideoRenderer> CreateVideoRenderer();
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif // CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#include "platform/video_renderer.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "platform/metal_video_renderer.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
std::unique_ptr<VideoRenderer> CreateVideoRenderer() {
|
||||||
|
return std::make_unique<MacMetalVideoRenderer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "platform/opengl_video_renderer.h"
|
||||||
|
#include "platform/video_renderer.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
std::unique_ptr<VideoRenderer> CreateVideoRenderer() {
|
||||||
|
return std::make_unique<OpenGlVideoRenderer>();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
#include "rendering/slint_video_presenter.h"
|
||||||
|
|
||||||
|
#include <libyuv.h>
|
||||||
|
#include <slint.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "nv12_scaler.h"
|
||||||
|
#include "platform/video_renderer.h"
|
||||||
|
#include "rd_log.h"
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
#include "platform/metal_video_renderer.h"
|
||||||
|
#else
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
#include <GL/gl.h>
|
||||||
|
|
||||||
|
#include "platform/opengl_video_renderer.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#if !defined(__APPLE__)
|
||||||
|
constexpr GLint kGlClampToEdge = 0x812F;
|
||||||
|
constexpr float kStreamWindowCornerRadius = 12.0f;
|
||||||
|
#else
|
||||||
|
constexpr int kMetalAttachmentAttemptLimit = 30;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct VideoRenderSize {
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
|
||||||
|
bool operator==(const VideoRenderSize&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
VideoRenderSize FitVideoToRenderArea(int source_width, int source_height,
|
||||||
|
int area_width, int area_height) {
|
||||||
|
if (source_width <= 0 || source_height <= 0 || area_width <= 0 ||
|
||||||
|
area_height <= 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const double scale =
|
||||||
|
std::min({1.0, static_cast<double>(area_width) / source_width,
|
||||||
|
static_cast<double>(area_height) / source_height});
|
||||||
|
int width = std::max(2, static_cast<int>(std::floor(source_width * scale)));
|
||||||
|
int height = std::max(2, static_cast<int>(std::floor(source_height * scale)));
|
||||||
|
|
||||||
|
width &= ~1;
|
||||||
|
height &= ~1;
|
||||||
|
return {std::min(width, source_width & ~1),
|
||||||
|
std::min(height, source_height & ~1)};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct SlintVideoPresenter::Impl {
|
||||||
|
explicit Impl(VideoRenderer& renderer) : renderer(renderer) {}
|
||||||
|
|
||||||
|
VideoRenderer& renderer;
|
||||||
|
slint::ComponentHandle<ui::StreamWindow>* stream = nullptr;
|
||||||
|
StateProvider state_provider;
|
||||||
|
DirtyCallback dirty_callback;
|
||||||
|
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
|
||||||
|
std::unordered_map<std::string, uint64_t> seeded_frame_sequence;
|
||||||
|
std::vector<uint8_t> scaled_video_frame;
|
||||||
|
std::vector<uint8_t> scaled_video_frame_scratch;
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
int attachment_attempts = 0;
|
||||||
|
bool skip_attachment_once = false;
|
||||||
|
#else
|
||||||
|
std::mutex gl_mutex;
|
||||||
|
std::vector<uint8_t> gl_conversion_frame;
|
||||||
|
std::vector<uint8_t> gl_pending_frame;
|
||||||
|
uint32_t gl_texture = 0;
|
||||||
|
VideoRenderSize gl_texture_size;
|
||||||
|
VideoRenderSize gl_image_size;
|
||||||
|
VideoRenderSize gl_pending_size;
|
||||||
|
bool gl_pending_dirty = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SurfaceState CurrentState() const {
|
||||||
|
return state_provider ? state_provider() : SurfaceState{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void MarkDirty() const {
|
||||||
|
if (dirty_callback) {
|
||||||
|
dirty_callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MarkPresented(const std::string& remote_id, uint64_t sequence) {
|
||||||
|
if (remote_id.empty() || sequence == 0 ||
|
||||||
|
displayed_frame_sequence[remote_id] == sequence) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
displayed_frame_sequence[remote_id] = sequence;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(__APPLE__)
|
||||||
|
void ConfigureOpenGlNotifier() {
|
||||||
|
if (!stream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto error = (*stream)->window().set_rendering_notifier(
|
||||||
|
[this](slint::RenderingState state, slint::GraphicsAPI graphics_api) {
|
||||||
|
if (graphics_api != slint::GraphicsAPI::NativeOpenGL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock(gl_mutex);
|
||||||
|
auto* opengl_renderer =
|
||||||
|
dynamic_cast<OpenGlVideoRenderer*>(&renderer);
|
||||||
|
if (state == slint::RenderingState::RenderingSetup) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
if (texture == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLint previous_texture = 0;
|
||||||
|
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
||||||
|
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, kGlClampToEdge);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
|
||||||
|
gl_texture = texture;
|
||||||
|
if (opengl_renderer && opengl_renderer->Setup()) {
|
||||||
|
const std::string selected_stream =
|
||||||
|
CurrentState().selected_stream;
|
||||||
|
opengl_renderer->SetSelectedStream(selected_stream);
|
||||||
|
seeded_frame_sequence.erase(selected_stream);
|
||||||
|
MarkDirty();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == slint::RenderingState::BeforeRendering && stream &&
|
||||||
|
opengl_renderer && opengl_renderer->IsReady() &&
|
||||||
|
(*stream)->get_native_video_enabled()) {
|
||||||
|
const SurfaceState surface = CurrentState();
|
||||||
|
const auto window_size = (*stream)->window().size();
|
||||||
|
const float scale_factor =
|
||||||
|
std::max(1.0f, (*stream)->window().scale_factor());
|
||||||
|
const int top_inset =
|
||||||
|
!surface.fullscreen && surface.tab_count > 1
|
||||||
|
? static_cast<int>(std::lround(30.0f * scale_factor))
|
||||||
|
: 0;
|
||||||
|
const bool rounded_window = (*stream)->get_custom_titlebar() &&
|
||||||
|
!surface.fullscreen &&
|
||||||
|
!(*stream)->window().is_maximized();
|
||||||
|
const int corner_radius =
|
||||||
|
rounded_window ? static_cast<int>(std::lround(
|
||||||
|
kStreamWindowCornerRadius * scale_factor))
|
||||||
|
: 0;
|
||||||
|
opengl_renderer->RenderLatest(
|
||||||
|
surface.selected_stream, static_cast<int>(window_size.width),
|
||||||
|
static_cast<int>(window_size.height), top_inset, corner_radius);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == slint::RenderingState::BeforeRendering &&
|
||||||
|
gl_texture != 0 && gl_pending_dirty &&
|
||||||
|
!gl_pending_frame.empty()) {
|
||||||
|
GLint previous_texture = 0;
|
||||||
|
GLint previous_unpack_alignment = 0;
|
||||||
|
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
||||||
|
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, gl_texture);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
if (gl_texture_size != gl_pending_size) {
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, gl_pending_size.width,
|
||||||
|
gl_pending_size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
gl_pending_frame.data());
|
||||||
|
gl_texture_size = gl_pending_size;
|
||||||
|
} else {
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, gl_pending_size.width,
|
||||||
|
gl_pending_size.height, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
gl_pending_frame.data());
|
||||||
|
}
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
|
||||||
|
gl_pending_dirty = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == slint::RenderingState::RenderingTeardown) {
|
||||||
|
if (opengl_renderer) {
|
||||||
|
opengl_renderer->Teardown();
|
||||||
|
}
|
||||||
|
if (gl_texture != 0) {
|
||||||
|
const GLuint texture = gl_texture;
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
}
|
||||||
|
gl_texture = 0;
|
||||||
|
gl_texture_size = {};
|
||||||
|
gl_image_size = {};
|
||||||
|
gl_pending_size = {};
|
||||||
|
gl_pending_dirty = false;
|
||||||
|
gl_pending_frame.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error.has_value()) {
|
||||||
|
LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers");
|
||||||
|
} else {
|
||||||
|
renderer.SetSelectedStream(CurrentState().selected_stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
SlintVideoPresenter::SlintVideoPresenter(VideoRenderer& renderer)
|
||||||
|
: impl_(std::make_unique<Impl>(renderer)) {}
|
||||||
|
|
||||||
|
SlintVideoPresenter::~SlintVideoPresenter() = default;
|
||||||
|
|
||||||
|
void SlintVideoPresenter::PrepareWindow(
|
||||||
|
slint::ComponentHandle<ui::StreamWindow>& stream,
|
||||||
|
StateProvider state_provider, DirtyCallback dirty_callback) {
|
||||||
|
impl_->stream = &stream;
|
||||||
|
impl_->state_provider = std::move(state_provider);
|
||||||
|
impl_->dirty_callback = std::move(dirty_callback);
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
stream->set_native_video_enabled(false);
|
||||||
|
impl_->attachment_attempts = kMetalAttachmentAttemptLimit;
|
||||||
|
impl_->skip_attachment_once = true;
|
||||||
|
#else
|
||||||
|
impl_->ConfigureOpenGlNotifier();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SlintVideoPresenter::EnsureAttached() {
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
if (!impl_->stream || impl_->attachment_attempts <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (impl_->skip_attachment_once) {
|
||||||
|
impl_->skip_attachment_once = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* metal_renderer =
|
||||||
|
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
|
||||||
|
if (!metal_renderer) {
|
||||||
|
impl_->attachment_attempts = 0;
|
||||||
|
(*impl_->stream)->set_native_video_enabled(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!metal_renderer->IsReady()) {
|
||||||
|
impl_->attachment_attempts = 0;
|
||||||
|
(*impl_->stream)->set_native_video_enabled(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (metal_renderer->IsAttached()) {
|
||||||
|
impl_->attachment_attempts = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (void* view = (*impl_->stream)->window().appkit_view()) {
|
||||||
|
(*impl_->stream)->set_native_video_enabled(true);
|
||||||
|
if (metal_renderer->Attach(view)) {
|
||||||
|
impl_->attachment_attempts = 0;
|
||||||
|
const SurfaceState surface = impl_->CurrentState();
|
||||||
|
impl_->displayed_frame_sequence.erase(surface.selected_stream);
|
||||||
|
impl_->seeded_frame_sequence.erase(surface.selected_stream);
|
||||||
|
if (metal_renderer->SetSelectedStream(surface.selected_stream)) {
|
||||||
|
impl_->MarkDirty();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(*impl_->stream)->set_native_video_enabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (--impl_->attachment_attempts == 0) {
|
||||||
|
(*impl_->stream)->set_native_video_enabled(false);
|
||||||
|
LOG_WARN("Unable to attach the Metal video surface; using CPU rendering");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SlintVideoPresenter::NeedsRedraw() const {
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
const auto* metal_renderer =
|
||||||
|
dynamic_cast<const MacMetalVideoRenderer*>(&impl_->renderer);
|
||||||
|
return metal_renderer && metal_renderer->IsActive() &&
|
||||||
|
metal_renderer->NeedsSurfaceRedraw();
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SlintVideoPresenter::SelectStream(std::string remote_id) {
|
||||||
|
const bool changed = impl_->renderer.SetSelectedStream(remote_id);
|
||||||
|
if (changed && !remote_id.empty()) {
|
||||||
|
impl_->seeded_frame_sequence.erase(remote_id);
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
|
||||||
|
const Frame& frame) {
|
||||||
|
PresentResult result;
|
||||||
|
if (!impl_->stream || frame.remote_id.empty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (impl_->renderer.IsActive() && frame.nv12 && frame.width > 0 &&
|
||||||
|
frame.height > 0 && frame.sequence > 0 &&
|
||||||
|
impl_->seeded_frame_sequence[frame.remote_id] != frame.sequence) {
|
||||||
|
const auto submit_result = impl_->renderer.SubmitCachedNv12(
|
||||||
|
frame.remote_id, frame.nv12->data(), frame.nv12->size(), frame.width,
|
||||||
|
frame.height);
|
||||||
|
if (submit_result == VideoRenderer::SubmitResult::submitted ||
|
||||||
|
submit_result == VideoRenderer::SubmitResult::dropped) {
|
||||||
|
impl_->seeded_frame_sequence[frame.remote_id] = frame.sequence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
auto* metal_renderer =
|
||||||
|
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
|
||||||
|
if (metal_renderer && metal_renderer->IsActive()) {
|
||||||
|
const SurfaceState surface = impl_->CurrentState();
|
||||||
|
const double top_inset =
|
||||||
|
!surface.fullscreen && surface.tab_count > 1 ? 30.0 : 0.0;
|
||||||
|
const auto outcome = metal_renderer->RenderLatest(
|
||||||
|
frame.remote_id, top_inset, !surface.fullscreen);
|
||||||
|
if (outcome.result == VideoRenderer::RenderResult::rendered) {
|
||||||
|
(*impl_->stream)->set_has_frame(true);
|
||||||
|
(*impl_->stream)->set_receiving_text("");
|
||||||
|
result.width = outcome.width;
|
||||||
|
result.height = outcome.height;
|
||||||
|
result.new_frame =
|
||||||
|
impl_->MarkPresented(frame.remote_id, outcome.sequence);
|
||||||
|
} else if (outcome.result == VideoRenderer::RenderResult::empty) {
|
||||||
|
(*impl_->stream)->set_has_frame(false);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
auto* opengl_renderer =
|
||||||
|
dynamic_cast<OpenGlVideoRenderer*>(&impl_->renderer);
|
||||||
|
if (opengl_renderer && opengl_renderer->IsReady()) {
|
||||||
|
opengl_renderer->SetSelectedStream(frame.remote_id);
|
||||||
|
if (!(*impl_->stream)->get_native_video_enabled()) {
|
||||||
|
(*impl_->stream)->set_native_video_enabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool has_frame =
|
||||||
|
frame.width > 0 && frame.height > 0 && frame.sequence > 0;
|
||||||
|
(*impl_->stream)->set_has_frame(has_frame);
|
||||||
|
if (has_frame) {
|
||||||
|
(*impl_->stream)->set_receiving_text("");
|
||||||
|
result.width = frame.width;
|
||||||
|
result.height = frame.height;
|
||||||
|
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||||
|
}
|
||||||
|
(*impl_->stream)->window().request_redraw();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if ((*impl_->stream)->get_native_video_enabled()) {
|
||||||
|
(*impl_->stream)->set_native_video_enabled(false);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const size_t nv12_size =
|
||||||
|
static_cast<size_t>(frame.width) * frame.height * 3 / 2;
|
||||||
|
if (!frame.nv12 || frame.width <= 0 || frame.height <= 0 ||
|
||||||
|
frame.nv12->size() < nv12_size) {
|
||||||
|
(*impl_->stream)->set_has_frame(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (impl_->displayed_frame_sequence[frame.remote_id] == frame.sequence) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto window_size = (*impl_->stream)->window().size();
|
||||||
|
const VideoRenderSize render_size = FitVideoToRenderArea(
|
||||||
|
frame.width, frame.height, static_cast<int>(window_size.width),
|
||||||
|
static_cast<int>(window_size.height));
|
||||||
|
const uint8_t* y_plane = frame.nv12->data();
|
||||||
|
const uint8_t* uv_plane =
|
||||||
|
frame.nv12->data() + static_cast<size_t>(frame.width) * frame.height;
|
||||||
|
int output_width = frame.width;
|
||||||
|
int output_height = frame.height;
|
||||||
|
|
||||||
|
if (render_size.width > 0 && render_size.height > 0 &&
|
||||||
|
(render_size.width != frame.width ||
|
||||||
|
render_size.height != frame.height)) {
|
||||||
|
const size_t scaled_nv12_size =
|
||||||
|
static_cast<size_t>(render_size.width) * render_size.height * 3 / 2;
|
||||||
|
impl_->scaled_video_frame.resize(scaled_nv12_size);
|
||||||
|
uint8_t* scaled_y = impl_->scaled_video_frame.data();
|
||||||
|
uint8_t* scaled_uv =
|
||||||
|
scaled_y + static_cast<size_t>(render_size.width) * render_size.height;
|
||||||
|
if (ScaleNv12ViaI420(y_plane, frame.width, uv_plane, frame.width,
|
||||||
|
frame.width, frame.height, scaled_y, render_size.width,
|
||||||
|
scaled_uv, render_size.width, render_size.width,
|
||||||
|
render_size.height, libyuv::kFilterBox,
|
||||||
|
&impl_->scaled_video_frame_scratch) == 0) {
|
||||||
|
y_plane = scaled_y;
|
||||||
|
uv_plane = scaled_uv;
|
||||||
|
output_width = render_size.width;
|
||||||
|
output_height = render_size.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(__APPLE__)
|
||||||
|
uint32_t texture_id = 0;
|
||||||
|
{
|
||||||
|
std::lock_guard lock(impl_->gl_mutex);
|
||||||
|
texture_id = impl_->gl_texture;
|
||||||
|
}
|
||||||
|
if (texture_id != 0) {
|
||||||
|
impl_->gl_conversion_frame.resize(static_cast<size_t>(output_width) *
|
||||||
|
output_height * 4);
|
||||||
|
if (libyuv::NV12ToABGR(y_plane, output_width, uv_plane, output_width,
|
||||||
|
impl_->gl_conversion_frame.data(), output_width * 4,
|
||||||
|
output_width, output_height) != 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard lock(impl_->gl_mutex);
|
||||||
|
if (impl_->gl_texture == 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
texture_id = impl_->gl_texture;
|
||||||
|
impl_->gl_pending_frame.swap(impl_->gl_conversion_frame);
|
||||||
|
impl_->gl_pending_size = {output_width, output_height};
|
||||||
|
impl_->gl_pending_dirty = true;
|
||||||
|
}
|
||||||
|
const VideoRenderSize output_size{output_width, output_height};
|
||||||
|
if (impl_->gl_image_size != output_size) {
|
||||||
|
(*impl_->stream)
|
||||||
|
->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture(
|
||||||
|
texture_id,
|
||||||
|
slint::Size<uint32_t>{static_cast<uint32_t>(output_width),
|
||||||
|
static_cast<uint32_t>(output_height)}));
|
||||||
|
impl_->gl_image_size = output_size;
|
||||||
|
}
|
||||||
|
(*impl_->stream)->set_has_frame(true);
|
||||||
|
(*impl_->stream)->set_receiving_text("");
|
||||||
|
(*impl_->stream)->window().request_redraw();
|
||||||
|
result.width = frame.width;
|
||||||
|
result.height = frame.height;
|
||||||
|
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(output_width,
|
||||||
|
output_height);
|
||||||
|
if (libyuv::NV12ToRAW(y_plane, output_width, uv_plane, output_width,
|
||||||
|
reinterpret_cast<uint8_t*>(pixels.begin()),
|
||||||
|
output_width * 3, output_width, output_height) != 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*impl_->stream)->set_frame(slint::Image(std::move(pixels)));
|
||||||
|
(*impl_->stream)->set_has_frame(true);
|
||||||
|
(*impl_->stream)->set_receiving_text("");
|
||||||
|
result.width = frame.width;
|
||||||
|
result.height = frame.height;
|
||||||
|
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SlintVideoPresenter::ForgetStream(const std::string& remote_id) {
|
||||||
|
impl_->displayed_frame_sequence.erase(remote_id);
|
||||||
|
impl_->seeded_frame_sequence.erase(remote_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SlintVideoPresenter::Detach() {
|
||||||
|
impl_->renderer.SetSelectedStream({});
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
if (auto* metal_renderer =
|
||||||
|
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer)) {
|
||||||
|
metal_renderer->Detach();
|
||||||
|
}
|
||||||
|
impl_->attachment_attempts = 0;
|
||||||
|
impl_->skip_attachment_once = false;
|
||||||
|
#endif
|
||||||
|
impl_->stream = nullptr;
|
||||||
|
impl_->state_provider = {};
|
||||||
|
impl_->dirty_callback = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||||
|
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "crossdesk_ui.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
class VideoRenderer;
|
||||||
|
|
||||||
|
// Owns the bridge between decoded NV12 frames, the selected native graphics
|
||||||
|
// backend and Slint's StreamWindow. Platform macros and graphics-API lifecycle
|
||||||
|
// details are intentionally contained in the implementation file.
|
||||||
|
class SlintVideoPresenter {
|
||||||
|
public:
|
||||||
|
struct SurfaceState {
|
||||||
|
std::string selected_stream;
|
||||||
|
bool fullscreen = false;
|
||||||
|
size_t tab_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Frame {
|
||||||
|
std::string remote_id;
|
||||||
|
std::shared_ptr<std::vector<unsigned char>> nv12;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
uint64_t sequence = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PresentResult {
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
bool new_frame = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
using StateProvider = std::function<SurfaceState()>;
|
||||||
|
using DirtyCallback = std::function<void()>;
|
||||||
|
|
||||||
|
explicit SlintVideoPresenter(VideoRenderer& renderer);
|
||||||
|
~SlintVideoPresenter();
|
||||||
|
|
||||||
|
SlintVideoPresenter(const SlintVideoPresenter&) = delete;
|
||||||
|
SlintVideoPresenter& operator=(const SlintVideoPresenter&) = delete;
|
||||||
|
|
||||||
|
// Called before the window is shown. OpenGL installs Slint's rendering
|
||||||
|
// notifier here; macOS deliberately defers native view attachment until a
|
||||||
|
// later UI pass, after AppKit has committed the ordinary window style.
|
||||||
|
void PrepareWindow(slint::ComponentHandle<ui::StreamWindow>& stream,
|
||||||
|
StateProvider state_provider,
|
||||||
|
DirtyCallback dirty_callback);
|
||||||
|
|
||||||
|
// Returns true only when a deferred native surface became attached during
|
||||||
|
// this call, allowing the caller to seed it from a cached frame once.
|
||||||
|
bool EnsureAttached();
|
||||||
|
bool NeedsRedraw() const;
|
||||||
|
|
||||||
|
bool SelectStream(std::string remote_id);
|
||||||
|
PresentResult Present(const Frame& frame);
|
||||||
|
void ForgetStream(const std::string& remote_id);
|
||||||
|
void Detach();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||||
@@ -11,12 +11,7 @@
|
|||||||
#include "display_stream_id.h"
|
#include "display_stream_id.h"
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
#include "platform/video_renderer.h"
|
||||||
#include "platform/opengl_video_renderer.h"
|
|
||||||
#endif
|
|
||||||
#ifdef __APPLE__
|
|
||||||
#include "platform/metal_video_renderer.h"
|
|
||||||
#endif
|
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
@@ -229,21 +224,14 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
|||||||
frame_snapshot = props->front_frame_;
|
frame_snapshot = props->front_frame_;
|
||||||
video_width = props->video_width_;
|
video_width = props->video_width_;
|
||||||
video_height = props->video_height_;
|
video_height = props->video_height_;
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
if ((!frame_snapshot || frame_snapshot->empty()) &&
|
if ((!frame_snapshot || frame_snapshot->empty()) &&
|
||||||
props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) {
|
props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) {
|
||||||
frame_snapshot = props->thumbnail_frame_;
|
frame_snapshot = props->thumbnail_frame_;
|
||||||
video_width = props->thumbnail_width_;
|
video_width = props->thumbnail_width_;
|
||||||
video_height = props->thumbnail_height_;
|
video_height = props->thumbnail_height_;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
auto* native_renderer = video_renderer_.get();
|
||||||
#if defined(__APPLE__)
|
|
||||||
auto* native_renderer = mac_metal_video_renderer_.get();
|
|
||||||
#else
|
|
||||||
auto* native_renderer = opengl_video_renderer_.get();
|
|
||||||
#endif
|
|
||||||
if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) {
|
if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) {
|
||||||
auto native_snapshot = std::make_shared<std::vector<unsigned char>>();
|
auto native_snapshot = std::make_shared<std::vector<unsigned char>>();
|
||||||
if (native_renderer->CopyLatestNv12(props->remote_id_,
|
if (native_renderer->CopyLatestNv12(props->remote_id_,
|
||||||
@@ -252,7 +240,6 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
|||||||
frame_snapshot = std::move(native_snapshot);
|
frame_snapshot = std::move(native_snapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||||
video_height > 0) {
|
video_height > 0) {
|
||||||
@@ -276,12 +263,10 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
if (native_renderer) {
|
if (native_renderer) {
|
||||||
native_renderer->DiscardStream(props->remote_id_);
|
native_renderer->DiscardStream(props->remote_id_);
|
||||||
video_frame_dirty_.store(true, std::memory_order_release);
|
video_frame_dirty_.store(true, std::memory_order_release);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
if (props->peer_) {
|
if (props->peer_) {
|
||||||
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
|
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
|
||||||
@@ -343,12 +328,10 @@ void GuiRuntime::ResetRemoteSessionResources(
|
|||||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||||
props->front_frame_.reset();
|
props->front_frame_.reset();
|
||||||
props->back_frame_.reset();
|
props->back_frame_.reset();
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
props->thumbnail_frame_.reset();
|
props->thumbnail_frame_.reset();
|
||||||
props->thumbnail_width_ = 0;
|
props->thumbnail_width_ = 0;
|
||||||
props->thumbnail_height_ = 0;
|
props->thumbnail_height_ = 0;
|
||||||
props->background_snapshot_time_ = {};
|
props->background_snapshot_time_ = {};
|
||||||
#endif
|
|
||||||
props->video_width_ = 0;
|
props->video_width_ = 0;
|
||||||
props->video_height_ = 0;
|
props->video_height_ = 0;
|
||||||
props->video_size_ = 0;
|
props->video_size_ = 0;
|
||||||
|
|||||||
@@ -12,12 +12,7 @@
|
|||||||
|
|
||||||
#include "display_stream_id.h"
|
#include "display_stream_id.h"
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
#include "platform/video_renderer.h"
|
||||||
#include "platform/opengl_video_renderer.h"
|
|
||||||
#endif
|
|
||||||
#ifdef __APPLE__
|
|
||||||
#include "platform/metal_video_renderer.h"
|
|
||||||
#endif
|
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|||||||
@@ -15,12 +15,7 @@
|
|||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
class VideoRenderer;
|
||||||
class OpenGlVideoRenderer;
|
|
||||||
#endif
|
|
||||||
#ifdef __APPLE__
|
|
||||||
class MacMetalVideoRenderer;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
|
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
|
||||||
// state, but no window lifecycle, ImGui view, or transport callback methods.
|
// state, but no window lifecycle, ImGui view, or transport callback methods.
|
||||||
@@ -90,12 +85,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
|||||||
KeyboardController keyboard_;
|
KeyboardController keyboard_;
|
||||||
PeerEventHandler peer_events_;
|
PeerEventHandler peer_events_;
|
||||||
std::atomic<bool> video_frame_dirty_{false};
|
std::atomic<bool> video_frame_dirty_{false};
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
std::unique_ptr<VideoRenderer> video_renderer_;
|
||||||
std::unique_ptr<OpenGlVideoRenderer> opengl_video_renderer_;
|
|
||||||
#endif
|
|
||||||
#ifdef __APPLE__
|
|
||||||
std::unique_ptr<MacMetalVideoRenderer> mac_metal_video_renderer_;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class ClipboardController;
|
friend class ClipboardController;
|
||||||
|
|||||||
@@ -16,12 +16,7 @@
|
|||||||
#include "file_transfer.h"
|
#include "file_transfer.h"
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#if defined(_WIN32) || defined(__linux__)
|
#include "platform/video_renderer.h"
|
||||||
#include "platform/opengl_video_renderer.h"
|
|
||||||
#endif
|
|
||||||
#ifdef __APPLE__
|
|
||||||
#include "platform/metal_video_renderer.h"
|
|
||||||
#endif
|
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
#include "runtime/remote_action_codec.h"
|
#include "runtime/remote_action_codec.h"
|
||||||
@@ -319,61 +314,46 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
props->enable_mouse_control_ = false;
|
props->enable_mouse_control_ = false;
|
||||||
runtime->ResetRemoteServiceStatus(*props);
|
runtime->ResetRemoteServiceStatus(*props);
|
||||||
|
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
|
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
|
||||||
int native_snapshot_width = 0;
|
int native_snapshot_width = 0;
|
||||||
int native_snapshot_height = 0;
|
int native_snapshot_height = 0;
|
||||||
bool needs_native_snapshot = false;
|
bool needs_native_snapshot = false;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||||
needs_native_snapshot = !props->thumbnail_frame_ ||
|
needs_native_snapshot =
|
||||||
props->thumbnail_frame_->empty();
|
!props->thumbnail_frame_ || props->thumbnail_frame_->empty();
|
||||||
}
|
}
|
||||||
if (needs_native_snapshot) {
|
if (needs_native_snapshot) {
|
||||||
#if defined(__APPLE__)
|
auto* native_renderer = runtime->video_renderer_.get();
|
||||||
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
|
|
||||||
#else
|
|
||||||
auto* native_renderer = runtime->opengl_video_renderer_.get();
|
|
||||||
#endif
|
|
||||||
auto snapshot = std::make_shared<std::vector<unsigned char>>();
|
auto snapshot = std::make_shared<std::vector<unsigned char>>();
|
||||||
if (native_renderer && native_renderer->CopyLatestNv12(
|
if (native_renderer &&
|
||||||
remote_id, snapshot.get(),
|
native_renderer->CopyLatestNv12(remote_id, snapshot.get(),
|
||||||
&native_snapshot_width,
|
&native_snapshot_width,
|
||||||
&native_snapshot_height)) {
|
&native_snapshot_height)) {
|
||||||
native_snapshot = std::move(snapshot);
|
native_snapshot = std::move(snapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||||
props->front_frame_.reset();
|
props->front_frame_.reset();
|
||||||
props->back_frame_.reset();
|
props->back_frame_.reset();
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
if (native_snapshot &&
|
if (native_snapshot &&
|
||||||
(!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) {
|
(!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) {
|
||||||
props->thumbnail_frame_ = std::move(native_snapshot);
|
props->thumbnail_frame_ = std::move(native_snapshot);
|
||||||
props->thumbnail_width_ = native_snapshot_width;
|
props->thumbnail_width_ = native_snapshot_width;
|
||||||
props->thumbnail_height_ = native_snapshot_height;
|
props->thumbnail_height_ = native_snapshot_height;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
props->video_width_ = 0;
|
props->video_width_ = 0;
|
||||||
props->video_height_ = 0;
|
props->video_height_ = 0;
|
||||||
props->video_size_ = 0;
|
props->video_size_ = 0;
|
||||||
props->render_rect_dirty_ = true;
|
props->render_rect_dirty_ = true;
|
||||||
props->stream_cleanup_pending_ = true;
|
props->stream_cleanup_pending_ = true;
|
||||||
}
|
}
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
auto* native_renderer = runtime->video_renderer_.get();
|
||||||
#if defined(__APPLE__)
|
|
||||||
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
|
|
||||||
#else
|
|
||||||
auto* native_renderer = runtime->opengl_video_renderer_.get();
|
|
||||||
#endif
|
|
||||||
if (native_renderer) {
|
if (native_renderer) {
|
||||||
native_renderer->DiscardStream(remote_id);
|
native_renderer->DiscardStream(remote_id);
|
||||||
runtime->video_frame_dirty_.store(true,
|
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||||
std::memory_order_release);
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
runtime->focus_on_stream_window_ = false;
|
runtime->focus_on_stream_window_ = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,26 @@
|
|||||||
#include "runtime/peer_event_handler.h"
|
#include <chrono>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "platform/video_renderer.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
#include "runtime/peer_event_handler.h"
|
||||||
#include <chrono>
|
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
#include "platform/metal_video_renderer.h"
|
|
||||||
#else
|
|
||||||
#include "platform/opengl_video_renderer.h"
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
|
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
#endif
|
|
||||||
|
|
||||||
void PeerEventHandler::OnReceiveVideoBuffer(
|
void PeerEventHandler::OnReceiveVideoBuffer(
|
||||||
const XVideoFrame *video_frame, const char *user_id, size_t user_id_size,
|
const XVideoFrame* video_frame, const char* user_id, size_t user_id_size,
|
||||||
const char *src_id, size_t src_id_size, void *user_data) {
|
const char* src_id, size_t src_id_size, void* user_data) {
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||||
if (!runtime) {
|
if (!runtime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -41,32 +31,20 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
|||||||
runtime->remote_sessions_.end()) {
|
runtime->remote_sessions_.end()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GuiRuntime::RemoteSession *props =
|
GuiRuntime::RemoteSession* props =
|
||||||
runtime->remote_sessions_.find(remote_id)->second.get();
|
runtime->remote_sessions_.find(remote_id)->second.get();
|
||||||
|
|
||||||
if (props->connection_established_) {
|
if (props->connection_established_) {
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
bool background_snapshot_only = false;
|
bool background_snapshot_only = false;
|
||||||
#if defined(__APPLE__)
|
auto* native_renderer = runtime->video_renderer_.get();
|
||||||
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
|
if (native_renderer && native_renderer->IsActive()) {
|
||||||
using NativeVideoRenderer = MacMetalVideoRenderer;
|
|
||||||
#else
|
|
||||||
auto* native_renderer = runtime->opengl_video_renderer_.get();
|
|
||||||
using NativeVideoRenderer = OpenGlVideoRenderer;
|
|
||||||
#endif
|
|
||||||
if (native_renderer && native_renderer->IsReady()
|
|
||||||
#if defined(__APPLE__)
|
|
||||||
&& native_renderer->IsAttached()
|
|
||||||
#endif
|
|
||||||
) {
|
|
||||||
const auto submit_result = native_renderer->SubmitNv12(
|
const auto submit_result = native_renderer->SubmitNv12(
|
||||||
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||||
video_frame->size, video_frame->width, video_frame->height);
|
video_frame->size, video_frame->width, video_frame->height);
|
||||||
if (submit_result == NativeVideoRenderer::SubmitResult::submitted) {
|
if (submit_result == VideoRenderer::SubmitResult::submitted) {
|
||||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||||
const bool size_changed =
|
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||||
(props->video_width_ != video_frame->width) ||
|
(props->video_height_ != video_frame->height);
|
||||||
(props->video_height_ != video_frame->height);
|
|
||||||
if (size_changed) {
|
if (size_changed) {
|
||||||
props->render_rect_dirty_ = true;
|
props->render_rect_dirty_ = true;
|
||||||
}
|
}
|
||||||
@@ -86,22 +64,19 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
|||||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (submit_result == NativeVideoRenderer::SubmitResult::dropped ||
|
if (submit_result == VideoRenderer::SubmitResult::dropped ||
|
||||||
submit_result == NativeVideoRenderer::SubmitResult::failed) {
|
submit_result == VideoRenderer::SubmitResult::failed) {
|
||||||
// Keep presenting the last native frame. A later decoded frame can
|
// Keep presenting the last native frame. A later decoded frame can
|
||||||
// reuse the renderer without changing ownership mid-window.
|
// reuse the renderer without changing ownership mid-window.
|
||||||
props->streaming_ = true;
|
props->streaming_ = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (submit_result ==
|
if (submit_result == VideoRenderer::SubmitResult::not_selected) {
|
||||||
NativeVideoRenderer::SubmitResult::not_selected) {
|
|
||||||
background_snapshot_only = true;
|
background_snapshot_only = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
if (background_snapshot_only && props->thumbnail_frame_ &&
|
if (background_snapshot_only && props->thumbnail_frame_ &&
|
||||||
!props->thumbnail_frame_->empty() &&
|
!props->thumbnail_frame_->empty() &&
|
||||||
@@ -112,7 +87,6 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
|||||||
props->streaming_ = true;
|
props->streaming_ = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
// Allocate a third buffer only while the UI still owns the old snapshot.
|
// Allocate a third buffer only while the UI still owns the old snapshot.
|
||||||
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
|
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
|
||||||
@@ -137,32 +111,28 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
|||||||
props->video_size_ = video_frame->size;
|
props->video_size_ = video_frame->size;
|
||||||
|
|
||||||
props->front_frame_.swap(props->back_frame_);
|
props->front_frame_.swap(props->back_frame_);
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
props->thumbnail_frame_ = props->front_frame_;
|
props->thumbnail_frame_ = props->front_frame_;
|
||||||
props->thumbnail_width_ = video_frame->width;
|
props->thumbnail_width_ = video_frame->width;
|
||||||
props->thumbnail_height_ = video_frame->height;
|
props->thumbnail_height_ = video_frame->height;
|
||||||
if (background_snapshot_only) {
|
if (background_snapshot_only) {
|
||||||
props->background_snapshot_time_ = now;
|
props->background_snapshot_time_ = now;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
++props->video_frame_sequence_;
|
++props->video_frame_sequence_;
|
||||||
}
|
}
|
||||||
|
|
||||||
props->streaming_ = true;
|
props->streaming_ = true;
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
if (background_snapshot_only) {
|
if (background_snapshot_only) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PeerEventHandler::OnReceiveAudioBuffer(
|
void PeerEventHandler::OnReceiveAudioBuffer(
|
||||||
const char *data, size_t size, const char *user_id, size_t user_id_size,
|
const char* data, size_t size, const char* user_id, size_t user_id_size,
|
||||||
const char *src_id, size_t src_id_size, void *user_data) {
|
const char* src_id, size_t src_id_size, void* user_data) {
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||||
if (!runtime) {
|
if (!runtime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -172,4 +142,4 @@ void PeerEventHandler::OnReceiveAudioBuffer(
|
|||||||
runtime->devices_.PushAudio(data, size);
|
runtime->devices_.PushAudio(data, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -110,14 +110,12 @@ struct RemoteSession {
|
|||||||
std::mutex video_frame_mutex_;
|
std::mutex video_frame_mutex_;
|
||||||
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
||||||
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
||||||
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
|
|
||||||
// Retains the most recent background CPU frame across disconnect cleanup
|
// Retains the most recent background CPU frame across disconnect cleanup
|
||||||
// so closing an unselected tab can still update its recent-item thumbnail.
|
// so closing an unselected tab can still update its recent-item thumbnail.
|
||||||
std::shared_ptr<std::vector<unsigned char>> thumbnail_frame_;
|
std::shared_ptr<std::vector<unsigned char>> thumbnail_frame_;
|
||||||
int thumbnail_width_ = 0;
|
int thumbnail_width_ = 0;
|
||||||
int thumbnail_height_ = 0;
|
int thumbnail_height_ = 0;
|
||||||
std::chrono::steady_clock::time_point background_snapshot_time_;
|
std::chrono::steady_clock::time_point background_snapshot_time_;
|
||||||
#endif
|
|
||||||
bool render_rect_dirty_ = false;
|
bool render_rect_dirty_ = false;
|
||||||
bool stream_cleanup_pending_ = false;
|
bool stream_cleanup_pending_ = false;
|
||||||
float mouse_pos_x_ = 0;
|
float mouse_pos_x_ = 0;
|
||||||
|
|||||||
+7
-3
@@ -270,6 +270,7 @@ function setup_targets()
|
|||||||
"device_controller", "thumbnail", "version_checker", "tools")
|
"device_controller", "thumbnail", "version_checker", "tools")
|
||||||
add_files("src/gui/render.cpp", "src/gui/application/gui_application.cpp",
|
add_files("src/gui/render.cpp", "src/gui/application/gui_application.cpp",
|
||||||
"src/gui/application/portable_service_integration.cpp",
|
"src/gui/application/portable_service_integration.cpp",
|
||||||
|
"src/gui/rendering/*.cpp",
|
||||||
"src/gui/runtime/*.cpp",
|
"src/gui/runtime/*.cpp",
|
||||||
"src/gui/features/devices/*.cpp", "src/gui/features/input/*.cpp",
|
"src/gui/features/devices/*.cpp", "src/gui/features/input/*.cpp",
|
||||||
"src/gui/features/clipboard/*.cpp", "src/gui/features/file_transfer/*.cpp",
|
"src/gui/features/clipboard/*.cpp", "src/gui/features/file_transfer/*.cpp",
|
||||||
@@ -279,16 +280,19 @@ function setup_targets()
|
|||||||
add_cxxflags("/bigobj")
|
add_cxxflags("/bigobj")
|
||||||
add_links("opengl32")
|
add_links("opengl32")
|
||||||
add_files("src/gui/platform/tray/win_tray.cpp",
|
add_files("src/gui/platform/tray/win_tray.cpp",
|
||||||
"src/gui/platform/opengl_video_renderer.cpp")
|
"src/gui/platform/opengl_video_renderer.cpp",
|
||||||
|
"src/gui/platform/video_renderer_factory_opengl.cpp")
|
||||||
add_includedirs("src/service/windows", {public = true})
|
add_includedirs("src/service/windows", {public = true})
|
||||||
elseif is_os("macosx") then
|
elseif is_os("macosx") then
|
||||||
add_files("src/gui/runtime/*.mm", "src/gui/platform/tray/*.mm",
|
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")
|
"src/gui/platform/metal_video_renderer.mm",
|
||||||
|
"src/gui/platform/video_renderer_factory_metal.mm")
|
||||||
elseif is_os("linux") then
|
elseif is_os("linux") then
|
||||||
add_links("GL")
|
add_links("GL")
|
||||||
add_files("src/gui/platform/tray/linux_tray.cpp",
|
add_files("src/gui/platform/tray/linux_tray.cpp",
|
||||||
"src/gui/platform/opengl_video_renderer.cpp")
|
"src/gui/platform/opengl_video_renderer.cpp",
|
||||||
|
"src/gui/platform/video_renderer_factory_opengl.cpp")
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_os("windows") then
|
if is_os("windows") then
|
||||||
|
|||||||
Reference in New Issue
Block a user