mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-24 21:57:08 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16348444f0 | ||
|
|
e716346b26 | ||
|
|
b7e1adbec6 | ||
|
|
58f2daa9c1 |
@@ -12,6 +12,12 @@ bool IsValidTurnModeValue(long value) {
|
||||
value <= static_cast<long>(ConfigCenter::TURN_MODE::FORCE_TCP);
|
||||
}
|
||||
|
||||
bool IsValidVideoAdaptationPolicyValue(long value) {
|
||||
using Policy = ConfigCenter::VIDEO_ADAPTATION_POLICY;
|
||||
return value >= static_cast<long>(Policy::FRAME_RATE_PRIORITY) &&
|
||||
value <= static_cast<long>(Policy::BALANCED);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConfigCenter::ConfigCenter(const std::string& config_path)
|
||||
@@ -52,6 +58,18 @@ int ConfigCenter::Load() {
|
||||
video_frame_rate_ = static_cast<VIDEO_FRAME_RATE>(ini_.GetLongValue(
|
||||
section_, "video_frame_rate", static_cast<long>(video_frame_rate_)));
|
||||
|
||||
const long video_adaptation_policy_value = ini_.GetLongValue(
|
||||
section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
if (IsValidVideoAdaptationPolicyValue(video_adaptation_policy_value)) {
|
||||
video_adaptation_policy_ =
|
||||
static_cast<VIDEO_ADAPTATION_POLICY>(video_adaptation_policy_value);
|
||||
} else {
|
||||
LOG_WARN("Invalid video adaptation policy [{}], using quality priority",
|
||||
video_adaptation_policy_value);
|
||||
video_adaptation_policy_ = VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
|
||||
}
|
||||
|
||||
video_encode_format_ = static_cast<VIDEO_ENCODE_FORMAT>(
|
||||
ini_.GetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_)));
|
||||
@@ -137,6 +155,8 @@ int ConfigCenter::Save() {
|
||||
static_cast<long>(video_quality_));
|
||||
ini_.SetLongValue(section_, "video_frame_rate",
|
||||
static_cast<long>(video_frame_rate_));
|
||||
ini_.SetLongValue(section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
ini_.SetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_));
|
||||
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
|
||||
@@ -205,6 +225,18 @@ int ConfigCenter::SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoAdaptationPolicy(
|
||||
VIDEO_ADAPTATION_POLICY policy) {
|
||||
video_adaptation_policy_ = policy;
|
||||
ini_.SetLongValue(section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoEncodeFormat(
|
||||
VIDEO_ENCODE_FORMAT video_encode_format) {
|
||||
video_encode_format_ = video_encode_format;
|
||||
@@ -395,6 +427,11 @@ ConfigCenter::VIDEO_FRAME_RATE ConfigCenter::GetVideoFrameRate() const {
|
||||
return video_frame_rate_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_ADAPTATION_POLICY
|
||||
ConfigCenter::GetVideoAdaptationPolicy() const {
|
||||
return video_adaptation_policy_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
|
||||
return video_encode_format_;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ class ConfigCenter {
|
||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
|
||||
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
|
||||
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
|
||||
enum class VIDEO_ADAPTATION_POLICY {
|
||||
FRAME_RATE_PRIORITY = 0,
|
||||
QUALITY_PRIORITY = 1,
|
||||
BALANCED = 2
|
||||
};
|
||||
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
|
||||
enum class TURN_MODE {
|
||||
DISABLED = 0,
|
||||
@@ -34,6 +39,7 @@ class ConfigCenter {
|
||||
int SetLanguage(LANGUAGE language);
|
||||
int SetVideoQuality(VIDEO_QUALITY video_quality);
|
||||
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
|
||||
int SetVideoAdaptationPolicy(VIDEO_ADAPTATION_POLICY policy);
|
||||
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
|
||||
int SetHardwareVideoCodec(bool hardware_video_codec);
|
||||
int SetTurnMode(TURN_MODE turn_mode);
|
||||
@@ -53,6 +59,7 @@ class ConfigCenter {
|
||||
LANGUAGE GetLanguage() const;
|
||||
VIDEO_QUALITY GetVideoQuality() const;
|
||||
VIDEO_FRAME_RATE GetVideoFrameRate() const;
|
||||
VIDEO_ADAPTATION_POLICY GetVideoAdaptationPolicy() const;
|
||||
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
|
||||
bool IsHardwareVideoCodec() const;
|
||||
TURN_MODE GetTurnMode() const;
|
||||
@@ -81,6 +88,8 @@ class ConfigCenter {
|
||||
LANGUAGE language_ = LANGUAGE::CHINESE;
|
||||
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
|
||||
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
|
||||
VIDEO_ADAPTATION_POLICY video_adaptation_policy_ =
|
||||
VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
|
||||
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
|
||||
bool hardware_video_codec_ = false;
|
||||
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||
|
||||
@@ -1206,6 +1206,8 @@ void GuiApplication::ResetSettingsUi() {
|
||||
ui_->main->set_language_index(language_button_value_);
|
||||
ui_->main->set_video_quality_index(video_quality_button_value_);
|
||||
ui_->main->set_frame_rate_index(video_frame_rate_button_value_);
|
||||
ui_->main->set_adaptation_policy_index(
|
||||
video_adaptation_policy_button_value_);
|
||||
ui_->main->set_codec_index(video_encode_format_button_value_);
|
||||
ui_->main->set_hardware_codec_enabled(enable_hardware_video_codec_);
|
||||
ui_->main->set_turn_enabled(enable_turn_);
|
||||
@@ -2904,6 +2906,8 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
std::clamp(main->get_video_quality_index(), 0, 2);
|
||||
video_frame_rate_button_value_ =
|
||||
std::clamp(main->get_frame_rate_index(), 0, 1);
|
||||
video_adaptation_policy_button_value_ =
|
||||
std::clamp(main->get_adaptation_policy_index(), 0, 2);
|
||||
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
|
||||
enable_hardware_video_codec_ = main->get_hardware_codec_enabled();
|
||||
enable_turn_ = main->get_turn_enabled();
|
||||
@@ -2920,6 +2924,9 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
static_cast<ConfigCenter::VIDEO_QUALITY>(video_quality_button_value_));
|
||||
config_center_->SetVideoFrameRate(static_cast<ConfigCenter::VIDEO_FRAME_RATE>(
|
||||
video_frame_rate_button_value_));
|
||||
config_center_->SetVideoAdaptationPolicy(
|
||||
static_cast<ConfigCenter::VIDEO_ADAPTATION_POLICY>(
|
||||
video_adaptation_policy_button_value_));
|
||||
config_center_->SetVideoEncodeFormat(
|
||||
static_cast<ConfigCenter::VIDEO_ENCODE_FORMAT>(
|
||||
video_encode_format_button_value_));
|
||||
@@ -2951,6 +2958,8 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
language_button_value_last_ = language_button_value_;
|
||||
video_quality_button_value_last_ = video_quality_button_value_;
|
||||
video_frame_rate_button_value_last_ = video_frame_rate_button_value_;
|
||||
video_adaptation_policy_button_value_last_ =
|
||||
video_adaptation_policy_button_value_;
|
||||
video_encode_format_button_value_last_ = video_encode_format_button_value_;
|
||||
enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
|
||||
enable_turn_last_ = enable_turn_;
|
||||
|
||||
@@ -120,9 +120,15 @@ struct TranslationRow {
|
||||
u8"стола") \
|
||||
X(settings, u8"设置", "Settings", u8"Настройки") \
|
||||
X(language, u8"语言:", "Language:", u8"Язык:") \
|
||||
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
|
||||
X(video_quality, u8"画面质量:", "Video Quality:", u8"Качество видео:") \
|
||||
X(video_frame_rate, u8"画面采集帧率:", \
|
||||
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
|
||||
X(video_adaptation_policy, u8"画面偏好:", "Video Preference:", \
|
||||
u8"Режим видео:") \
|
||||
X(video_priority_frame_rate, u8"帧率优先", "Frame Rate", \
|
||||
u8"Частота кадров") \
|
||||
X(video_priority_quality, u8"画质优先", "Quality", u8"Качество") \
|
||||
X(video_priority_balanced, u8"平衡", "Balanced", u8"Баланс") \
|
||||
X(video_quality_high, u8"高", "High", u8"Высокое") \
|
||||
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
|
||||
X(video_quality_low, u8"低", "Low", u8"Низкое") \
|
||||
|
||||
@@ -301,6 +301,8 @@ int SettingsManager::Load() {
|
||||
static_cast<int>(owner_.config_center_->GetVideoQuality());
|
||||
owner_.video_frame_rate_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoFrameRate());
|
||||
owner_.video_adaptation_policy_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoAdaptationPolicy());
|
||||
owner_.video_encode_format_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
|
||||
owner_.enable_hardware_video_codec_ =
|
||||
@@ -324,6 +326,10 @@ int SettingsManager::Load() {
|
||||
|
||||
owner_.language_button_value_last_ = owner_.language_button_value_;
|
||||
owner_.video_quality_button_value_last_ = owner_.video_quality_button_value_;
|
||||
owner_.video_frame_rate_button_value_last_ =
|
||||
owner_.video_frame_rate_button_value_;
|
||||
owner_.video_adaptation_policy_button_value_last_ =
|
||||
owner_.video_adaptation_policy_button_value_;
|
||||
owner_.video_encode_format_button_value_last_ =
|
||||
owner_.video_encode_format_button_value_;
|
||||
owner_.enable_hardware_video_codec_last_ =
|
||||
|
||||
@@ -119,9 +119,21 @@ struct SlintVideoPresenter::Impl {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(gl_mutex);
|
||||
auto* opengl_renderer =
|
||||
dynamic_cast<OpenGlVideoRenderer*>(&renderer);
|
||||
if (state == slint::RenderingState::AfterRendering && stream &&
|
||||
opengl_renderer &&
|
||||
(*stream)->get_native_video_enabled() &&
|
||||
opengl_renderer->ShouldContinueRendering()) {
|
||||
// Queue the next frame directly from the display callback. A
|
||||
// 16/17 ms polling timer can drift across vsync and make Windows
|
||||
// coalesce every second request, effectively capping video near
|
||||
// 30 fps even when decode and submission stay near 60 fps.
|
||||
(*stream)->window().request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(gl_mutex);
|
||||
if (state == slint::RenderingState::RenderingSetup) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
|
||||
@@ -141,9 +141,13 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
sizeof(params_.log_path) - 1);
|
||||
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
|
||||
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
|
||||
// The Slint desktop renderer currently consumes packed CPU frames. Native
|
||||
// renderers can opt into platform-native frame output independently.
|
||||
#if defined(_WIN32)
|
||||
// Windows accepts ref-counted CPU NV12 and CUDA device NV12. The OpenGL
|
||||
// renderer retains the decoded frame until the UI thread has uploaded it.
|
||||
params_.native_video_output = true;
|
||||
#else
|
||||
params_.native_video_output = false;
|
||||
#endif
|
||||
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
|
||||
? true
|
||||
@@ -158,8 +162,21 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
: 60;
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainFrameRate;
|
||||
switch (config_center_->GetVideoAdaptationPolicy()) {
|
||||
case ConfigCenter::VIDEO_ADAPTATION_POLICY::FRAME_RATE_PRIORITY:
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainFrameRate;
|
||||
break;
|
||||
case ConfigCenter::VIDEO_ADAPTATION_POLICY::BALANCED:
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::Balanced;
|
||||
break;
|
||||
case ConfigCenter::VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY:
|
||||
default:
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainResolution;
|
||||
break;
|
||||
}
|
||||
params_.on_receive_video_buffer = nullptr;
|
||||
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
|
||||
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
|
||||
|
||||
@@ -14,6 +14,23 @@ namespace {
|
||||
|
||||
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
|
||||
|
||||
#if defined(_WIN32)
|
||||
const XWindowsVideoFrame* GetWindowsNativeFrame(const XVideoFrame* frame) {
|
||||
if (!frame ||
|
||||
frame->native_handle_type != XVideoFrameNativeHandleWindowsNv12 ||
|
||||
!frame->native_handle) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto* native =
|
||||
static_cast<const XWindowsVideoFrame*>(frame->native_handle);
|
||||
return native->struct_size >=
|
||||
static_cast<uint32_t>(sizeof(XWindowsVideoFrame)) &&
|
||||
native->owner && native->copy_to_cpu
|
||||
? native
|
||||
: nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
@@ -38,9 +55,19 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
bool background_snapshot_only = false;
|
||||
auto* native_renderer = runtime->video_renderer_.get();
|
||||
if (native_renderer && native_renderer->IsActive()) {
|
||||
const auto submit_result = native_renderer->SubmitNv12(
|
||||
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||
video_frame->size, video_frame->width, video_frame->height);
|
||||
VideoRenderer::SubmitResult submit_result =
|
||||
VideoRenderer::SubmitResult::failed;
|
||||
#if defined(_WIN32)
|
||||
if (GetWindowsNativeFrame(video_frame)) {
|
||||
submit_result =
|
||||
native_renderer->SubmitNativeFrame(remote_id, *video_frame);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
submit_result = native_renderer->SubmitNv12(
|
||||
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||
video_frame->size, video_frame->width, video_frame->height);
|
||||
}
|
||||
if (submit_result == VideoRenderer::SubmitResult::submitted) {
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
@@ -88,17 +115,43 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
return;
|
||||
}
|
||||
|
||||
size_t frame_size = video_frame->size;
|
||||
#if defined(_WIN32)
|
||||
const XWindowsVideoFrame* windows_native_frame =
|
||||
GetWindowsNativeFrame(video_frame);
|
||||
if (windows_native_frame) {
|
||||
frame_size = static_cast<size_t>(windows_native_frame->width) *
|
||||
windows_native_frame->height * 3U / 2U;
|
||||
}
|
||||
#endif
|
||||
if (frame_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate a third buffer only while the UI still owns the old snapshot.
|
||||
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
|
||||
props->back_frame_ =
|
||||
std::make_shared<std::vector<unsigned char>>(video_frame->size);
|
||||
std::make_shared<std::vector<unsigned char>>(frame_size);
|
||||
}
|
||||
if (props->back_frame_->size() != video_frame->size) {
|
||||
props->back_frame_->resize(video_frame->size);
|
||||
if (props->back_frame_->size() != frame_size) {
|
||||
props->back_frame_->resize(frame_size);
|
||||
}
|
||||
|
||||
std::memcpy(props->back_frame_->data(), video_frame->data,
|
||||
video_frame->size);
|
||||
#if defined(_WIN32)
|
||||
if (windows_native_frame) {
|
||||
if (windows_native_frame->copy_to_cpu(
|
||||
windows_native_frame->owner, props->back_frame_->data(),
|
||||
props->back_frame_->size()) != 0) {
|
||||
return;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (!video_frame->data || video_frame->size < frame_size) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(props->back_frame_->data(), video_frame->data, frame_size);
|
||||
}
|
||||
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
(props->video_height_ != video_frame->height);
|
||||
@@ -108,7 +161,7 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
props->video_size_ = frame_size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
props->thumbnail_frame_ = props->front_frame_;
|
||||
|
||||
@@ -122,6 +122,7 @@ struct UserSettingsState {
|
||||
int language_button_value_ = 0;
|
||||
int video_quality_button_value_ = 2;
|
||||
int video_frame_rate_button_value_ = 1;
|
||||
int video_adaptation_policy_button_value_ = 1;
|
||||
int video_encode_format_button_value_ = 0;
|
||||
bool enable_hardware_video_codec_ = true;
|
||||
bool enable_turn_ = true;
|
||||
@@ -133,6 +134,7 @@ struct UserSettingsState {
|
||||
int language_button_value_last_ = 0;
|
||||
int video_quality_button_value_last_ = 0;
|
||||
int video_frame_rate_button_value_last_ = 0;
|
||||
int video_adaptation_policy_button_value_last_ = 1;
|
||||
int video_encode_format_button_value_last_ = 0;
|
||||
bool enable_hardware_video_codec_last_ = false;
|
||||
bool enable_turn_last_ = true;
|
||||
|
||||
@@ -39,6 +39,10 @@ export global UiStrings {
|
||||
in-out property <string> language: "Language";
|
||||
in-out property <string> video-quality: "Video quality";
|
||||
in-out property <string> frame-rate: "Frame rate";
|
||||
in-out property <string> adaptation-policy: "Video preference";
|
||||
in-out property <string> priority-frame-rate: "Frame rate";
|
||||
in-out property <string> priority-quality: "Quality";
|
||||
in-out property <string> priority-balanced: "Balanced";
|
||||
in-out property <string> codec: "Video codec";
|
||||
in-out property <string> hardware-codec: "Hardware video codec";
|
||||
in-out property <string> turn-relay: "TURN relay";
|
||||
@@ -234,6 +238,7 @@ export component MainWindow inherits Window {
|
||||
in-out property <int> language-index: 0;
|
||||
in-out property <int> video-quality-index: 2;
|
||||
in-out property <int> frame-rate-index: 1;
|
||||
in-out property <int> adaptation-policy-index: 1;
|
||||
in-out property <int> codec-index: 0;
|
||||
in-out property <bool> hardware-codec-enabled: true;
|
||||
in-out property <bool> turn-enabled: true;
|
||||
@@ -1451,6 +1456,11 @@ export component MainWindow inherits Window {
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.adaptation-policy; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
CompactComboBox { x: root.compact-language ? 91px : 160px; y: 2px; width: root.compact-language ? 88px : 87px; enabled: !root.settings-session-active; model: [UiStrings.priority-frame-rate, UiStrings.priority-quality, UiStrings.priority-balanced]; current-index <=> root.adaptation-policy-index; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
|
||||
@@ -48,6 +48,14 @@ inline int ApplyMainWindowStrings(
|
||||
strings.set_language(Text(localization::language[language]));
|
||||
strings.set_video_quality(Text(localization::video_quality[language]));
|
||||
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
|
||||
strings.set_adaptation_policy(
|
||||
Text(localization::video_adaptation_policy[language]));
|
||||
strings.set_priority_frame_rate(
|
||||
Text(localization::video_priority_frame_rate[language]));
|
||||
strings.set_priority_quality(
|
||||
Text(localization::video_priority_quality[language]));
|
||||
strings.set_priority_balanced(
|
||||
Text(localization::video_priority_balanced[language]));
|
||||
strings.set_codec(Text(localization::video_encode_format[language]));
|
||||
strings.set_hardware_codec(
|
||||
Text(localization::enable_hardware_video_codec[language]));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,10 +13,11 @@
|
||||
namespace crossdesk {
|
||||
|
||||
// NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux.
|
||||
// MiniRTC submits tightly packed CPU NV12 frames from its decode callback
|
||||
// thread. The Slint UI thread uploads the newest frame as Y and UV textures and
|
||||
// performs color conversion and scaling in a fragment shader before Slint
|
||||
// draws its overlay.
|
||||
// MiniRTC submits either ref-counted Windows native NV12 frames or packed CPU
|
||||
// NV12 fallback frames from its decode callback thread. CUDA-backed frames use
|
||||
// CUDA/OpenGL interop; CPU-backed frames are retained without an intermediate
|
||||
// queue copy. The Slint UI thread updates Y and UV textures and performs color
|
||||
// conversion and scaling before Slint draws its overlay.
|
||||
class OpenGlVideoRenderer final : public VideoRenderer {
|
||||
public:
|
||||
OpenGlVideoRenderer();
|
||||
@@ -32,6 +33,11 @@ public:
|
||||
bool IsReady() const override;
|
||||
bool IsActive() const override;
|
||||
|
||||
// Keeps Slint's native surface on a display-driven redraw loop while decoded
|
||||
// frames are arriving. This avoids timer/vsync phase drift that otherwise
|
||||
// causes every second redraw request to be coalesced on Windows.
|
||||
bool ShouldContinueRendering() const;
|
||||
|
||||
// Only the selected stream is queued for upload. Frames for other tabs are
|
||||
// retained by GuiRuntime at a throttled rate for thumbnail/tab restoration.
|
||||
bool SetSelectedStream(std::string remote_id) override;
|
||||
@@ -42,6 +48,8 @@ public:
|
||||
size_t size, int width, int height) override;
|
||||
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
|
||||
size_t size, int width, int height) override;
|
||||
SubmitResult SubmitNativeFrame(std::string_view remote_id,
|
||||
const XVideoFrame &frame) override;
|
||||
|
||||
// Draws below Slint while its OpenGL context is current. All dimensions are
|
||||
// physical pixels in the window client area. corner_radius_pixels clips the
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Platform-independent contract for the native stream video renderer.
|
||||
@@ -56,6 +58,10 @@ public:
|
||||
virtual SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
||||
const uint8_t* data, size_t size,
|
||||
int width, int height) = 0;
|
||||
virtual SubmitResult SubmitNativeFrame(std::string_view,
|
||||
const XVideoFrame&) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
|
||||
virtual bool CopyLatestNv12(std::string_view remote_id,
|
||||
std::vector<unsigned char>* output, int* width,
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
#include "video_receive_scheduler.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::unique_ptr<minirtc::ReceivedFrame>
|
||||
MakeFrame(uint32_t rtp_timestamp, int64_t received_us, bool key_frame = false) {
|
||||
const uint8_t payload = key_frame ? 0x65 : 0x41;
|
||||
auto frame =
|
||||
std::make_unique<minirtc::ReceivedFrame>(&payload, sizeof(payload));
|
||||
frame->SetRtpTimestamp(rtp_timestamp);
|
||||
frame->SetReceivedTimestamp(received_us);
|
||||
frame->SetCapturedTimestamp(received_us - 5000);
|
||||
frame->SetFrameType(key_frame ? minirtc::kVideoFrameKey
|
||||
: minirtc::kVideoFrameDelta);
|
||||
return frame;
|
||||
}
|
||||
|
||||
bool Expect(bool condition, const char *message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
|
||||
bool TestOneFramePerPlaybackCycle() {
|
||||
minirtc::VideoReceiveScheduler scheduler(60);
|
||||
scheduler.Enqueue(MakeFrame(90000, 1000000), 1000000);
|
||||
scheduler.Enqueue(MakeFrame(91500, 1016667), 1016667);
|
||||
|
||||
auto first = scheduler.Poll(1016667, false);
|
||||
bool ok = Expect(first.frame != nullptr, "first frame was not released");
|
||||
ok &= Expect(first.frame && first.frame->RtpTimestamp() == 90000,
|
||||
"scheduler did not preserve RTP order");
|
||||
|
||||
auto same_cycle = scheduler.Poll(1016667, false);
|
||||
ok &= Expect(!same_cycle.frame,
|
||||
"scheduler released more than one frame in a cycle");
|
||||
ok &= Expect(same_cycle.next_delay_us > 0,
|
||||
"scheduler did not schedule the next playback cycle");
|
||||
|
||||
auto second = scheduler.Poll(1033334, false);
|
||||
ok &= Expect(second.frame != nullptr,
|
||||
"second frame was not released on the next cycle");
|
||||
ok &= Expect(second.frame && second.frame->RtpTimestamp() == 91500,
|
||||
"second RTP timestamp was incorrect");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestSevereLagJumpsToNewestKeyFrame() {
|
||||
minirtc::VideoReceiveScheduler scheduler(60);
|
||||
for (uint32_t i = 0; i < 10; ++i) {
|
||||
scheduler.Enqueue(MakeFrame(90000 + i * 1500, 1000000 + i * 16667, i == 8),
|
||||
1000000 + i * 16667);
|
||||
}
|
||||
|
||||
auto result = scheduler.Poll(1200000, false);
|
||||
const auto metrics = scheduler.GetMetrics(1200000);
|
||||
bool ok = Expect(result.frame && result.frame->IsKeyFrame(),
|
||||
"catch-up did not jump to the newest key frame");
|
||||
ok &= Expect(result.dropped_frames == 8,
|
||||
"catch-up dropped an unexpected number of frames");
|
||||
ok &= Expect(metrics.catch_up_count == 1,
|
||||
"catch-up metric was not incremented");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestSevereLagRequestsKeyFrameWhenNoSyncPointExists() {
|
||||
minirtc::VideoReceiveScheduler scheduler(60);
|
||||
for (uint32_t i = 0; i < 10; ++i) {
|
||||
scheduler.Enqueue(MakeFrame(180000 + i * 1500, 2000000 + i * 16667),
|
||||
2000000 + i * 16667);
|
||||
}
|
||||
|
||||
auto result = scheduler.Poll(2200000, false);
|
||||
const auto metrics = scheduler.GetMetrics(2200000);
|
||||
bool ok = Expect(!result.frame,
|
||||
"undecodable delta frame was released during catch-up");
|
||||
ok &=
|
||||
Expect(result.request_key_frame, "catch-up did not request a key frame");
|
||||
ok &= Expect(metrics.buffered_frames == 0 && metrics.awaiting_key_frame,
|
||||
"scheduler did not enter key-frame wait state");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestAdaptiveBufferStaysWithinOneToThreeFrames() {
|
||||
minirtc::VideoReceiveScheduler scheduler(60);
|
||||
int64_t arrival_us = 3000000;
|
||||
for (uint32_t i = 0; i < 40; ++i) {
|
||||
arrival_us += (i % 2 == 0) ? 5000 : 30000;
|
||||
scheduler.Enqueue(MakeFrame(270000 + i * 1500, arrival_us), arrival_us);
|
||||
}
|
||||
|
||||
const auto metrics = scheduler.GetMetrics(arrival_us);
|
||||
bool ok = Expect(metrics.target_buffer_frames >= 1 &&
|
||||
metrics.target_buffer_frames <= 3,
|
||||
"adaptive target left the 1-3 frame range");
|
||||
ok &= Expect(metrics.target_buffer_frames > 1,
|
||||
"arrival jitter did not increase the target buffer");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestLegacyMicrosecondTimestampFallsBackToArrivalCadence() {
|
||||
minirtc::VideoReceiveScheduler scheduler(60);
|
||||
int64_t arrival_us = 4000000;
|
||||
for (uint32_t i = 0; i < 20; ++i) {
|
||||
scheduler.Enqueue(MakeFrame(500000 + i * 16667, arrival_us), arrival_us);
|
||||
scheduler.Poll(arrival_us, false);
|
||||
arrival_us += 16667;
|
||||
}
|
||||
scheduler.Poll(arrival_us, false);
|
||||
|
||||
const auto metrics = scheduler.GetMetrics(arrival_us);
|
||||
bool ok = Expect(metrics.estimated_frame_interval_us >= 15000 &&
|
||||
metrics.estimated_frame_interval_us <= 20000,
|
||||
"legacy RTP units slowed the playback interval");
|
||||
ok &= Expect(metrics.timestamp_fallbacks > 0,
|
||||
"legacy RTP units did not activate timestamp fallback");
|
||||
ok &= Expect(metrics.released_frames == 20 && metrics.dropped_frames == 0,
|
||||
"legacy RTP units caused scheduler frame loss");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestSixtyFpsCadenceDoesNotTriggerCatchUp() {
|
||||
minirtc::VideoReceiveScheduler scheduler(60);
|
||||
constexpr int64_t kStartUs = 6000000;
|
||||
constexpr int64_t kFrameIntervalUs = 16667;
|
||||
constexpr uint32_t kStartRtpTimestamp = 700000;
|
||||
constexpr uint32_t kRtpFrameInterval = 1500;
|
||||
|
||||
for (uint32_t i = 0; i < 60; ++i) {
|
||||
const int64_t now_us = kStartUs + static_cast<int64_t>(i) *
|
||||
kFrameIntervalUs;
|
||||
scheduler.Enqueue(
|
||||
MakeFrame(kStartRtpTimestamp + i * kRtpFrameInterval, now_us),
|
||||
now_us);
|
||||
scheduler.Poll(now_us, false);
|
||||
}
|
||||
scheduler.Poll(kStartUs + 60 * kFrameIntervalUs, false);
|
||||
|
||||
const auto metrics =
|
||||
scheduler.GetMetrics(kStartUs + 60 * kFrameIntervalUs);
|
||||
bool ok = Expect(metrics.released_frames == 60,
|
||||
"60 fps input was not released at 60 fps cadence");
|
||||
ok &= Expect(metrics.dropped_frames == 0 && metrics.catch_up_count == 0,
|
||||
"steady 60 fps input incorrectly triggered catch-up");
|
||||
ok &= Expect(metrics.estimated_frame_interval_us >= 15000 &&
|
||||
metrics.estimated_frame_interval_us <= 18000,
|
||||
"steady 60 fps interval estimate drifted");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestKeyFrameRequestCooldownKeepsDecodableProgress() {
|
||||
minirtc::VideoReceiveScheduler scheduler(60);
|
||||
for (uint32_t i = 0; i < 10; ++i) {
|
||||
scheduler.Enqueue(MakeFrame(600000 + i * 1500,
|
||||
5000000 + static_cast<int64_t>(i) * 16667),
|
||||
5000000 + static_cast<int64_t>(i) * 16667);
|
||||
}
|
||||
auto first_catch_up = scheduler.Poll(5200000, false);
|
||||
bool ok = Expect(first_catch_up.request_key_frame,
|
||||
"initial severe lag did not request a key frame");
|
||||
|
||||
scheduler.Enqueue(MakeFrame(615000, 5220000, true), 5220000);
|
||||
auto key_frame = scheduler.Poll(5240000, false);
|
||||
ok &= Expect(key_frame.frame && key_frame.frame->IsKeyFrame(),
|
||||
"requested key frame was not released");
|
||||
|
||||
for (uint32_t i = 0; i < 10; ++i) {
|
||||
scheduler.Enqueue(MakeFrame(616500 + i * 1500,
|
||||
5250000 + static_cast<int64_t>(i) * 16667),
|
||||
5250000 + static_cast<int64_t>(i) * 16667);
|
||||
}
|
||||
auto cooldown_catch_up = scheduler.Poll(5450000, false);
|
||||
const auto metrics = scheduler.GetMetrics(5450000);
|
||||
ok &= Expect(!cooldown_catch_up.request_key_frame,
|
||||
"key frame request cooldown was ignored");
|
||||
ok &= Expect(cooldown_catch_up.frame != nullptr,
|
||||
"cooldown prevented forward playback progress");
|
||||
ok &= Expect(metrics.key_frame_requests == 1,
|
||||
"key frame request metric ignored cooldown");
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
bool ok = true;
|
||||
ok &= TestOneFramePerPlaybackCycle();
|
||||
ok &= TestSevereLagJumpsToNewestKeyFrame();
|
||||
ok &= TestSevereLagRequestsKeyFrameWhenNoSyncPointExists();
|
||||
ok &= TestAdaptiveBufferStaysWithinOneToThreeFrames();
|
||||
ok &= TestLegacyMicrosecondTimestampFallsBackToArrivalCadence();
|
||||
ok &= TestSixtyFpsCadenceDoesNotTriggerCatchUp();
|
||||
ok &= TestKeyFrameRequestCooldownKeepsDecodableProgress();
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "rtp_defines.h"
|
||||
#include "rtp_packetizer_av1.h"
|
||||
#include "rtp_packetizer_h264.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool Expect(bool condition, const char *message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
|
||||
bool AllPacketsUseTimestamp(
|
||||
const std::vector<std::unique_ptr<minirtc::RtpPacket>> &packets,
|
||||
uint32_t expected_timestamp) {
|
||||
if (packets.empty()) {
|
||||
return false;
|
||||
}
|
||||
for (const auto &packet : packets) {
|
||||
if (!packet || packet->Timestamp() != expected_timestamp) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TestTimestampConversion() {
|
||||
bool ok = Expect(minirtc::rtp::VideoTimestampFromMicroseconds(1000000) ==
|
||||
90000,
|
||||
"one second did not convert to 90 kHz");
|
||||
ok &= Expect(minirtc::rtp::VideoTimestampFromMicroseconds(16667) == 1500,
|
||||
"60 fps interval did not convert to 1500 RTP ticks");
|
||||
ok &= Expect(minirtc::rtp::VideoTimestampToMicroseconds(90000) == 1000000,
|
||||
"90 kHz timestamp did not convert to one second");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestH264PacketizerPreservesTimestamp() {
|
||||
constexpr uint32_t kTimestamp = 0x89abcdef;
|
||||
minirtc::RtpPacketizerH264 packetizer(1234);
|
||||
std::vector<uint8_t> small_nalu = {0x65, 0x01, 0x02};
|
||||
auto single_packet =
|
||||
packetizer.Build(small_nalu.data(), small_nalu.size(), kTimestamp, true);
|
||||
bool ok = Expect(AllPacketsUseTimestamp(single_packet, kTimestamp),
|
||||
"H.264 single-NAL packetizer changed RTP timestamp");
|
||||
|
||||
std::vector<uint8_t> large_nalu(MAX_NALU_LEN + 100, 0x55);
|
||||
large_nalu.front() = 0x65;
|
||||
auto fragmented =
|
||||
packetizer.Build(large_nalu.data(), large_nalu.size(), kTimestamp, true);
|
||||
ok &= Expect(AllPacketsUseTimestamp(fragmented, kTimestamp),
|
||||
"H.264 FU-A packetizer changed RTP timestamp");
|
||||
|
||||
auto padding = packetizer.BuildPadding(2000, kTimestamp, true);
|
||||
ok &= Expect(AllPacketsUseTimestamp(padding, kTimestamp),
|
||||
"H.264 padding packetizer changed RTP timestamp");
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestAv1PacketizerPreservesTimestamp() {
|
||||
constexpr uint32_t kTimestamp = 0x12345678;
|
||||
minirtc::RtpPacketizerAv1 packetizer(5678);
|
||||
// One frame OBU with a one-byte payload.
|
||||
std::vector<uint8_t> obu = {0x32, 0x01, 0x00};
|
||||
auto packets = packetizer.Build(obu.data(), obu.size(), kTimestamp, true);
|
||||
return Expect(AllPacketsUseTimestamp(packets, kTimestamp),
|
||||
"AV1 packetizer changed RTP timestamp");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
bool ok = true;
|
||||
ok &= TestTimestampConversion();
|
||||
ok &= TestH264PacketizerPreservesTimestamp();
|
||||
ok &= TestAv1PacketizerPreservesTimestamp();
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -106,6 +106,18 @@ function setup_targets()
|
||||
set_default(false)
|
||||
add_files("apps/desktop/tests/video_callback_lifetime_test.cpp")
|
||||
|
||||
target("video_receive_scheduler_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_deps("minirtc")
|
||||
add_files("apps/desktop/tests/video_receive_scheduler_test.cpp")
|
||||
|
||||
target("video_rtp_timestamp_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_deps("minirtc")
|
||||
add_files("apps/desktop/tests/video_rtp_timestamp_test.cpp")
|
||||
|
||||
target("windows_manifest_resource_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
|
||||
@@ -35,6 +35,12 @@ typedef NS_ENUM(NSInteger, CrossDeskPointerAction) {
|
||||
CrossDeskPointerActionMiddleUp,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
|
||||
CrossDeskVideoAdaptationPolicyFrameRatePriority = 0,
|
||||
CrossDeskVideoAdaptationPolicyQualityPriority,
|
||||
CrossDeskVideoAdaptationPolicyBalanced,
|
||||
};
|
||||
|
||||
@class CrossDeskRTCBridge;
|
||||
|
||||
@protocol CrossDeskRTCBridgeDelegate <NSObject>
|
||||
@@ -96,6 +102,7 @@ typedef NS_ENUM(NSInteger, CrossDeskPointerAction) {
|
||||
turnPort:(NSInteger)turnPort
|
||||
enableSRTP:(BOOL)enableSRTP;
|
||||
- (void)setHardwareAccelerationEnabled:(BOOL)enabled;
|
||||
- (void)setVideoAdaptationPolicy:(CrossDeskVideoAdaptationPolicy)policy;
|
||||
- (void)requestPresenceForRemoteIDs:(NSArray<NSString *> *)remoteIDs
|
||||
NS_SWIFT_NAME(requestPresence(remoteIDs:));
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ struct RTCState {
|
||||
int turn_port = 0;
|
||||
bool enable_srtp = false;
|
||||
bool hardware_acceleration = true;
|
||||
VideoDegradationPreference video_adaptation_policy =
|
||||
VideoDegradationPreference::MaintainResolution;
|
||||
bool identity_ready = false;
|
||||
bool identity_recovery_attempted = false;
|
||||
std::string identity_with_password;
|
||||
@@ -334,8 +336,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
params.video_content_type = VideoContentType::ScreenContent;
|
||||
params.video_quality = VideoQuality::QualityHigh;
|
||||
params.video_frame_rate = 60;
|
||||
params.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainFrameRate;
|
||||
params.video_degradation_preference = state.video_adaptation_policy;
|
||||
params.on_receive_video_buffer = nullptr;
|
||||
params.on_receive_audio_buffer = OnAudioBuffer;
|
||||
params.on_receive_data_buffer = OnDataBuffer;
|
||||
@@ -496,6 +497,26 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
});
|
||||
}
|
||||
|
||||
- (void)setVideoAdaptationPolicy:(CrossDeskVideoAdaptationPolicy)policy {
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
switch (policy) {
|
||||
case CrossDeskVideoAdaptationPolicyFrameRatePriority:
|
||||
self->_state->video_adaptation_policy =
|
||||
VideoDegradationPreference::MaintainFrameRate;
|
||||
break;
|
||||
case CrossDeskVideoAdaptationPolicyBalanced:
|
||||
self->_state->video_adaptation_policy =
|
||||
VideoDegradationPreference::Balanced;
|
||||
break;
|
||||
case CrossDeskVideoAdaptationPolicyQualityPriority:
|
||||
default:
|
||||
self->_state->video_adaptation_policy =
|
||||
VideoDegradationPreference::MaintainResolution;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)requestPresenceForRemoteIDs:(NSArray<NSString *> *)remoteIDs {
|
||||
NSArray<NSString *> *owned_ids = [remoteIDs copy];
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
|
||||
@@ -689,6 +689,22 @@ private struct ServerSettingsView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
Section("画面偏好") {
|
||||
Picker("偏好模式", selection: $session.videoAdaptationPolicy) {
|
||||
ForEach(VideoAdaptationPolicy.allCases) { policy in
|
||||
Text(policy.title).tag(policy)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
Text(session.videoAdaptationPolicy.detail)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("修改后从下一次连接开始生效。")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
Section("服务器") {
|
||||
TextField("信令服务器", text: $session.signalHost)
|
||||
.textInputAutocapitalization(.never)
|
||||
|
||||
@@ -79,6 +79,55 @@ enum VideoCodecMode: String, CaseIterable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
enum VideoAdaptationPolicy: String, CaseIterable, Identifiable {
|
||||
case frameRatePriority
|
||||
case qualityPriority
|
||||
case balanced
|
||||
|
||||
private static let defaultsKey = "crossdesk.mobile.video-adaptation-policy"
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .frameRatePriority: return "帧率优先"
|
||||
case .qualityPriority: return "画质优先"
|
||||
case .balanced: return "平衡"
|
||||
}
|
||||
}
|
||||
|
||||
var detail: String {
|
||||
switch self {
|
||||
case .frameRatePriority:
|
||||
return "优先保持画面流畅,带宽或编码压力较大时会更积极地降低分辨率。"
|
||||
case .qualityPriority:
|
||||
return "始终保持较高分辨率,带宽不足时通过降低帧率来保证画面清晰度。"
|
||||
case .balanced:
|
||||
return "兼顾清晰度和流畅度,只在持续压力下逐步降低分辨率。"
|
||||
}
|
||||
}
|
||||
|
||||
var bridgeValue: CrossDeskVideoAdaptationPolicy {
|
||||
switch self {
|
||||
case .frameRatePriority: return .frameRatePriority
|
||||
case .qualityPriority: return .qualityPriority
|
||||
case .balanced: return .balanced
|
||||
}
|
||||
}
|
||||
|
||||
static var saved: VideoAdaptationPolicy {
|
||||
guard let rawValue = UserDefaults.standard.string(forKey: defaultsKey),
|
||||
let policy = VideoAdaptationPolicy(rawValue: rawValue) else {
|
||||
return .qualityPriority
|
||||
}
|
||||
return policy
|
||||
}
|
||||
|
||||
func save() {
|
||||
UserDefaults.standard.set(rawValue, forKey: Self.defaultsKey)
|
||||
}
|
||||
}
|
||||
|
||||
private struct RemoteVideoFrame {
|
||||
let pixelBuffer: CVPixelBuffer
|
||||
let encodedSize: CGSize
|
||||
@@ -317,6 +366,9 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
@Published var videoCodecMode = VideoCodecMode.saved {
|
||||
didSet { videoCodecMode.save() }
|
||||
}
|
||||
@Published var videoAdaptationPolicy = VideoAdaptationPolicy.saved {
|
||||
didSet { videoAdaptationPolicy.save() }
|
||||
}
|
||||
@Published var remoteID = ""
|
||||
@Published var password = ""
|
||||
@Published private(set) var signalStatus = "正在连接信令服务"
|
||||
@@ -381,6 +433,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
return
|
||||
}
|
||||
bridge.setHardwareAccelerationEnabled(videoCodecMode == .hardware)
|
||||
bridge.setVideoAdaptationPolicy(videoAdaptationPolicy.bridgeValue)
|
||||
bridge.configure(withSignalHost: signalHost,
|
||||
signalPort: signal,
|
||||
turnPort: turn,
|
||||
|
||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: 6424ecb77d...6738542d75
Reference in New Issue
Block a user