mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-23 13:06:45 +08:00
[feat] allocate DeskHub video bandwidth automatically
This commit is contained in:
@@ -37,6 +37,7 @@
|
|||||||
#include "features/gallery/gallery_fullscreen_transition.h"
|
#include "features/gallery/gallery_fullscreen_transition.h"
|
||||||
#include "features/gallery/gallery_window_state.h"
|
#include "features/gallery/gallery_window_state.h"
|
||||||
#include "features/gallery/gallery_workspace.h"
|
#include "features/gallery/gallery_workspace.h"
|
||||||
|
#include "features/gallery/preview_bandwidth.h"
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#include "platform/video_renderer.h"
|
#include "platform/video_renderer.h"
|
||||||
@@ -873,6 +874,14 @@ struct GuiApplication::SlintUi {
|
|||||||
slint::Image image;
|
slint::Image image;
|
||||||
};
|
};
|
||||||
std::unordered_map<std::string, Preview> gallery_previews;
|
std::unordered_map<std::string, Preview> gallery_previews;
|
||||||
|
struct PendingPreview {
|
||||||
|
std::shared_ptr<RemoteSession> props;
|
||||||
|
PreviewSubscription demand;
|
||||||
|
bool needs_sync = false;
|
||||||
|
};
|
||||||
|
std::vector<PendingPreview> gallery_pending_previews;
|
||||||
|
std::vector<gallery::PreviewBandwidthDemand> gallery_bandwidth_demands;
|
||||||
|
gallery::PreviewBandwidthAllocator gallery_bandwidth_allocator;
|
||||||
struct BatchOperation {
|
struct BatchOperation {
|
||||||
std::string id, action, file;
|
std::string id, action, file;
|
||||||
// Only explicit add-device requests carry credentials; never persisted.
|
// Only explicit add-device requests carry credentials; never persisted.
|
||||||
@@ -1321,6 +1330,8 @@ void GuiApplication::ResetSettingsUi() {
|
|||||||
ui_->main->set_gallery_frame_rate_index(ui_->gallery.stream_settings.FrameRateIndex());
|
ui_->main->set_gallery_frame_rate_index(ui_->gallery.stream_settings.FrameRateIndex());
|
||||||
ui_->main->set_gallery_resolution_draft(ui_->gallery.stream_settings.resolution);
|
ui_->main->set_gallery_resolution_draft(ui_->gallery.stream_settings.resolution);
|
||||||
ui_->main->set_gallery_frame_rate_draft(ui_->gallery.stream_settings.FrameRateIndex());
|
ui_->main->set_gallery_frame_rate_draft(ui_->gallery.stream_settings.FrameRateIndex());
|
||||||
|
ui_->main->set_gallery_bandwidth_index(ui_->gallery.stream_settings.BandwidthIndex());
|
||||||
|
ui_->main->set_gallery_bandwidth_draft(ui_->gallery.stream_settings.BandwidthIndex());
|
||||||
ui_->main->set_gallery_settings_error("");
|
ui_->main->set_gallery_settings_error("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3721,10 +3732,12 @@ bool GuiApplication::SaveSettingsFromUi() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto wall_settings = gallery::StreamSettings::FromIndices(
|
const auto wall_settings = gallery::StreamSettings::FromIndices(
|
||||||
main->get_gallery_resolution_draft(), main->get_gallery_frame_rate_draft());
|
main->get_gallery_resolution_draft(), main->get_gallery_frame_rate_draft(),
|
||||||
|
main->get_gallery_bandwidth_draft());
|
||||||
const bool wall_changed = wall_settings &&
|
const bool wall_changed = wall_settings &&
|
||||||
(wall_settings->resolution != ui_->gallery.stream_settings.resolution ||
|
(wall_settings->resolution != ui_->gallery.stream_settings.resolution ||
|
||||||
wall_settings->frame_rate != ui_->gallery.stream_settings.frame_rate);
|
wall_settings->frame_rate != ui_->gallery.stream_settings.frame_rate ||
|
||||||
|
wall_settings->bandwidth_mbps != ui_->gallery.stream_settings.bandwidth_mbps);
|
||||||
auto updated_wall = ui_->gallery;
|
auto updated_wall = ui_->gallery;
|
||||||
if (wall_settings) updated_wall.stream_settings = *wall_settings;
|
if (wall_settings) updated_wall.stream_settings = *wall_settings;
|
||||||
// Persist the draft first, but leave live sessions unchanged until the
|
// Persist the draft first, but leave live sessions unchanged until the
|
||||||
@@ -5352,9 +5365,25 @@ void GuiApplication::SyncGallery() {
|
|||||||
old.connected != device.connected)
|
old.connected != device.connected)
|
||||||
ui_->gallery_selected_model->set_row_data(i, device);
|
ui_->gallery_selected_model->set_row_data(i, device);
|
||||||
}
|
}
|
||||||
|
auto& pending_previews = ui_->gallery_pending_previews;
|
||||||
|
auto& bandwidth_demands = ui_->gallery_bandwidth_demands;
|
||||||
|
pending_previews.clear();
|
||||||
|
bandwidth_demands.clear();
|
||||||
|
const bool stream_window_visible = ui_->stream &&
|
||||||
|
(*ui_->stream)->window().is_visible() &&
|
||||||
|
!(*ui_->stream)->window().is_minimized();
|
||||||
|
const auto stream_viewport = stream_window_visible
|
||||||
|
? gallery::PreviewResolution::Viewport(
|
||||||
|
(*ui_->stream)->get_video_area_width(),
|
||||||
|
(*ui_->stream)->get_video_area_height(),
|
||||||
|
(*ui_->stream)->window().scale_factor())
|
||||||
|
: gallery::PreviewResolution{};
|
||||||
{
|
{
|
||||||
std::shared_lock lock(remote_sessions_mutex_);
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
|
pending_previews.reserve(remote_sessions_.size());
|
||||||
|
bandwidth_demands.reserve(remote_sessions_.size());
|
||||||
for (const auto& [id, props] : remote_sessions_) {
|
for (const auto& [id, props] : remote_sessions_) {
|
||||||
|
if (!props || props->closing_) continue;
|
||||||
if (!preview_ids.count(id)) {
|
if (!preview_ids.count(id)) {
|
||||||
props->gallery_preview_width_ = 0;
|
props->gallery_preview_width_ = 0;
|
||||||
std::unique_lock conversion_lock(props->gallery_conversion_mutex_,
|
std::unique_lock conversion_lock(props->gallery_conversion_mutex_,
|
||||||
@@ -5371,8 +5400,8 @@ void GuiApplication::SyncGallery() {
|
|||||||
props->connection_status_ == ConnectionStatus::Connected) {
|
props->connection_status_ == ConnectionStatus::Connected) {
|
||||||
if (wall_mode && !ui_->gallery_hidden_active_ids.count(id))
|
if (wall_mode && !ui_->gallery_hidden_active_ids.count(id))
|
||||||
props->wall_managed_ = true;
|
props->wall_managed_ = true;
|
||||||
// Keep session quality and audio; wall settings override only the
|
// Keep session quality and audio; wall bandwidth, resolution and
|
||||||
// requested resolution/cadence and never overwrite session preferences.
|
// cadence requests never overwrite session preferences.
|
||||||
PreviewSubscription demand;
|
PreviewSubscription demand;
|
||||||
{
|
{
|
||||||
std::lock_guard settings_lock(props->video_settings_mutex_);
|
std::lock_guard settings_lock(props->video_settings_mutex_);
|
||||||
@@ -5383,42 +5412,66 @@ void GuiApplication::SyncGallery() {
|
|||||||
if (props->wall_managed_)
|
if (props->wall_managed_)
|
||||||
wall.stream_settings.ApplyFrameRate(demand, props->preview_fps15_supported_);
|
wall.stream_settings.ApplyFrameRate(demand, props->preview_fps15_supported_);
|
||||||
const bool needs_sync = props->preview_needs_sync_.exchange(false);
|
const bool needs_sync = props->preview_needs_sync_.exchange(false);
|
||||||
|
const bool stream_visible = stream_window_visible &&
|
||||||
|
props->tab_opened_ && id == focused_remote_id_;
|
||||||
if (props->preview_resolution_supported_) {
|
if (props->preview_resolution_supported_) {
|
||||||
gallery::PreviewResolution stream_viewport;
|
|
||||||
if (props->tab_opened_ && id == focused_remote_id_ && ui_->stream &&
|
|
||||||
(*ui_->stream)->window().is_visible() &&
|
|
||||||
!(*ui_->stream)->window().is_minimized()) {
|
|
||||||
stream_viewport = gallery::PreviewResolution::Viewport(
|
|
||||||
(*ui_->stream)->get_video_area_width(),
|
|
||||||
(*ui_->stream)->get_video_area_height(),
|
|
||||||
(*ui_->stream)->window().scale_factor());
|
|
||||||
}
|
|
||||||
const auto resolution = props->preview_resolution_.Update(
|
const auto resolution = props->preview_resolution_.Update(
|
||||||
wall.stream_settings.Resolution(
|
wall.stream_settings.Resolution(
|
||||||
props->wall_managed_, ui_->gallery_focus == id
|
props->wall_managed_, ui_->gallery_focus == id
|
||||||
? gallery_viewport : gallery::PreviewResolution{},
|
? gallery_viewport : gallery::PreviewResolution{},
|
||||||
stream_viewport), now, needs_sync);
|
stream_visible ? stream_viewport : gallery::PreviewResolution{}),
|
||||||
|
now, needs_sync);
|
||||||
demand.version = 2;
|
demand.version = 2;
|
||||||
demand.width = resolution.width;
|
demand.width = resolution.width;
|
||||||
demand.height = resolution.height;
|
demand.height = resolution.height;
|
||||||
}
|
}
|
||||||
const bool acknowledged =
|
if (props->wall_managed_ && !preview_ids.count(id) && !stream_visible)
|
||||||
props->preview_ack_.load() == props->preview_request_ &&
|
demand.fps = 0;
|
||||||
props->preview_accepted_.load();
|
gallery::PreviewBandwidthDemand bandwidth;
|
||||||
if (needs_sync || !demand.SameSettings(props->preview_requested_) ||
|
if (props->preview_bandwidth_supported_) {
|
||||||
(!acknowledged && now - props->preview_request_at_ > 5s)) {
|
demand.max_bitrate_bps = 0;
|
||||||
demand.request_id = ++props->preview_request_;
|
if (props->wall_managed_ && demand.fps > 0) {
|
||||||
if (!demand.request_id) demand.request_id = ++props->preview_request_;
|
bandwidth = {
|
||||||
const auto message = demand.ToJson().dump();
|
gallery::PreviewBandwidthDemand::Estimate(
|
||||||
if (SendReliableDataFrame(props->peer_, message.data(),
|
demand.width, demand.height,
|
||||||
message.size(),
|
demand.fps == 60 ? demand.frame_rate : demand.fps),
|
||||||
props->control_data_label_.c_str()) == 0)
|
stream_visible || (visible && ui_->gallery_focus == id)};
|
||||||
props->preview_requested_ = demand;
|
}
|
||||||
props->preview_request_at_ = now;
|
|
||||||
}
|
}
|
||||||
|
bandwidth_demands.push_back(bandwidth);
|
||||||
|
pending_previews.push_back({props, demand, needs_sync});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const auto& allocations = ui_->gallery_bandwidth_allocator.Allocate(
|
||||||
|
bandwidth_demands, wall.stream_settings.bandwidth_mbps * 1'000'000);
|
||||||
|
for (size_t i = 0; i < pending_previews.size(); ++i) {
|
||||||
|
if (bandwidth_demands[i].desired_bps == 0) continue;
|
||||||
|
auto& demand = pending_previews[i].demand;
|
||||||
|
// Quantize down to avoid resending subscriptions for sub-kbit rounding.
|
||||||
|
const int bitrate = allocations[i] / 1000 * 1000;
|
||||||
|
demand.max_bitrate_bps = bitrate;
|
||||||
|
if (!bitrate) demand.fps = 0;
|
||||||
|
}
|
||||||
|
for (auto& pending : pending_previews) {
|
||||||
|
auto& props = pending.props;
|
||||||
|
auto& demand = pending.demand;
|
||||||
|
const bool acknowledged =
|
||||||
|
props->preview_ack_.load() == props->preview_request_ &&
|
||||||
|
props->preview_accepted_.load();
|
||||||
|
if (pending.needs_sync || !demand.SameSettings(props->preview_requested_) ||
|
||||||
|
(!acknowledged && now - props->preview_request_at_ > 5s)) {
|
||||||
|
demand.request_id = ++props->preview_request_;
|
||||||
|
if (!demand.request_id) demand.request_id = ++props->preview_request_;
|
||||||
|
const auto message = demand.ToJson().dump();
|
||||||
|
if (SendReliableDataFrame(props->peer_, message.data(), message.size(),
|
||||||
|
props->control_data_label_.c_str()) == 0)
|
||||||
|
props->preview_requested_ = demand;
|
||||||
|
props->preview_request_at_ = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reuse capacity without retaining disconnected sessions between refreshes.
|
||||||
|
pending_previews.clear();
|
||||||
std::erase_if(ui_->gallery_previews, [&](const auto& item) {
|
std::erase_if(ui_->gallery_previews, [&](const auto& item) {
|
||||||
return !preview_ids.count(item.first);
|
return !preview_ids.count(item.first);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,22 +23,31 @@ struct StreamSettings {
|
|||||||
{}, {320, 180}, {640, 360}, {1280, 720}, {1920, 1080},
|
{}, {320, 180}, {640, 360}, {1280, 720}, {1920, 1080},
|
||||||
{2560, 1440}, {3840, 2160}, {}}};
|
{2560, 1440}, {3840, 2160}, {}}};
|
||||||
static constexpr std::array<int, 4> kFrameRates{0, 15, 30, 60};
|
static constexpr std::array<int, 4> kFrameRates{0, 15, 30, 60};
|
||||||
|
static constexpr std::array<int, 6> kBandwidthMbps{2, 5, 10, 20, 50, 100};
|
||||||
int resolution = 0;
|
int resolution = 0;
|
||||||
int frame_rate = 0; // 0 follows the session's existing 30/60 FPS preference.
|
int frame_rate = 0; // 0 follows the session's existing 30/60 FPS preference.
|
||||||
|
int bandwidth_mbps = 20;
|
||||||
|
|
||||||
static std::optional<StreamSettings> FromIndices(int resolution, int fps) {
|
static std::optional<StreamSettings> FromIndices(int resolution, int fps,
|
||||||
|
int bandwidth = 3) {
|
||||||
if (resolution < 0 || resolution >= int(kResolutions.size()) ||
|
if (resolution < 0 || resolution >= int(kResolutions.size()) ||
|
||||||
fps < 0 || fps >= int(kFrameRates.size()))
|
fps < 0 || fps >= int(kFrameRates.size()) ||
|
||||||
|
bandwidth < 0 || bandwidth >= int(kBandwidthMbps.size()))
|
||||||
return {};
|
return {};
|
||||||
return StreamSettings{resolution, kFrameRates[fps]};
|
return StreamSettings{resolution, kFrameRates[fps], kBandwidthMbps[bandwidth]};
|
||||||
}
|
}
|
||||||
int FrameRateIndex() const {
|
int FrameRateIndex() const {
|
||||||
return int(std::find(kFrameRates.begin(), kFrameRates.end(), frame_rate) -
|
return int(std::find(kFrameRates.begin(), kFrameRates.end(), frame_rate) -
|
||||||
kFrameRates.begin());
|
kFrameRates.begin());
|
||||||
}
|
}
|
||||||
|
int BandwidthIndex() const {
|
||||||
|
return int(std::find(kBandwidthMbps.begin(), kBandwidthMbps.end(), bandwidth_mbps) -
|
||||||
|
kBandwidthMbps.begin());
|
||||||
|
}
|
||||||
bool Valid() const {
|
bool Valid() const {
|
||||||
return resolution >= 0 && resolution < int(kResolutions.size()) &&
|
return resolution >= 0 && resolution < int(kResolutions.size()) &&
|
||||||
FrameRateIndex() < int(kFrameRates.size());
|
FrameRateIndex() < int(kFrameRates.size()) &&
|
||||||
|
BandwidthIndex() < int(kBandwidthMbps.size());
|
||||||
}
|
}
|
||||||
static StreamSettings FromJson(const nlohmann::json& json) {
|
static StreamSettings FromJson(const nlohmann::json& json) {
|
||||||
try {
|
try {
|
||||||
@@ -50,13 +59,19 @@ struct StreamSettings {
|
|||||||
fps < 0 || fps > 60) return {};
|
fps < 0 || fps > 60) return {};
|
||||||
// Retired sparse presets migrate to the lowest current setting.
|
// Retired sparse presets migrate to the lowest current setting.
|
||||||
StreamSettings result{int(resolution), fps == 1 || fps == 8 ? 15 : int(fps)};
|
StreamSettings result{int(resolution), fps == 1 || fps == 8 ? 15 : int(fps)};
|
||||||
|
if (json.contains("bandwidth_mbps") && json.at("bandwidth_mbps").is_number_integer()) {
|
||||||
|
const auto bandwidth = json.at("bandwidth_mbps").get<int64_t>();
|
||||||
|
if (std::find(kBandwidthMbps.begin(), kBandwidthMbps.end(), bandwidth) !=
|
||||||
|
kBandwidthMbps.end()) result.bandwidth_mbps = int(bandwidth);
|
||||||
|
}
|
||||||
return result.Valid() ? result : StreamSettings{};
|
return result.Valid() ? result : StreamSettings{};
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nlohmann::json ToJson() const {
|
nlohmann::json ToJson() const {
|
||||||
return {{"resolution", resolution}, {"frame_rate", frame_rate}};
|
return {{"resolution", resolution}, {"frame_rate", frame_rate},
|
||||||
|
{"bandwidth_mbps", bandwidth_mbps}};
|
||||||
}
|
}
|
||||||
PreviewResolution Resolution(bool wall_managed, PreviewResolution wall_view,
|
PreviewResolution Resolution(bool wall_managed, PreviewResolution wall_view,
|
||||||
PreviewResolution stream_view) const {
|
PreviewResolution stream_view) const {
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2026-10-13
|
||||||
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _PREVIEW_BANDWIDTH_H_
|
||||||
|
#define _PREVIEW_BANDWIDTH_H_
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace crossdesk::gallery {
|
||||||
|
struct PreviewBandwidthDemand {
|
||||||
|
int desired_bps = 0;
|
||||||
|
bool foreground = false;
|
||||||
|
|
||||||
|
bool operator==(const PreviewBandwidthDemand& other) const {
|
||||||
|
return desired_bps == other.desired_bps && foreground == other.foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Estimate(int width, int height, int fps) {
|
||||||
|
// A demand ceiling, not a throughput measurement. Include room for video
|
||||||
|
// repair traffic; the sender's congestion control may allocate less.
|
||||||
|
const int64_t pixels = width > 0 && height > 0
|
||||||
|
? int64_t(width) * height : 3840LL * 2160;
|
||||||
|
const int64_t estimate = pixels * std::clamp(fps, 1, 60) * 15 / 100;
|
||||||
|
return int(std::clamp<int64_t>((estimate + 49'999) / 50'000 * 50'000,
|
||||||
|
150'000, 20'000'000));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::vector<int> AllocatePreviewBandwidth(
|
||||||
|
const std::vector<PreviewBandwidthDemand>& demands, int budget_bps) {
|
||||||
|
std::vector<int> result(demands.size(), 0);
|
||||||
|
int remaining = std::max(0, budget_bps);
|
||||||
|
int active = 0;
|
||||||
|
int64_t desired_total = 0;
|
||||||
|
for (const auto& demand : demands) {
|
||||||
|
active += demand.desired_bps > 0;
|
||||||
|
desired_total += std::max(0, demand.desired_bps);
|
||||||
|
}
|
||||||
|
if (!active || !remaining) return result;
|
||||||
|
if (desired_total <= remaining) {
|
||||||
|
for (std::size_t i = 0; i < demands.size(); ++i)
|
||||||
|
result[i] = std::max(0, demands[i].desired_bps);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// First preserve a small fair share for every visible device, then perform
|
||||||
|
// weighted max-min allocation. Satisfied devices return unused capacity.
|
||||||
|
const int baseline = std::min(100'000, remaining / active);
|
||||||
|
for (std::size_t i = 0; i < demands.size(); ++i) {
|
||||||
|
result[i] = std::min(std::max(0, demands[i].desired_bps), baseline);
|
||||||
|
remaining -= result[i];
|
||||||
|
}
|
||||||
|
while (remaining > 0) {
|
||||||
|
int weights = 0;
|
||||||
|
for (std::size_t i = 0; i < demands.size(); ++i)
|
||||||
|
if (result[i] < demands[i].desired_bps)
|
||||||
|
weights += demands[i].foreground ? 4 : 1;
|
||||||
|
if (!weights) break;
|
||||||
|
const int available = remaining;
|
||||||
|
for (std::size_t i = 0; i < demands.size() && remaining > 0; ++i) {
|
||||||
|
if (result[i] >= demands[i].desired_bps) continue;
|
||||||
|
const int share = int(int64_t(available) *
|
||||||
|
(demands[i].foreground ? 4 : 1) / weights);
|
||||||
|
const int extra = std::min({remaining, demands[i].desired_bps - result[i],
|
||||||
|
std::max(1, share)});
|
||||||
|
result[i] += extra;
|
||||||
|
remaining -= extra;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI refreshes much more often than video demand changes. Keep the previous
|
||||||
|
// result while membership, dimensions, cadence, priority and budget are stable.
|
||||||
|
class PreviewBandwidthAllocator {
|
||||||
|
public:
|
||||||
|
const std::vector<int>& Allocate(
|
||||||
|
const std::vector<PreviewBandwidthDemand>& demands, int budget_bps) {
|
||||||
|
if (budget_bps_ != budget_bps || demands_ != demands) {
|
||||||
|
demands_ = demands;
|
||||||
|
budget_bps_ = budget_bps;
|
||||||
|
allocations_ = AllocatePreviewBandwidth(demands, budget_bps);
|
||||||
|
}
|
||||||
|
return allocations_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
int budget_bps_ = -1;
|
||||||
|
std::vector<PreviewBandwidthDemand> demands_;
|
||||||
|
std::vector<int> allocations_;
|
||||||
|
};
|
||||||
|
} // namespace crossdesk::gallery
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -177,8 +177,13 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
subscription->frame_rate,
|
subscription->frame_rate,
|
||||||
static_cast<VideoDegradationPreference>(
|
static_cast<VideoDegradationPreference>(
|
||||||
subscription->EncoderPreference())) == 0;
|
subscription->EncoderPreference())) == 0;
|
||||||
// Audio-only updates must not force a keyframe. The pause API requests
|
if (accepted && (!previous || previous->max_bitrate_bps.value_or(0) !=
|
||||||
// its own keyframes on resume.
|
subscription->max_bitrate_bps.value_or(0)))
|
||||||
|
accepted = UpdateVideoBitrateLimit(
|
||||||
|
runtime->peer_, remote_id.data(), remote_id.size(),
|
||||||
|
subscription->max_bitrate_bps.value_or(0)) == 0;
|
||||||
|
// Rebalancing bandwidth alone must not force every device to send a
|
||||||
|
// keyframe. The pause API requests its own keyframes on resume.
|
||||||
if (accepted && (!previous || previous->width != subscription->width ||
|
if (accepted && (!previous || previous->width != subscription->width ||
|
||||||
previous->height != subscription->height))
|
previous->height != subscription->height))
|
||||||
accepted = UpdateVideoResolution(
|
accepted = UpdateVideoResolution(
|
||||||
@@ -419,6 +424,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
props->preview_resolution_supported_ =
|
props->preview_resolution_supported_ =
|
||||||
remote_action.i.supports_deskhub_resolution;
|
remote_action.i.supports_deskhub_resolution;
|
||||||
props->preview_fps15_supported_ = remote_action.i.supports_deskhub_fps15;
|
props->preview_fps15_supported_ = remote_action.i.supports_deskhub_fps15;
|
||||||
|
props->preview_bandwidth_supported_ = remote_action.i.supports_deskhub_bandwidth;
|
||||||
props->video_settings_supported_ =
|
props->video_settings_supported_ =
|
||||||
remote_action.i.supports_video_settings;
|
remote_action.i.supports_video_settings;
|
||||||
if (first_advertisement && props->video_settings_supported_) {
|
if (first_advertisement && props->video_settings_supported_) {
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
remote_action.i.supports_deskhub_preview = true;
|
remote_action.i.supports_deskhub_preview = true;
|
||||||
remote_action.i.supports_deskhub_resolution = true;
|
remote_action.i.supports_deskhub_resolution = true;
|
||||||
remote_action.i.supports_deskhub_fps15 = true;
|
remote_action.i.supports_deskhub_fps15 = true;
|
||||||
|
remote_action.i.supports_deskhub_bandwidth = true;
|
||||||
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
|
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
|
||||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||||
remote_action.i.display_list[i]);
|
remote_action.i.display_list[i]);
|
||||||
@@ -335,6 +336,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
props->preview_subscription_supported_ = false;
|
props->preview_subscription_supported_ = false;
|
||||||
props->preview_resolution_supported_ = false;
|
props->preview_resolution_supported_ = false;
|
||||||
props->preview_fps15_supported_ = false;
|
props->preview_fps15_supported_ = false;
|
||||||
|
props->preview_bandwidth_supported_ = false;
|
||||||
props->preview_needs_sync_ = true;
|
props->preview_needs_sync_ = true;
|
||||||
props->preview_ack_ = 0;
|
props->preview_ack_ = 0;
|
||||||
props->preview_accepted_ = false;
|
props->preview_accepted_ = false;
|
||||||
@@ -440,6 +442,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
remote_action.i.supports_deskhub_preview = true;
|
remote_action.i.supports_deskhub_preview = true;
|
||||||
remote_action.i.supports_deskhub_resolution = true;
|
remote_action.i.supports_deskhub_resolution = true;
|
||||||
remote_action.i.supports_deskhub_fps15 = true;
|
remote_action.i.supports_deskhub_fps15 = true;
|
||||||
|
remote_action.i.supports_deskhub_bandwidth = true;
|
||||||
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
|
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
|
||||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||||
remote_action.i.display_list[i]);
|
remote_action.i.display_list[i]);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ struct RemoteSession {
|
|||||||
std::atomic<bool> preview_subscription_supported_{false};
|
std::atomic<bool> preview_subscription_supported_{false};
|
||||||
std::atomic<bool> preview_resolution_supported_{false};
|
std::atomic<bool> preview_resolution_supported_{false};
|
||||||
std::atomic<bool> preview_fps15_supported_{false};
|
std::atomic<bool> preview_fps15_supported_{false};
|
||||||
|
std::atomic<bool> preview_bandwidth_supported_{false};
|
||||||
std::atomic<bool> preview_needs_sync_{true};
|
std::atomic<bool> preview_needs_sync_{true};
|
||||||
std::atomic<uint32_t> preview_ack_{0};
|
std::atomic<uint32_t> preview_ack_{0};
|
||||||
std::atomic<bool> preview_accepted_{false};
|
std::atomic<bool> preview_accepted_{false};
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { SettingsGroup, SettingsRow, SettingsSelect } from "settings_controls.sl
|
|||||||
export component GallerySettings inherits Rectangle {
|
export component GallerySettings inherits Rectangle {
|
||||||
in-out property <int> resolution-index: 0;
|
in-out property <int> resolution-index: 0;
|
||||||
in-out property <int> frame-rate-index: 0;
|
in-out property <int> frame-rate-index: 0;
|
||||||
|
in-out property <int> bandwidth-index: 3;
|
||||||
in property <string> error;
|
in property <string> error;
|
||||||
SettingsGroup {
|
SettingsGroup {
|
||||||
y: 0px; width: parent.width;
|
y: 0px; width: parent.width;
|
||||||
title: GalleryStrings.tr("画面传输", "Video transmission", "Передача видео"); rows: 2;
|
title: GalleryStrings.tr("画面传输", "Video transmission", "Передача видео"); rows: 3;
|
||||||
SettingsRow {
|
SettingsRow {
|
||||||
label: GalleryStrings.tr("分辨率上限", "Resolution limit", "Разрешение");
|
label: GalleryStrings.tr("分辨率上限", "Resolution limit", "Разрешение");
|
||||||
label-reserve: self.width - 112px;
|
label-reserve: self.width - 112px;
|
||||||
@@ -21,16 +22,33 @@ export component GallerySettings inherits Rectangle {
|
|||||||
}
|
}
|
||||||
SettingsRow {
|
SettingsRow {
|
||||||
label: GalleryStrings.tr("帧率", "Frame rate", "Частота кадров");
|
label: GalleryStrings.tr("帧率", "Frame rate", "Частота кадров");
|
||||||
label-reserve: self.width - 112px; divider: false;
|
label-reserve: self.width - 112px;
|
||||||
SettingsSelect {
|
SettingsSelect {
|
||||||
x: 136px; y: 4px; width: parent.width - 150px;
|
x: 136px; y: 4px; width: parent.width - 150px;
|
||||||
current-index <=> root.frame-rate-index;
|
current-index <=> root.frame-rate-index;
|
||||||
model: [GalleryStrings.tr("跟随设备设置", "Follow device settings", "Настройки устройства"), "15 FPS", "30 FPS", "60 FPS"];
|
model: [GalleryStrings.tr("跟随设备设置", "Follow device settings", "Настройки устройства"), "15 FPS", "30 FPS", "60 FPS"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SettingsRow {
|
||||||
|
label: GalleryStrings.tr("视频总带宽", "Video budget", "Бюджет видео");
|
||||||
|
label-reserve: self.width - 112px; divider: false;
|
||||||
|
SettingsSelect {
|
||||||
|
x: 136px; y: 4px; width: parent.width - 150px;
|
||||||
|
current-index <=> root.bandwidth-index;
|
||||||
|
model: ["2 Mbps", "5 Mbps", "10 Mbps", "20 Mbps", "50 Mbps", "100 Mbps"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
x: 2px; y: 144px; width: parent.width - 4px; height: 70px;
|
||||||
|
text: GalleryStrings.tr(
|
||||||
|
"可见设备共享视频预算,优先保障展开画面;不可见设备暂停视频。音频与文件传输不计入,旧版设备不支持限额。瞬时速率可能超过预算。",
|
||||||
|
"Visible devices share the video budget, with priority for expanded views. Hidden video pauses. Audio, files and older devices are excluded; brief bursts may exceed the budget.",
|
||||||
|
"Бюджет видео делится между видимыми устройствами с приоритетом крупного вида. Скрытое видео приостановлено. Звук, файлы и старые устройства не учитываются; возможны краткие превышения.");
|
||||||
|
color: #718096; font-size: 11px; wrap: word-wrap;
|
||||||
}
|
}
|
||||||
if root.error != "": Text {
|
if root.error != "": Text {
|
||||||
x: 2px; y: 108px; width: parent.width - 4px; height: 32px;
|
x: 2px; y: 224px; width: parent.width - 4px; height: 32px;
|
||||||
text: root.error; color: #c53e3e; font-size: 11px; wrap: word-wrap;
|
text: root.error; color: #c53e3e; font-size: 11px; wrap: word-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,8 +315,10 @@ export component MainWindow inherits Window {
|
|||||||
in-out property <bool> gallery-network-open: false;
|
in-out property <bool> gallery-network-open: false;
|
||||||
in property <int> gallery-resolution-index: 0;
|
in property <int> gallery-resolution-index: 0;
|
||||||
in property <int> gallery-frame-rate-index: 0;
|
in property <int> gallery-frame-rate-index: 0;
|
||||||
|
in property <int> gallery-bandwidth-index: 3;
|
||||||
in-out property <int> gallery-resolution-draft: 0;
|
in-out property <int> gallery-resolution-draft: 0;
|
||||||
in-out property <int> gallery-frame-rate-draft: 0;
|
in-out property <int> gallery-frame-rate-draft: 0;
|
||||||
|
in-out property <int> gallery-bandwidth-draft: 3;
|
||||||
in property <string> gallery-settings-error;
|
in property <string> gallery-settings-error;
|
||||||
in property <[GalleryNetworkDevice]> gallery-network-devices;
|
in property <[GalleryNetworkDevice]> gallery-network-devices;
|
||||||
changed gallery-network-open => {
|
changed gallery-network-open => {
|
||||||
@@ -572,6 +574,7 @@ export component MainWindow inherits Window {
|
|||||||
root.gallery-network-open = false;
|
root.gallery-network-open = false;
|
||||||
root.gallery-resolution-draft = root.gallery-resolution-index;
|
root.gallery-resolution-draft = root.gallery-resolution-index;
|
||||||
root.gallery-frame-rate-draft = root.gallery-frame-rate-index;
|
root.gallery-frame-rate-draft = root.gallery-frame-rate-index;
|
||||||
|
root.gallery-bandwidth-draft = root.gallery-bandwidth-index;
|
||||||
root.gallery-action("settings-open", "");
|
root.gallery-action("settings-open", "");
|
||||||
focus-sink.focus();
|
focus-sink.focus();
|
||||||
}
|
}
|
||||||
@@ -2088,6 +2091,7 @@ export component MainWindow inherits Window {
|
|||||||
x: 18px; y: 108px; width: parent.width - 36px; height: 272px;
|
x: 18px; y: 108px; width: parent.width - 36px; height: 272px;
|
||||||
resolution-index <=> root.gallery-resolution-draft;
|
resolution-index <=> root.gallery-resolution-draft;
|
||||||
frame-rate-index <=> root.gallery-frame-rate-draft;
|
frame-rate-index <=> root.gallery-frame-rate-draft;
|
||||||
|
bandwidth-index <=> root.gallery-bandwidth-draft;
|
||||||
error: root.gallery-settings-error;
|
error: root.gallery-settings-error;
|
||||||
}
|
}
|
||||||
Rectangle { x: 18px; y: 382px; width: parent.width - 36px; height: 1px; background: #e6ebf2; }
|
Rectangle { x: 18px; y: 382px; width: parent.width - 36px; height: 1px; background: #e6ebf2; }
|
||||||
@@ -2096,7 +2100,7 @@ export component MainWindow inherits Window {
|
|||||||
if root.settings-tab == 1: SettingsButton {
|
if root.settings-tab == 1: SettingsButton {
|
||||||
width: 110px; quiet: true;
|
width: 110px; quiet: true;
|
||||||
text: GalleryStrings.tr("恢复默认", "Reset defaults", "По умолчанию");
|
text: GalleryStrings.tr("恢复默认", "Reset defaults", "По умолчанию");
|
||||||
clicked => { root.gallery-resolution-draft = 0; root.gallery-frame-rate-draft = 0; }
|
clicked => { root.gallery-resolution-draft = 0; root.gallery-frame-rate-draft = 0; root.gallery-bandwidth-draft = 3; }
|
||||||
}
|
}
|
||||||
Rectangle { horizontal-stretch: 1; }
|
Rectangle { horizontal-stretch: 1; }
|
||||||
SettingsButton {
|
SettingsButton {
|
||||||
|
|||||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: ac48aa4b70...2818e2dd15
+14
-2
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
## 预览与兼容性
|
## 预览与兼容性
|
||||||
|
|
||||||
自动分辨率模式下,桌面墙发起或接管的会话在能力协商后默认请求最长边不超过 320 像素的画面。点卡片展开后,根据视频显示区域的实际物理像素(包含 HiDPI 缩放)请求分辨率;全屏、退出全屏和调整窗口大小后会重新计算,尺寸稳定 200 毫秒后更新订阅。返回网格、隐藏桌面墙或切换到其他设备后恢复缩略图尺寸。同一会话同时在独立串流窗口显示时,按两个可见视频区域中较大的尺寸请求,避免小预览降低大窗口的清晰度。选择固定或原始分辨率后使用对应上限;恢复自动后重新跟随视图。普通连接页发起且未加入桌面墙管理的会话保留原有分辨率行为。
|
自动分辨率模式下,桌面墙发起或接管的会话在能力协商后默认请求最长边不超过 320 像素的画面。点卡片展开后,根据视频显示区域的实际物理像素(包含 HiDPI 缩放)请求分辨率;全屏、退出全屏和调整窗口大小后会重新计算,尺寸稳定 200 毫秒后更新订阅。返回网格或切换到其他设备后恢复缩略图尺寸。不可见设备暂停视频;如果同一设备仍在可见的独立串流窗口显示,则继续传输。同一会话同时在独立串流窗口显示时,按两个可见视频区域中较大的尺寸请求,避免小预览降低大窗口的清晰度。选择固定或原始分辨率后使用对应上限;恢复自动后重新跟随视图。普通连接页发起且未加入桌面墙管理的会话保留原有分辨率行为。
|
||||||
|
|
||||||
被控端按连接分别在编码前缩放,完整保留桌面比例,不放大超过源画面的尺寸,也不修改被控端屏幕的系统分辨率。请求的宽高是上限,画质和网络自适应仍可降低实际发送尺寸。默认跟随每个会话原有的 30/60 帧设置;指定帧率仅覆盖桌面墙订阅,不修改会话的画质或原有帧率偏好。声音仍需明确开启。
|
被控端按连接分别在编码前缩放,完整保留桌面比例,不放大超过源画面的尺寸,也不修改被控端屏幕的系统分辨率。请求的宽高是上限,画质和网络自适应仍可降低实际发送尺寸。默认跟随每个会话原有的 30/60 帧设置;指定帧率仅覆盖桌面墙订阅,不修改会话的画质或原有帧率偏好。声音仍需明确开启。
|
||||||
|
|
||||||
@@ -39,15 +39,27 @@
|
|||||||
|
|
||||||
连接旧版被控端时,若仅声明 `supports_deskhub_preview`,控制端发送原有 v1 订阅;若未声明预览能力,则不发送该订阅,沿用其视频策略。旧控制端连接新版被控端时继续使用原分辨率;v1 订阅也会清除此前的尺寸限制。只有同时声明 `supports_deskhub_preview` 与 `supports_deskhub_resolution` 的被控端才会收到 v2 尺寸请求,旧版本不承诺发送端节省资源。
|
连接旧版被控端时,若仅声明 `supports_deskhub_preview`,控制端发送原有 v1 订阅;若未声明预览能力,则不发送该订阅,沿用其视频策略。旧控制端连接新版被控端时继续使用原分辨率;v1 订阅也会清除此前的尺寸限制。只有同时声明 `supports_deskhub_preview` 与 `supports_deskhub_resolution` 的被控端才会收到 v2 尺寸请求,旧版本不承诺发送端节省资源。
|
||||||
|
|
||||||
|
### 视频带宽分配
|
||||||
|
|
||||||
|
“设置 → 桌面墙 → 视频总带宽”配置所有支持带宽协商的桌面墙会话共享的视频预算,默认 20 Mbps,可选 2、5、10、20、50、100 Mbps。保存立即应用,取消不生效,重启后保留;恢复默认后需要保存。它是视频的持续目标预算,包含发送端为视频纠错、重传及 RTP 开销预留的份额;音频、文件传输、普通连接及不支持带宽协商的旧版设备不计入,关键帧和切换期间的瞬时速率可能超过预算。
|
||||||
|
|
||||||
|
控制端根据请求的分辨率和帧率估算每台设备的需求上限。先为每台可见设备保留至多 100 Kbps 的基础份额(总预算不足时等分),再按权重分配剩余预算:展开画面或可见的独立串流窗口权重为缩略图的 4 倍。相同规格、相同优先级的设备基本均分;达到需求上限的设备让出剩余额度,不强制用满总预算。默认缩略图 320 × 320 上限、30 FPS 的需求预算为 500 Kbps。分配随连接、分页、分组、窗口可见性以及分辨率/帧率变化更新,独立串流窗口和墙内画面使用同一份设备预算。需求和总预算未变化时复用分配结果,不随 UI 刷新重复计算。
|
||||||
|
|
||||||
|
翻页、切换分组、隐藏或最小化桌面墙后,没有可见画面的墙内会话发送 `fps=0` 暂停视频,保留连接和主动开启的音频。重新可见后恢复当前需求并请求关键帧。单纯重新分配带宽不会触发所有设备同时发送关键帧,也不会重置 15 FPS 的发送节奏。
|
||||||
|
|
||||||
|
预算不代表测速结果。每条连接原有的拥塞控制继续工作,发送端使用网络可用带宽与设备预算中的较小值,并在设备的多个活动显示流之间再分配。视频探测速率受同一设备额度约束,以便网络恢复或画面展开后继续升速;额度下降会取消旧探测,暂停视频时关闭探测和填充包。只有声明 `supports_deskhub_bandwidth` 的对端会收到带宽字段;支持预览但不支持带宽协商的旧版仍能暂停不可见视频,更旧版本保持原行为。
|
||||||
|
|
||||||
### 协议与接口
|
### 协议与接口
|
||||||
|
|
||||||
可靠控制通道的 `deskhub_preview` v2 保留 v1 的 `request_id`、`fps`、`quality`、`frame_rate`、`preference`、`audio` 字段,新增必填 `width`、`height` 整数,单位为物理像素。两个值均为 0 表示恢复源尺寸上限,否则两个值均须在 2~16384 之间;非法或缺失的值不应用。`deskhub_preview_status` 回传请求的协议版本、请求 ID 与 `accepted`。请求 ID 跳过 0,支持 32 位回绕,重复或过期请求不会覆盖新设置。断开会话清除订阅和 15 FPS 协商能力,再连接会重新发送当前需求。15 FPS 扩展通过可选能力 `supports_deskhub_fps15` 协商,沿用 v1/v2 的字段结构,使用 `fps=15, frame_rate=30`,在编码前将实际输入限制为 15 FPS。
|
可靠控制通道的 `deskhub_preview` v2 保留 v1 的 `request_id`、`fps`、`quality`、`frame_rate`、`preference`、`audio` 字段,新增必填 `width`、`height` 整数,单位为物理像素。两个值均为 0 表示恢复源尺寸上限,否则两个值均须在 2~16384 之间;非法或缺失的值不应用。`deskhub_preview_status` 回传请求的协议版本、请求 ID 与 `accepted`。请求 ID 跳过 0,支持 32 位回绕,重复或过期请求不会覆盖新设置。断开会话清除订阅和 15 FPS 协商能力,再连接会重新发送当前需求。15 FPS 扩展通过可选能力 `supports_deskhub_fps15` 协商,沿用 v1/v2 的字段结构,使用 `fps=15, frame_rate=30`,在编码前将实际输入限制为 15 FPS。
|
||||||
|
|
||||||
|
带宽扩展沿用 v1/v2,增加可选整数 `max_bitrate_bps`(0~100000000,单位 bps),仅向声明 `supports_deskhub_bandwidth` 的对端发送。0 或缺省表示解除额外的视频预算限制;暂停视频仍使用 `fps=0`。非法字段拒绝整个订阅,断开后清除能力,重连重新协商。MiniRTC 的 `UpdateVideoBitrateLimit(peer, remote_id, remote_id_size, max_bitrate_bps)` 为指定连接的所有视频流设置合计预算,不改变音频与数据发送;返回 0 表示已进入拥塞控制队列,-1 表示参数、连接或传输不支持。
|
||||||
|
|
||||||
MiniRTC 新增 `UpdateVideoResolution(peer, remote_id, remote_id_size, width, height)` 接口,返回 0 表示已接受,-1 表示参数、连接或传输类型不支持。它为指定控制端的所有显示流设置独立尺寸上限,切换时丢弃旧设置尚未发出的编码结果,并请求新关键帧;底层自适应也以这个上限作为有效源尺寸。
|
MiniRTC 新增 `UpdateVideoResolution(peer, remote_id, remote_id_size, width, height)` 接口,返回 0 表示已接受,-1 表示参数、连接或传输类型不支持。它为指定控制端的所有显示流设置独立尺寸上限,切换时丢弃旧设置尚未发出的编码结果,并请求新关键帧;底层自适应也以这个上限作为有效源尺寸。
|
||||||
|
|
||||||
`SetVideoSendPaused(peer, remote_id, remote_id_size, paused)` 与 `SetAudioSendPaused(peer, remote_id, remote_id_size, paused)` 分别控制指定连接的所有视频/音频发送通道,`true` 暂停、`false` 恢复。重复设置不产生额外操作;返回 0 表示接受,-1 表示参数、连接或传输类型不支持。视频暂停阻止新帧进入编码,废弃旧编码结果,并在发送队列上清除待发视频/重传/纠错包,停止探测和填充;恢复请求新关键帧。音频暂停阻止 PCM 编码并清理待发音频,恢复时仅发送新音频。已交给网络的包无法撤回。API 保留连接、采集源、码率与画质设置以及其他媒体/数据通道,也不改变其他控制端的发送状态。
|
`SetVideoSendPaused(peer, remote_id, remote_id_size, paused)` 与 `SetAudioSendPaused(peer, remote_id, remote_id_size, paused)` 分别控制指定连接的所有视频/音频发送通道,`true` 暂停、`false` 恢复。重复设置不产生额外操作;返回 0 表示接受,-1 表示参数、连接或传输类型不支持。视频暂停阻止新帧进入编码,废弃旧编码结果,并在发送队列上清除待发视频/重传/纠错包,停止探测和填充;恢复请求新关键帧。音频暂停阻止 PCM 编码并清理待发音频,恢复时仅发送新音频。已交给网络的包无法撤回。API 保留连接、采集源、码率与画质设置以及其他媒体/数据通道,也不改变其他控制端的发送状态。
|
||||||
|
|
||||||
桌面墙将 `fps=0` 和 `audio=false` 分别映射到上述 API;应用层仅保留低帧率节流,静音在 MiniRTC 内部执行。接收端仍保留静音门控及播放队列清理,以立即停止播放网络中已在途的音频。旧版 `audio_capture` 命令在支持新 API 的传输中也按连接静音,已有订阅的音频状态优先;不支持 API 的旧/Web 传输沿用采集开关回退。
|
桌面墙将 `fps=0` 和 `audio=false` 分别映射到上述 API;应用层仅保留低帧率节流,不再使用 `1 bps` 代替视频暂停或在采集回调中过滤静音音频。接收端仍保留静音门控及播放队列清理,以立即停止播放网络中已在途的音频。旧版 `audio_capture` 命令在支持新 API 的传输中也按连接静音,已有订阅的音频状态优先;不支持 API 的旧/Web 传输沿用采集开关回退。
|
||||||
|
|
||||||
## 批量操作
|
## 批量操作
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ struct PreviewSubscription {
|
|||||||
int width = 0;
|
int width = 0;
|
||||||
int height = 0;
|
int height = 0;
|
||||||
int version = 1;
|
int version = 1;
|
||||||
|
// Optional negotiated extension: total video budget for this peer, in bps.
|
||||||
|
// 0 restores normal congestion control; absent preserves legacy wire format.
|
||||||
|
std::optional<int> max_bitrate_bps;
|
||||||
|
|
||||||
static bool ValidResolution(int width, int height) {
|
static bool ValidResolution(int width, int height) {
|
||||||
return (width == 0 && height == 0) ||
|
return (width == 0 && height == 0) ||
|
||||||
@@ -45,7 +48,8 @@ struct PreviewSubscription {
|
|||||||
frame_rate == other.frame_rate &&
|
frame_rate == other.frame_rate &&
|
||||||
EncoderPreference() == other.EncoderPreference() &&
|
EncoderPreference() == other.EncoderPreference() &&
|
||||||
audio == other.audio && width == other.width &&
|
audio == other.audio && width == other.width &&
|
||||||
height == other.height && version == other.version;
|
height == other.height && version == other.version &&
|
||||||
|
max_bitrate_bps == other.max_bitrate_bps;
|
||||||
}
|
}
|
||||||
|
|
||||||
int EncoderPreference() const {
|
int EncoderPreference() const {
|
||||||
@@ -63,6 +67,7 @@ struct PreviewSubscription {
|
|||||||
json["width"] = width;
|
json["width"] = width;
|
||||||
json["height"] = height;
|
json["height"] = height;
|
||||||
}
|
}
|
||||||
|
if (max_bitrate_bps) json["max_bitrate_bps"] = *max_bitrate_bps;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
static std::optional<PreviewSubscription> Parse(const nlohmann::json &json) {
|
static std::optional<PreviewSubscription> Parse(const nlohmann::json &json) {
|
||||||
@@ -108,6 +113,12 @@ struct PreviewSubscription {
|
|||||||
result.height = json.at("height").get<int>();
|
result.height = json.at("height").get<int>();
|
||||||
if (!ValidResolution(result.width, result.height)) return {};
|
if (!ValidResolution(result.width, result.height)) return {};
|
||||||
}
|
}
|
||||||
|
if (json.contains("max_bitrate_bps")) {
|
||||||
|
if (!json.at("max_bitrate_bps").is_number_integer()) return {};
|
||||||
|
const auto bitrate = json.at("max_bitrate_bps").get<int64_t>();
|
||||||
|
if (bitrate < 0 || bitrate > 100'000'000) return {};
|
||||||
|
result.max_bitrate_bps = int(bitrate);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ struct HostInfo {
|
|||||||
bool supports_deskhub_preview;
|
bool supports_deskhub_preview;
|
||||||
bool supports_deskhub_resolution;
|
bool supports_deskhub_resolution;
|
||||||
bool supports_deskhub_fps15;
|
bool supports_deskhub_fps15;
|
||||||
|
bool supports_deskhub_bandwidth;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ServiceStatus {
|
struct ServiceStatus {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ void ResetHostInfo(HostInfo& info) {
|
|||||||
info.supports_deskhub_preview = false;
|
info.supports_deskhub_preview = false;
|
||||||
info.supports_deskhub_resolution = false;
|
info.supports_deskhub_resolution = false;
|
||||||
info.supports_deskhub_fps15 = false;
|
info.supports_deskhub_fps15 = false;
|
||||||
|
info.supports_deskhub_bandwidth = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AllocateHostDisplays(HostInfo& info, std::size_t count) {
|
bool AllocateHostDisplays(HostInfo& info, std::size_t count) {
|
||||||
@@ -225,7 +226,8 @@ std::string RemoteAction::ToJson(const RemoteAction& action) {
|
|||||||
{"supports_video_settings", action.i.supports_video_settings},
|
{"supports_video_settings", action.i.supports_video_settings},
|
||||||
{"supports_deskhub_preview", action.i.supports_deskhub_preview},
|
{"supports_deskhub_preview", action.i.supports_deskhub_preview},
|
||||||
{"supports_deskhub_resolution", action.i.supports_deskhub_resolution},
|
{"supports_deskhub_resolution", action.i.supports_deskhub_resolution},
|
||||||
{"supports_deskhub_fps15", action.i.supports_deskhub_fps15}};
|
{"supports_deskhub_fps15", action.i.supports_deskhub_fps15},
|
||||||
|
{"supports_deskhub_bandwidth", action.i.supports_deskhub_bandwidth}};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ControlType::invalid:
|
case ControlType::invalid:
|
||||||
@@ -424,6 +426,8 @@ bool RemoteAction::FromJson(const std::string& json_string,
|
|||||||
host_info_object.value("supports_deskhub_resolution", false);
|
host_info_object.value("supports_deskhub_resolution", false);
|
||||||
output.i.supports_deskhub_fps15 =
|
output.i.supports_deskhub_fps15 =
|
||||||
host_info_object.value("supports_deskhub_fps15", false);
|
host_info_object.value("supports_deskhub_fps15", false);
|
||||||
|
output.i.supports_deskhub_bandwidth =
|
||||||
|
host_info_object.value("supports_deskhub_bandwidth", false);
|
||||||
const std::string host_name =
|
const std::string host_name =
|
||||||
host_info_object.at("host_name").get<std::string>();
|
host_info_object.at("host_name").get<std::string>();
|
||||||
std::strncpy(output.i.host_name, host_name.c_str(),
|
std::strncpy(output.i.host_name, host_name.c_str(),
|
||||||
|
|||||||
Reference in New Issue
Block a user