diff --git a/src/globals.h b/src/globals.h index 1617b7c6f..5940b447a 100644 --- a/src/globals.h +++ b/src/globals.h @@ -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 diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp index 9749e4fff..599590019 100644 --- a/src/nvenc/nvenc_base.cpp +++ b/src/nvenc/nvenc_base.cpp @@ -7,6 +7,7 @@ #include "nvenc_api.h" // standard includes +#include #include #include @@ -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 = ¤t_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; @@ -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(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(new_bitrate_bps) * current_enc_config.rcParams.vbvBufferSize / prev_bitrate_bps; + enc_config.rcParams.vbvBufferSize = static_cast(std::max(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 { diff --git a/src/nvenc/nvenc_base.h b/src/nvenc/nvenc_base.h index eeaa63c16..c2b709b16 100644 --- a/src/nvenc/nvenc_base.h +++ b/src/nvenc/nvenc_base.h @@ -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()`. @@ -146,6 +153,11 @@ namespace nvenc { std::pair last_rfi_range; logging::min_max_avg_periodic_logger 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 diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 8fe1bec03..ba6fb94dd 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -3419,6 +3419,85 @@ namespace nvhttp { return; } + void setBitrate(resp_https_t response, req_https_t request) { + print_req(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..status_code", 403); + tree.put("root..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..status_code", 400); + tree.put("root..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..status_code", 404); + tree.put("root..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..status_code", 200); + } + + void getAbrCapabilities(resp_https_t response, req_https_t request) { + print_req(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; @@ -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); diff --git a/src/stream.cpp b/src/stream.cpp index 5697f7fa4..c794fe5e3 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -500,6 +500,7 @@ namespace stream { safe::mail_raw_t::event_t idr_events; safe::mail_raw_t::event_t> invalidate_ref_frames_events; + safe::mail_raw_t::event_t bitrate_events; std::unique_ptr qos; } video; @@ -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"; @@ -2957,6 +2985,7 @@ namespace stream { session->video.idr_events = mail->event(mail::idr); session->video.invalidate_ref_frames_events = mail->event>(mail::invalidate_ref_frames); + session->video.bitrate_events = mail->event(mail::dynamic_bitrate); session->video.lowseq = 0; session->video.ping_payload = launch_session.av_ping_payload; if (config.encryptionFlagsEnabled & SS_ENC_VIDEO) { diff --git a/src/stream.h b/src/stream.h index a9cb633af..a45b69e96 100644 --- a/src/stream.h +++ b/src/stream.h @@ -160,4 +160,12 @@ namespace stream { std::vector 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 diff --git a/src/video.cpp b/src/video.cpp index 2ac60ff5b..ee720a7c9 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -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 {}; @@ -787,6 +794,7 @@ namespace video { safe::mail_raw_t::event_t idr_events; safe::mail_raw_t::event_t hdr_events; safe::mail_raw_t::event_t touch_port_events; + safe::mail_raw_t::event_t bitrate_events; config_t config; int frame_nr; @@ -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 disp, std::unique_ptr encode_device, safe::signal_t &reinit_event, @@ -2758,6 +2766,7 @@ namespace video { auto packets = mail::man->queue(mail::video_packets); auto idr_events = mail->event(mail::idr); auto invalidate_ref_frames_events = mail->event>(mail::invalidate_ref_frames); + auto bitrate_events = mail->event(mail::dynamic_bitrate); { // Load a dummy image into the AVFrame to ensure we have something to encode @@ -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 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()) { @@ -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 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 frame_timestamp; std::optional capture_timestamp; @@ -3455,6 +3506,7 @@ namespace video { std::move(idr_events), mail->event(mail::hdr), mail->event(mail::touch_port), + mail->event(mail::dynamic_bitrate), config, 1, channel_data, diff --git a/src/video.h b/src/video.h index f784d1e84..357d09adb 100644 --- a/src/video.h +++ b/src/video.h @@ -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