Compare commits

...
22 changed files with 1412 additions and 122 deletions
@@ -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);
+21 -4
View File
@@ -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;
+10
View File
@@ -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;
}
+12
View File
@@ -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,