Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ namespace mail {
MAIL(invalidate_ref_frames);
MAIL(gamepad_feedback);
MAIL(hdr);
MAIL(dynamic_bitrate); // Runtime encoder bitrate change (kbps), posted from the HTTP /bitrate handler
#undef MAIL

} // namespace mail
59 changes: 59 additions & 0 deletions src/nvenc/nvenc_base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "nvenc_api.h"

// standard includes
#include <algorithm>
#include <format>
#include <optional>

Expand Down Expand Up @@ -722,6 +723,12 @@ namespace nvenc {
}
}

// Preserve init params + config so set_bitrate() can reconfigure the live encoder later.
// encodeConfig must point at our own storage that outlives this function.
saved_init_params = init_params;
current_enc_config = enc_config;
saved_init_params.encodeConfig = &current_enc_config;

if (async_event_handle) {
NV_ENC_EVENT_PARAMS event_params = {api::event_params_version(selected_api_version)};
event_params.completionEvent = async_event_handle;
Expand Down Expand Up @@ -945,6 +952,58 @@ namespace nvenc {
return true;
}

bool nvenc_base::set_bitrate(int bitrate_kbps) {
if (!encoder || !nvenc) {
BOOST_LOG(warning) << "NvEnc: encoder not initialized; cannot change bitrate";
return false;
}
// Reject non-positive and absurd values. The 800000 kbps ceiling keeps bitrate_kbps * 1000
// well inside uint32 so the rate-control fields below cannot overflow.
if (bitrate_kbps <= 0 || bitrate_kbps > 800000) {
BOOST_LOG(error) << "NvEnc: out-of-range bitrate " << bitrate_kbps << " kbps";
return false;
}

const bool is_hevc = (saved_init_params.encodeGUID == NV_ENC_CODEC_HEVC_GUID);
const uint32_t new_bitrate_bps = static_cast<uint32_t>(bitrate_kbps) * 1000u;
const uint32_t prev_bitrate_bps = current_enc_config.rcParams.averageBitRate;

// Copy the saved config and apply the new bitrate.
NV_ENC_CONFIG enc_config = current_enc_config;
enc_config.rcParams.averageBitRate = new_bitrate_bps;
enc_config.rcParams.maxBitRate = new_bitrate_bps;

// Scale the VBV buffer with the bitrate so the rate controller keeps a comparable buffering
// window (mirrors the create_encoder VBV sizing), with a floor to avoid an undersized buffer.
if (enc_config.rcParams.vbvBufferSize > 0 && prev_bitrate_bps > 0) {
const uint64_t scaled = static_cast<uint64_t>(new_bitrate_bps) * current_enc_config.rcParams.vbvBufferSize / prev_bitrate_bps;
enc_config.rcParams.vbvBufferSize = static_cast<uint32_t>(std::max<uint64_t>(scaled, 100u * 1000u));
}

NV_ENC_RECONFIGURE_PARAMS reconfigure_params {api::reconfigure_params_version(selected_api_version)};
reconfigure_params.reInitEncodeParams = saved_init_params;
reconfigure_params.reInitEncodeParams.encodeConfig = &enc_config;

// When raising the ceiling, reset the rate controller and force an IDR so the higher bitrate
// takes effect immediately instead of draining the old VBV first.
if (new_bitrate_bps > prev_bitrate_bps) {
reconfigure_params.resetEncoder = 1;
reconfigure_params.forceIDR = 1;
}

if (nvenc_failed(nvenc->nvEncReconfigureEncoder(encoder, &reconfigure_params))) {
BOOST_LOG(error) << "NvEnc: nvEncReconfigureEncoder() for " << bitrate_kbps << " kbps failed: " << last_nvenc_error_string;
return false;
}

current_enc_config.rcParams.averageBitRate = new_bitrate_bps;
current_enc_config.rcParams.maxBitRate = new_bitrate_bps;
current_enc_config.rcParams.vbvBufferSize = enc_config.rcParams.vbvBufferSize;

BOOST_LOG(info) << "NvEnc: " << (is_hevc ? "HEVC" : "H.264/AV1") << " bitrate reconfigured to " << bitrate_kbps << " kbps";
return true;
}

bool nvenc_base::nvenc_failed(NVENCSTATUS status) {
last_nvenc_status = status;
auto status_string = [](NVENCSTATUS status) -> std::string {
Expand Down
12 changes: 12 additions & 0 deletions src/nvenc/nvenc_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ namespace nvenc {
*/
bool invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame);

/**
* @brief Reconfigure the live encoder with a new average/max bitrate (no rebuild).
* @param bitrate_kbps New bitrate in kbps.
* @return `true` if reconfigured in place; `false` if the caller must rebuild the encoder.
*/
bool set_bitrate(int bitrate_kbps);

protected:
/**
* @brief Required. Used for loading NvEnc library and setting `nvenc` variable with `NvEncodeAPICreateInstance()`.
Expand Down Expand Up @@ -146,6 +153,11 @@ namespace nvenc {
std::pair<uint64_t, uint64_t> last_rfi_range;
logging::min_max_avg_periodic_logger<double> frame_size_logger = {debug, "NvEnc: encoded frame sizes in kB", ""};
} encoder_state;

// Saved during create_encoder() so set_bitrate() can drive nvEncReconfigureEncoder().
// saved_init_params.encodeConfig points at current_enc_config.
NV_ENC_INITIALIZE_PARAMS saved_init_params {};
NV_ENC_CONFIG current_enc_config {};
};

} // namespace nvenc
81 changes: 81 additions & 0 deletions src/nvhttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3419,6 +3419,85 @@ namespace nvhttp {
return;
}

void setBitrate(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);

pt::ptree tree;
auto g = util::fail_guard([&]() {
std::ostringstream data;
pt::write_xml(data, tree);
response->write(data.str());
response->close_connection_after_response = true;
});

auto named_cert_p = get_verified_cert(request);
if (!has_client_perm(named_cert_p, PERM::_allow_view)) {
log_permission_denied("SetBitrate"sv, "View stream"sv, named_cert_p);
tree.put("root.bitrate", 0);
tree.put("root.<xmlattr>.status_code", 403);
tree.put("root.<xmlattr>.status_message", permission_denied_status_message(named_cert_p, "View stream"sv));
return;
}

auto args = request->parse_query_string();
const int requested = (int) util::from_view(get_arg(args, "bitrate", "0"));
if (requested <= 0) {
tree.put("root.bitrate", 0);
tree.put("root.<xmlattr>.status_code", 400);
tree.put("root.<xmlattr>.status_message", "Missing or invalid bitrate parameter");
return;
}

// Clamp to the host bitrate ceiling. max_bitrate == 0 means "unlimited" in config, so also
// enforce an absolute ceiling to keep the value sane and avoid overflow downstream
// (bitrate_kbps * 1000 must fit the encoder's 32-bit rate-control fields).
constexpr int absolute_max_bitrate_kbps = 500000; // 500 Mbps
int applied = requested;
if (config::video.max_bitrate > 0 && applied > config::video.max_bitrate) {
applied = config::video.max_bitrate;
}
if (applied > absolute_max_bitrate_kbps) {
applied = absolute_max_bitrate_kbps;
}
if (applied != requested) {
BOOST_LOG(info) << "Clamped requested bitrate "sv << requested << " kbps to "sv << applied << " kbps"sv;
}

const int updated = stream::set_bitrate_for_sessions(named_cert_p->uuid, applied);
if (updated <= 0) {
BOOST_LOG(warning) << "Bitrate change requested by ["sv << named_cert_p->name << "] but no matching active session was found"sv;
tree.put("root.bitrate", 0);
tree.put("root.<xmlattr>.status_code", 404);
tree.put("root.<xmlattr>.status_message", "No active session for this client");
return;
}

BOOST_LOG(info) << "Client ["sv << named_cert_p->name << "] set runtime bitrate to "sv << applied << " kbps ("sv << updated << " session(s))"sv;
tree.put("root.bitrate", applied);
tree.put("root.<xmlattr>.status_code", 200);
}

void getAbrCapabilities(resp_https_t response, req_https_t request) {
print_req<SunshineHTTPS>(request);

auto named_cert_p = get_verified_cert(request);
if (!has_client_perm(named_cert_p, PERM::_allow_view)) {
log_permission_denied("AbrCapabilities"sv, "View stream"sv, named_cert_p, true);
response->write(SimpleWeb::StatusCode::client_error_unauthorized);
response->close_connection_after_response = true;
return;
}

// Server-side adaptive bitrate decisioning is not implemented. Reporting it unsupported makes
// Foundation-compatible clients (e.g. Moonlight V+) drive their own local ABR controller, which
// applies decisions through the runtime /bitrate endpoint above.
const std::string body = R"({"supported":false,"version":1,"features":["runtime_bitrate"]})";
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "application/json");
response->write(SimpleWeb::StatusCode::success_ok, body, headers);
response->close_connection_after_response = true;
}

void setup(const std::string &pkey, const std::string &cert) {
conf_intern.pkey = pkey;
conf_intern.servercert = cert;
Expand Down Expand Up @@ -3558,6 +3637,8 @@ namespace nvhttp {
https_server.resource["^/cancel$"]["GET"] = cancel;
https_server.resource["^/actions/clipboard$"]["GET"] = getClipboard;
https_server.resource["^/actions/clipboard$"]["POST"] = setClipboard;
https_server.resource["^/bitrate$"]["GET"] = setBitrate;
https_server.resource["^/api/abr/capabilities$"]["GET"] = getAbrCapabilities;

https_server.config.reuse_address = true;
https_server.config.address = net::get_bind_address(address_family);
Expand Down
29 changes: 29 additions & 0 deletions src/stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ namespace stream {

safe::mail_raw_t::event_t<bool> idr_events;
safe::mail_raw_t::event_t<std::pair<int64_t, int64_t>> invalidate_ref_frames_events;
safe::mail_raw_t::event_t<int> bitrate_events;

std::unique_ptr<platf::deinit_t> qos;
} video;
Expand Down Expand Up @@ -682,6 +683,33 @@ namespace stream {
}
}

int set_bitrate_for_sessions(const std::string &client_uuid, int bitrate_kbps) {
if (bitrate_kbps <= 0) {
return 0;
}
auto ref = broadcast.ref();
if (!ref) {
return 0;
}
int updated = 0;
auto lg = ref->control_server._sessions.lock();
for (auto *session : *ref->control_server._sessions) {
if (!session || !session->video.bitrate_events) {
continue;
}
if (!client_uuid.empty() && session->device_uuid != client_uuid) {
continue;
}
// Keep the session metadata (runtime sessions API, history, stats) in sync with the
// value the encoder thread will adopt from the event below.
session->config.monitor.bitrate = bitrate_kbps;
session->config.monitor.client_requested_bitrate = bitrate_kbps;
session->video.bitrate_events->raise(bitrate_kbps);
++updated;
}
return updated;
}

static const char *state_name(session::state_e st) {
switch (st) {
case session::state_e::STOPPED: return "stopped";
Expand Down Expand Up @@ -2957,6 +2985,7 @@ namespace stream {

session->video.idr_events = mail->event<bool>(mail::idr);
session->video.invalidate_ref_frames_events = mail->event<std::pair<int64_t, int64_t>>(mail::invalidate_ref_frames);
session->video.bitrate_events = mail->event<int>(mail::dynamic_bitrate);
session->video.lowseq = 0;
session->video.ping_payload = launch_session.av_ping_payload;
if (config.encryptionFlagsEnabled & SS_ENC_VIDEO) {
Expand Down
8 changes: 8 additions & 0 deletions src/stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,12 @@ namespace stream {
std::vector<session_info_t> get_all_session_info();

void request_idr_for_all_sessions();

/**
* @brief Apply a new encoder bitrate to active streaming sessions at runtime.
* @param client_uuid Target client UUID; when empty, applies to every active session.
* @param bitrate_kbps New encoder bitrate in kbps (already clamped by the caller).
* @return Number of sessions updated.
*/
int set_bitrate_for_sessions(const std::string &client_uuid, int bitrate_kbps);
} // namespace stream
54 changes: 53 additions & 1 deletion src/video.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,13 @@ namespace video {
}
}

bool set_bitrate(int bitrate_kbps) override {
if (!device || !device->nvenc) {
return false;
}
return device->nvenc->set_bitrate(bitrate_kbps);
}

nvenc::nvenc_encoded_frame encode_frame(uint64_t frame_index) {
if (!device || !device->nvenc) {
return {};
Expand All @@ -787,6 +794,7 @@ namespace video {
safe::mail_raw_t::event_t<bool> idr_events;
safe::mail_raw_t::event_t<hdr_info_t> hdr_events;
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_events;
safe::mail_raw_t::event_t<int> bitrate_events;

config_t config;
int frame_nr;
Expand Down Expand Up @@ -2661,7 +2669,7 @@ namespace video {
int &frame_nr, // Store progress of the frame number
safe::mail_t mail,
img_event_t images,
config_t config,
config_t &config,
std::shared_ptr<platf::display_t> disp,
std::unique_ptr<platf::encode_device_t> encode_device,
safe::signal_t &reinit_event,
Expand Down Expand Up @@ -2758,6 +2766,7 @@ namespace video {
auto packets = mail::man->queue<packet_t>(mail::video_packets);
auto idr_events = mail->event<bool>(mail::idr);
auto invalidate_ref_frames_events = mail->event<std::pair<int64_t, int64_t>>(mail::invalidate_ref_frames);
auto bitrate_events = mail->event<int>(mail::dynamic_bitrate);

{
// Load a dummy image into the AVFrame to ensure we have something to encode
Expand Down Expand Up @@ -2830,6 +2839,27 @@ namespace video {
break;
}

// Apply any runtime bitrate change, coalescing rapid ABR updates to the latest value so a
// burst of requests causes at most one reconfigure/rebuild. NVENC reconfigures the live
// encoder seamlessly; encoders that cannot (avcodec-based) report failure and we rebuild this
// session by breaking out, so capture_async re-enters with config (held by reference) anew.
std::optional<int> latest_bitrate;
while (bitrate_events->peek()) {
if (auto new_bitrate = bitrate_events->pop(0ms)) {
latest_bitrate = *new_bitrate;
}
}
if (latest_bitrate) {
config.bitrate = *latest_bitrate;
config.client_requested_bitrate = *latest_bitrate;
if (session->set_bitrate(*latest_bitrate)) {
BOOST_LOG(info) << "Applied runtime bitrate "sv << *latest_bitrate << " kbps (live)"sv;
} else if (frame_nr > 1) {
BOOST_LOG(info) << "Rebuilding encoder to apply runtime bitrate "sv << *latest_bitrate << " kbps"sv;
break;
}
}

bool requested_idr_frame = false;

while (invalidate_ref_frames_events->peek()) {
Expand Down Expand Up @@ -3227,6 +3257,27 @@ namespace video {
pos->session->request_idr_frame();
ctx->idr_events->pop();
}
if (ctx->bitrate_events->peek()) {
// Coalesce rapid ABR updates to the latest requested value.
std::optional<int> latest_bitrate;
while (ctx->bitrate_events->peek()) {
if (auto new_bitrate = ctx->bitrate_events->pop(0ms)) {
latest_bitrate = *new_bitrate;
}
}
if (latest_bitrate) {
ctx->config.bitrate = *latest_bitrate;
ctx->config.client_requested_bitrate = *latest_bitrate;
if (pos->session->set_bitrate(*latest_bitrate)) {
BOOST_LOG(info) << "Applied runtime bitrate "sv << *latest_bitrate << " kbps (live, sync)"sv;
} else {
// avcodec encoder: rebuild synced sessions from their (now-updated) ctx config.
BOOST_LOG(info) << "Rebuilding encoder to apply runtime bitrate "sv << *latest_bitrate << " kbps (sync)"sv;
ec = platf::capture_e::reinit;
return false;
}
}
}

std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
std::optional<std::chrono::steady_clock::time_point> capture_timestamp;
Expand Down Expand Up @@ -3455,6 +3506,7 @@ namespace video {
std::move(idr_events),
mail->event<hdr_info_t>(mail::hdr),
mail->event<input::touch_port_t>(mail::touch_port),
mail->event<int>(mail::dynamic_bitrate),
config,
1,
channel_data,
Expand Down
10 changes: 10 additions & 0 deletions src/video.h
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,16 @@ namespace video {
virtual void request_normal_frame() = 0;

virtual void invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0;

/**
* @brief Apply a new encoder bitrate to the live encoder without rebuilding it.
* @param bitrate_kbps New bitrate in kbps.
* @return `true` if the encoder reconfigured itself in place; `false` if the caller must
* rebuild the encode session to apply the change.
*/
virtual bool set_bitrate(int bitrate_kbps) {
return false;
}
};

// encoders
Expand Down