diff --git a/js/module.d.ts b/js/module.d.ts index 4a74523a5..d3c5847a1 100644 --- a/js/module.d.ts +++ b/js/module.d.ts @@ -663,6 +663,7 @@ export interface IVideo { destroy(): void; readonly skippedFrames: number; readonly encodedFrames: number; + readonly canvasId: number; } export interface IVideoFactory { create(): IVideo; @@ -988,6 +989,146 @@ export interface IAudioTrackFactory { importLegacySettings(): void; saveLegacySettings(): void; } +export interface IAutoConfigCapabilities { + apiVersion: 2; + resultSchemaVersion: 1; + previewApplySplit: true; + awaitableCancel: true; + perUploadLegResults: true; + desktopOwnedApply: true; + multipleActiveProbes: true; + bandwidthModes: ['twitch-standard-active', 'youtube-unbound-active', 'estimate']; +} +export type AutoConfigTopology = 'direct-single' | 'cloud-multistream' | 'custom-rtmp' | 'dual-output' | 'enhanced-broadcasting' | 'stream-shift' | 'mixed'; +export type AutoConfigDisplay = 'horizontal' | 'vertical' | 'both'; +export type AutoConfigPlatform = 'twitch' | 'youtube' | 'facebook' | 'kick' | 'tiktok' | 'custom' | 'other'; +export type AutoConfigEstimateReason = 'non_twitch' | 'custom_rtmp' | 'cloud_multistream' | 'dual_output' | 'enhanced_broadcasting' | 'stream_shift' | 'mixed_topology' | 'probe_disabled'; +export interface IAutoConfigDestination { + platform: AutoConfigPlatform; +} +export interface IAutoConfigCurrentSettings { + width: number; + height: number; + fpsNum: number; + fpsDen: number; + bitrateKbps: number; + encoderId: string; + codec: string; + preset?: string; +} +export interface IAutoConfigLimits { + maxBitrateKbps?: number; + maxWidth?: number; + maxHeight?: number; + maxFpsNum?: number; + maxFpsDen?: number; +} +export interface IAutoConfigLegRequest { + legId: string; + display: AutoConfigDisplay; + destinations: IAutoConfigDestination[]; + current: IAutoConfigCurrentSettings; + limits?: IAutoConfigLimits; + estimateReason?: AutoConfigEstimateReason; +} +export interface IAutoConfigTwitchActiveProbe { + probeId: string; + kind: 'twitch-standard'; + legId: string; + serviceName: 'Twitch'; + server: string; + streamKey: string; +} +export interface IAutoConfigYoutubeActiveProbe { + probeId: string; + kind: 'youtube-unbound'; + legId: string; + serviceName: 'YouTube - RTMPS'; + server: string; + streamKey: string; +} +export type IAutoConfigActiveProbe = IAutoConfigTwitchActiveProbe | IAutoConfigYoutubeActiveProbe; +export interface IAutoConfigRequest { + schemaVersion: 1; + topology: AutoConfigTopology; + legs: IAutoConfigLegRequest[]; + activeProbes?: IAutoConfigActiveProbe[]; +} +export type AutoConfigEventType = 'phase' | 'progress' | 'result' | 'error' | 'cancelled' | 'complete'; +export type AutoConfigPhase = 'preflight' | 'hardware' | 'bandwidth' | 'recommendation' | 'cleanup'; +export type AutoConfigMeasurementMode = 'active' | 'estimated'; +export interface IAutoConfigEvent { + schemaVersion: 1; + sessionId: string; + sequence: number; + type: AutoConfigEventType; + phase: AutoConfigPhase; + progress: number; + code?: string; + legId?: string; + measurementMode?: AutoConfigMeasurementMode; + probeId?: string; + provider?: 'twitch' | 'youtube'; + targetBitrateKbps?: number; +} +export interface IAutoConfigProbeMeasurement { + provider: 'twitch' | 'youtube'; + method: 'twitch-bandwidth-test' | 'youtube-unbound-ramp'; + success: boolean; + measuredKbps?: number; + safeKbps?: number; + headroomPercent?: number; + ceilingReached: boolean; +} +export interface IAutoConfigMeasurement { + mode: AutoConfigMeasurementMode; + confidence: 'high' | 'medium' | 'low'; + reason?: string; + probes?: IAutoConfigProbeMeasurement[]; +} +export interface IAutoConfigRecommendation { + width: number; + height: number; + fpsNum: number; + fpsDen: number; + bitrateKbps: number; + encoderId: string; + codec: string; + preset?: string; +} +export interface IAutoConfigResultDestination { + platform: string; +} +export interface IAutoConfigLegResult { + legId: string; + display: AutoConfigDisplay; + destinations: IAutoConfigResultDestination[]; + measurement: IAutoConfigMeasurement; + recommendation: IAutoConfigRecommendation; + limits?: IAutoConfigLimits; +} +export interface IAutoConfigError { + code: string; +} +export interface IAutoConfigResult { + schemaVersion: 1; + sessionId: string; + status: 'complete' | 'partial' | 'cancelled' | 'failed'; + error?: IAutoConfigError; + legs: IAutoConfigLegResult[]; +} +export interface IAutoConfigNativeApi { + GetAutoConfigCapabilities(): string; + CreateAutoConfigSession(requestJson: string, callback: (event: IAutoConfigEvent) => void): string; + StartAutoConfigSession(sessionId: string): void; + ConfirmAutoConfigProbeIngest(sessionId: string, probeId: string, received: boolean): void; + GetAutoConfigResult(sessionId: string): string; + CancelAutoConfigSession(sessionId: string): void; + CloseAutoConfigSession(sessionId: string): void; +} +export interface INodeObsApi extends IAutoConfigNativeApi { + [key: string]: any; +} export declare const enum VCamOutputType { Invalid = 0, SceneOutput = 1, @@ -995,4 +1136,4 @@ export declare const enum VCamOutputType { ProgramView = 3, PreviewOutput = 4 } -export declare const NodeObs: any; +export declare const NodeObs: INodeObsApi; diff --git a/js/module.ts b/js/module.ts index 6e39424ca..d93267b4a 100644 --- a/js/module.ts +++ b/js/module.ts @@ -1470,6 +1470,11 @@ export interface IVideo { * Number of total encoded frames */ readonly encodedFrames: number; + + /** + * Server-side canvas id. Pass to APIs that reference video contexts by id. + */ + readonly canvasId: number; } export interface IVideoFactory { @@ -1956,6 +1961,210 @@ export interface IAudioTrackFactory { saveLegacySettings(): void; } +// ---- Auto Optimizer API ---- + +export interface IAutoConfigCapabilities { + apiVersion: 2; + resultSchemaVersion: 1; + previewApplySplit: true; + awaitableCancel: true; + perUploadLegResults: true; + desktopOwnedApply: true; + multipleActiveProbes: true; + bandwidthModes: ['twitch-standard-active', 'youtube-unbound-active', 'estimate']; +} + +export type AutoConfigTopology = + 'direct-single' | + 'cloud-multistream' | + 'custom-rtmp' | + 'dual-output' | + 'enhanced-broadcasting' | + 'stream-shift' | + 'mixed'; + +export type AutoConfigDisplay = 'horizontal' | 'vertical' | 'both'; + +export type AutoConfigPlatform = + 'twitch' | + 'youtube' | + 'facebook' | + 'kick' | + 'tiktok' | + 'custom' | + 'other'; + +export type AutoConfigEstimateReason = + 'non_twitch' | + 'custom_rtmp' | + 'cloud_multistream' | + 'dual_output' | + 'enhanced_broadcasting' | + 'stream_shift' | + 'mixed_topology' | + 'probe_disabled'; + +export interface IAutoConfigDestination { + platform: AutoConfigPlatform; +} + +export interface IAutoConfigCurrentSettings { + width: number; + height: number; + fpsNum: number; + fpsDen: number; + bitrateKbps: number; + encoderId: string; + codec: string; + preset?: string; +} + +export interface IAutoConfigLimits { + maxBitrateKbps?: number; + maxWidth?: number; + maxHeight?: number; + maxFpsNum?: number; + maxFpsDen?: number; +} + +export interface IAutoConfigLegRequest { + legId: string; + display: AutoConfigDisplay; + destinations: IAutoConfigDestination[]; + current: IAutoConfigCurrentSettings; + limits?: IAutoConfigLimits; + estimateReason?: AutoConfigEstimateReason; +} + +export interface IAutoConfigTwitchActiveProbe { + probeId: string; + kind: 'twitch-standard'; + legId: string; + serviceName: 'Twitch'; + server: string; + streamKey: string; +} + +export interface IAutoConfigYoutubeActiveProbe { + /** + * Security contract: Desktop's trusted worker must create an exact-marked, + * reusable-but-unbound liveStream and status-confirm that same resource. + * Native validates the official RTMPS endpoint but cannot query YouTube + * resource ownership or binding. Desktop must close the native session + * before deleting the liveStream through the YouTube API. + */ + probeId: string; + kind: 'youtube-unbound'; + legId: string; + serviceName: 'YouTube - RTMPS'; + server: string; + streamKey: string; +} + +export type IAutoConfigActiveProbe = IAutoConfigTwitchActiveProbe | IAutoConfigYoutubeActiveProbe; + +export interface IAutoConfigRequest { + schemaVersion: 1; + topology: AutoConfigTopology; + legs: IAutoConfigLegRequest[]; + activeProbes?: IAutoConfigActiveProbe[]; +} + +export type AutoConfigEventType = 'phase' | 'progress' | 'result' | 'error' | 'cancelled' | 'complete'; +export type AutoConfigPhase = 'preflight' | 'hardware' | 'bandwidth' | 'recommendation' | 'cleanup'; +export type AutoConfigMeasurementMode = 'active' | 'estimated'; + +export interface IAutoConfigEvent { + schemaVersion: 1; + sessionId: string; + sequence: number; + type: AutoConfigEventType; + phase: AutoConfigPhase; + progress: number; + code?: string; + legId?: string; + measurementMode?: AutoConfigMeasurementMode; + probeId?: string; + provider?: 'twitch' | 'youtube'; + /** Applied video bitrate for the active probe substep; audio is additional. */ + targetBitrateKbps?: number; +} + +export interface IAutoConfigProbeMeasurement { + provider: 'twitch' | 'youtube'; + method: 'twitch-bandwidth-test' | 'youtube-unbound-ramp'; + success: boolean; + /** Observed aggregate RTMP throughput, including audio. */ + measuredKbps?: number; + /** Safe video bitrate after headroom and the probe audio reserve. */ + safeKbps?: number; + headroomPercent?: number; + ceilingReached: boolean; +} + +export interface IAutoConfigMeasurement { + mode: AutoConfigMeasurementMode; + confidence: 'high' | 'medium' | 'low'; + reason?: string; + probes?: IAutoConfigProbeMeasurement[]; +} + +export interface IAutoConfigRecommendation { + width: number; + height: number; + fpsNum: number; + fpsDen: number; + bitrateKbps: number; + encoderId: string; + codec: string; + preset?: string; +} + +export interface IAutoConfigResultDestination { + platform: string; +} + +export interface IAutoConfigLegResult { + legId: string; + display: AutoConfigDisplay; + destinations: IAutoConfigResultDestination[]; + measurement: IAutoConfigMeasurement; + recommendation: IAutoConfigRecommendation; + limits?: IAutoConfigLimits; +} + +export interface IAutoConfigError { + code: string; +} + +export interface IAutoConfigResult { + schemaVersion: 1; + sessionId: string; + status: 'complete' | 'partial' | 'cancelled' | 'failed'; + error?: IAutoConfigError; + legs: IAutoConfigLegResult[]; +} + +/** Raw native methods. Requests and results cross IPC as JSON strings. */ +export interface IAutoConfigNativeApi { + GetAutoConfigCapabilities(): string; + CreateAutoConfigSession(requestJson: string, callback: (event: IAutoConfigEvent) => void): string; + StartAutoConfigSession(sessionId: string): void; + ConfirmAutoConfigProbeIngest(sessionId: string, probeId: string, received: boolean): void; + GetAutoConfigResult(sessionId: string): string; + CancelAutoConfigSession(sessionId: string): void; + CloseAutoConfigSession(sessionId: string): void; +} + +/** + * Most of the addon's non-optimizer surface remains dynamically typed. This + * intersection gives the Auto Optimizer methods useful declarations without + * changing unrelated callers. + */ +export interface INodeObsApi extends IAutoConfigNativeApi { + [key: string]: any; +} + export const enum VCamOutputType { Invalid, SceneOutput, @@ -1977,4 +2186,4 @@ else if (fs.existsSync(path.resolve(__dirname, `obs64.exe`).replace('app.asar', else { obs.IPC.setServerPath(path.resolve(__dirname, `obs32.exe`).replace('app.asar', 'app.asar.unpacked'), path.resolve(__dirname).replace('app.asar', 'app.asar.unpacked')); } -export const NodeObs = obs; +export const NodeObs: INodeObsApi = obs; diff --git a/obs-studio-client/CMakeLists.txt b/obs-studio-client/CMakeLists.txt index 9c8c9b838..d6854f963 100644 --- a/obs-studio-client/CMakeLists.txt +++ b/obs-studio-client/CMakeLists.txt @@ -179,6 +179,7 @@ if(BUILD_TESTING) "tests/polling-pacer-test.cpp" "tests/best-effort-gate-test.cpp" "tests/polling-pacer-best-effort-gate-test.cpp" + "tests/autoconfig-probe-policy-test.cpp" ${OSN_CLIENT_CORE_SOURCES} ) @@ -186,6 +187,7 @@ if(BUILD_TESTING) obs_studio_client_unit_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/source" + "${CMAKE_SOURCE_DIR}/source" ) target_link_libraries( diff --git a/obs-studio-client/source/controller.cpp b/obs-studio-client/source/controller.cpp index 651554b72..fa26b6dc3 100644 --- a/obs-studio-client/source/controller.cpp +++ b/obs-studio-client/source/controller.cpp @@ -17,6 +17,7 @@ ******************************************************************************/ #include "controller.hpp" +#include "nodeobs_autoconfig.hpp" #include #include #include @@ -380,6 +381,11 @@ std::shared_ptr Controller::connect(const std::string &uri) void Controller::disconnect() { + // AutoConfig owns a polling thread and a Node ThreadSafeFunction. Tear them + // down before dropping IPC so an abandoned session cannot keep the renderer + // environment or addon alive. + autoConfig::Shutdown(); + if (m_isServer) { m_connection->call_synchronous_helper("System", "Shutdown", {}); m_isServer = false; diff --git a/obs-studio-client/source/nodeobs_autoconfig.cpp b/obs-studio-client/source/nodeobs_autoconfig.cpp index 546beba19..0fb1098b4 100644 --- a/obs-studio-client/source/nodeobs_autoconfig.cpp +++ b/obs-studio-client/source/nodeobs_autoconfig.cpp @@ -19,278 +19,432 @@ #include "nodeobs_autoconfig.hpp" #include "polling-pacer.hpp" #include "shared.hpp" +#include "utility-v8.hpp" + +#include +#include +#include +#include +#include + +namespace { +struct AutoConfigEvent { + uint32_t schemaVersion = 0; + std::string sessionId; + uint64_t sequence = 0; + std::string type; + std::string phase; + double progress = 0; + std::string code; + std::string legId; + std::string measurementMode; + std::string probeId; + std::string provider; + uint32_t targetBitrateKbps = 0; +}; + +std::atomic workerStop{true}; +constexpr std::chrono::milliseconds sleepInterval(33); +Napi::ThreadSafeFunction jsThread; +bool jsThreadActive = false; +std::thread *workerThread = nullptr; +std::mutex sessionMutex; +std::mutex lifecycleMutex; +std::string activeSessionId; + +enum class CallbackShutdownMode { Release, Abort }; + +std::string GetActiveSessionId() +{ + std::lock_guard lock(sessionMutex); + return activeSessionId; +} -bool autoConfig::isWorkerRunning = false; -bool autoConfig::worker_stop = true; -std::chrono::milliseconds autoConfig::sleepInterval(33); -Napi::ThreadSafeFunction autoConfig::js_thread; -std::thread *autoConfig::worker_thread = nullptr; -std::vector autoConfig::ac_queue_task_workers; - -#ifdef WIN32 -const char *ac_sem_name = nullptr; // Not used on Windows -HANDLE ac_sem; -#else -const char *ac_sem_name = "autoconfig-semaphore"; -sem_t *ac_sem; -#endif - -void autoConfig::worker() +void SetActiveSessionId(const std::string &sessionId) { - osn::PollingPacer pacer(sleepInterval); - while (!worker_stop) { - auto tp_start = std::chrono::high_resolution_clock::now(); + std::lock_guard lock(sessionMutex); + activeSessionId = sessionId; +} + +uint64_t ReadUnsigned(const ipc::value &value) +{ + switch (value.type) { + case ipc::type::UInt32: + return value.value_union.ui32; + case ipc::type::UInt64: + return value.value_union.ui64; + case ipc::type::Int32: + return value.value_union.i32 < 0 ? 0 : static_cast(value.value_union.i32); + case ipc::type::Int64: + return value.value_union.i64 < 0 ? 0 : static_cast(value.value_union.i64); + default: + return 0; + } +} + +double ReadDouble(const ipc::value &value) +{ + switch (value.type) { + case ipc::type::Float: + return value.value_union.fp32; + case ipc::type::Double: + return value.value_union.fp64; + case ipc::type::Int32: + return value.value_union.i32; + case ipc::type::Int64: + return static_cast(value.value_union.i64); + case ipc::type::UInt32: + return value.value_union.ui32; + case ipc::type::UInt64: + return static_cast(value.value_union.ui64); + default: + return 0; + } +} + +bool GetSessionArgument(const Napi::CallbackInfo &info, const char *method, std::string &sessionId) +{ + if (info.Length() < 1 || !info[0].IsString()) { + Napi::TypeError::New(info.Env(), std::string(method) + " expects (sessionId: string)").ThrowAsJavaScriptException(); + return false; + } - auto conn = Controller::GetInstance().GetConnection(); - if (!conn) { - goto do_sleep; + sessionId = info[0].As().Utf8Value(); + if (sessionId.empty()) { + Napi::TypeError::New(info.Env(), std::string(method) + " expects a non-empty sessionId").ThrowAsJavaScriptException(); + return false; + } + + return true; +} + +void DispatchEvent(AutoConfigEvent *event) +{ + auto callback = [](Napi::Env env, Napi::Function jsCallback, AutoConfigEvent *eventData) { + try { + Napi::Object result = Napi::Object::New(env); + result.Set("schemaVersion", Napi::Number::New(env, eventData->schemaVersion)); + result.Set("sessionId", Napi::String::New(env, eventData->sessionId)); + result.Set("sequence", Napi::Number::New(env, static_cast(eventData->sequence))); + result.Set("type", Napi::String::New(env, eventData->type)); + result.Set("phase", Napi::String::New(env, eventData->phase)); + result.Set("progress", Napi::Number::New(env, eventData->progress)); + + if (!eventData->code.empty()) + result.Set("code", Napi::String::New(env, eventData->code)); + if (!eventData->legId.empty()) + result.Set("legId", Napi::String::New(env, eventData->legId)); + if (!eventData->measurementMode.empty()) + result.Set("measurementMode", Napi::String::New(env, eventData->measurementMode)); + if (!eventData->probeId.empty()) + result.Set("probeId", Napi::String::New(env, eventData->probeId)); + if (!eventData->provider.empty()) + result.Set("provider", Napi::String::New(env, eventData->provider)); + if (eventData->targetBitrateKbps > 0) + result.Set("targetBitrateKbps", Napi::Number::New(env, eventData->targetBitrateKbps)); + + jsCallback.Call({result}); + } catch (...) { } + delete eventData; + }; - { - std::vector response = conn->call_synchronous_helper("AutoConfig", "Query", {}); - if (!response.size() || (response.size() == 1)) { - goto do_sleep; - } + if (jsThread.NonBlockingCall(event, callback) != napi_ok) + delete event; +} - ErrorCode error = (ErrorCode)response[0].value_union.ui64; - if (error == ErrorCode::Ok) { - AutoConfigInfo *data = new AutoConfigInfo; +void Worker() +{ + osn::PollingPacer pacer(sleepInterval); - data->event = response[1].value_str; - data->description = response[2].value_str; - data->percentage = response[3].value_union.fp64; - ac_queue_task_workers.push_back(new std::thread(&autoConfig::queueTask, data)); + while (!workerStop.load()) { + const auto cycleStart = std::chrono::high_resolution_clock::now(); + try { + auto conn = Controller::GetInstance().GetConnection(); + const std::string sessionId = GetActiveSessionId(); + + if (conn && !sessionId.empty()) { + std::vector response = + conn->call_synchronous_helper("AutoConfig", "QueryAutoConfigSession", {ipc::value(sessionId)}); + if (response.size() >= 12 && static_cast(response[0].value_union.ui64) == ErrorCode::Ok) { + auto *event = new AutoConfigEvent; + event->schemaVersion = static_cast(ReadUnsigned(response[1])); + event->sessionId = response[2].value_str; + event->sequence = ReadUnsigned(response[3]); + event->type = response[4].value_str; + event->phase = response[5].value_str; + event->progress = ReadDouble(response[6]); + event->code = response[7].value_str; + event->legId = response[8].value_str; + event->measurementMode = response[9].value_str; + event->probeId = response[10].value_str; + event->provider = response[11].value_str; + if (response.size() >= 13) + event->targetBitrateKbps = static_cast(ReadUnsigned(response[12])); + + if (event->sessionId == sessionId) + DispatchEvent(event); + else + delete event; + } } + } catch (...) { + // A peer disappearing must never escape the native polling thread. + // Explicit IPC disconnect and environment teardown set workerStop and + // perform the corresponding join/ThreadSafeFunction abort. } - do_sleep: - auto tp_end = std::chrono::high_resolution_clock::now(); - auto dur = std::chrono::duration_cast(tp_end - tp_start); - const bool shouldSleep = pacer.finishCycle(dur); - if (shouldSleep) + const auto cycleEnd = std::chrono::high_resolution_clock::now(); + const auto duration = std::chrono::duration_cast(cycleEnd - cycleStart); + if (pacer.finishCycle(duration)) std::this_thread::sleep_for(pacer.sleepDuration()); } - return; } -void autoConfig::start_worker() +bool IsWorkerRunning() +{ + return !workerStop.load(); +} + +void StartWorker() { - if (!worker_stop) + if (IsWorkerRunning()) return; - worker_stop = false; - ac_sem = create_semaphore(ac_sem_name); - worker_thread = new std::thread(&autoConfig::worker); + workerStop.store(false); + workerThread = new std::thread(Worker); } -void autoConfig::stop_worker() +void StopWorker(CallbackShutdownMode mode) { - if (worker_stop != false) + workerStop.store(true); + if (workerThread && workerThread->joinable()) + workerThread->join(); + delete workerThread; + workerThread = nullptr; + if (jsThreadActive) { + if (mode == CallbackShutdownMode::Abort) + jsThread.Abort(); + else + jsThread.Release(); + jsThreadActive = false; + jsThread = Napi::ThreadSafeFunction(); + } +} + +void StopLocalSession(const std::string &sessionId, CallbackShutdownMode mode) +{ + std::lock_guard lock(lifecycleMutex); + if (GetActiveSessionId() != sessionId) return; + StopWorker(mode); + SetActiveSessionId(""); +} - worker_stop = true; - if (worker_thread->joinable()) { - worker_thread->join(); - } - for (auto queue_worker : ac_queue_task_workers) { - if (queue_worker->joinable()) { - queue_worker->join(); - } +void BestEffortServerCall(const std::shared_ptr &conn, const char *method, const std::string &sessionId) +{ + if (!conn || sessionId.empty()) + return; + try { + conn->call_synchronous_helper("AutoConfig", method, {ipc::value(sessionId)}); + } catch (...) { + // Disconnect and environment teardown must always continue locally. } - remove_semaphore(ac_sem, ac_sem_name); - js_thread.Release(); } -Napi::Value autoConfig::InitializeAutoConfig(const Napi::CallbackInfo &info) +Napi::Value GetAutoConfigCapabilities(const Napi::CallbackInfo &info) { - Napi::Function async_callback = info[0].As(); - Napi::Object serverInfo = info[1].ToObject(); - std::string continent = serverInfo.Get("continent").ToString().Utf8Value(); - std::string service = serverInfo.Get("service_name").ToString().Utf8Value(); - auto conn = GetConnection(info); if (!conn) return info.Env().Undefined(); - std::vector response = conn->call_synchronous_helper("AutoConfig", "InitializeAutoConfig", {continent, service}); - - if (!ValidateResponse(info, response)) + std::vector response = conn->call_synchronous_helper("AutoConfig", "GetAutoConfigCapabilities", {}); + if (!ValidateResponse(info, response) || response.size() < 2) return info.Env().Undefined(); - js_thread = Napi::ThreadSafeFunction::New(info.Env(), async_callback, "AutoConfig", 0, 1, [](Napi::Env) {}); - - start_worker(); - isWorkerRunning = true; - - return Napi::Boolean::New(info.Env(), true); + return Napi::String::New(info.Env(), response[1].value_str); } -Napi::Value autoConfig::StartBandwidthTest(const Napi::CallbackInfo &info) +Napi::Value CreateAutoConfigSession(const Napi::CallbackInfo &info) { - auto conn = GetConnection(info); - if (!conn) + if (info.Length() < 2 || !info[0].IsString() || !info[1].IsFunction()) { + Napi::TypeError::New(info.Env(), "CreateAutoConfigSession expects (requestJson: string, callback)").ThrowAsJavaScriptException(); return info.Env().Undefined(); + } - std::vector response = conn->call_synchronous_helper("AutoConfig", "StartBandwidthTest", {}); - if (!ValidateResponse(info, response)) + if (IsWorkerRunning()) { + Napi::Error::New(info.Env(), "An AutoConfig session is already active").ThrowAsJavaScriptException(); return info.Env().Undefined(); + } - return info.Env().Undefined(); -} + const std::string requestJson = info[0].As().Utf8Value(); + if (requestJson.empty()) { + Napi::TypeError::New(info.Env(), "CreateAutoConfigSession expects non-empty request JSON").ThrowAsJavaScriptException(); + return info.Env().Undefined(); + } -Napi::Value autoConfig::StartStreamEncoderTest(const Napi::CallbackInfo &info) -{ auto conn = GetConnection(info); if (!conn) return info.Env().Undefined(); - std::vector response = conn->call_synchronous_helper("AutoConfig", "StartStreamEncoderTest", {}); - if (!ValidateResponse(info, response)) + std::vector response = conn->call_synchronous_helper("AutoConfig", "CreateAutoConfigSession", {ipc::value(requestJson)}); + if (!ValidateResponse(info, response) || response.size() < 2) return info.Env().Undefined(); - return info.Env().Undefined(); + const std::string sessionId = response[1].value_str; + if (sessionId.empty()) { + Napi::Error::New(info.Env(), "CreateAutoConfigSession returned an empty sessionId").ThrowAsJavaScriptException(); + return info.Env().Undefined(); + } + + jsThread = Napi::ThreadSafeFunction::New(info.Env(), info[1].As(), "AutoConfigSession", 0, 1, [](Napi::Env) {}); + // The poller must not keep a renderer's Node environment alive. Its cleanup + // hook below owns the final abort/join if JavaScript never closes the session. + jsThread.Unref(info.Env()); + jsThreadActive = true; + SetActiveSessionId(sessionId); + StartWorker(); + + return Napi::String::New(info.Env(), sessionId); } -Napi::Value autoConfig::StartRecordingEncoderTest(const Napi::CallbackInfo &info) +Napi::Value StartAutoConfigSession(const Napi::CallbackInfo &info) { + std::string sessionId; + if (!GetSessionArgument(info, "StartAutoConfigSession", sessionId)) + return info.Env().Undefined(); + auto conn = GetConnection(info); if (!conn) return info.Env().Undefined(); - std::vector response = conn->call_synchronous_helper("AutoConfig", "StartRecordingEncoderTest", {}); + std::vector response = conn->call_synchronous_helper("AutoConfig", "StartAutoConfigSession", {ipc::value(sessionId)}); if (!ValidateResponse(info, response)) return info.Env().Undefined(); return info.Env().Undefined(); } -void autoConfig::queueTask(AutoConfigInfo *data) +Napi::Value ConfirmAutoConfigProbeIngest(const Napi::CallbackInfo &info) { - wait_semaphore(ac_sem); - - auto sources_callback = [](Napi::Env env, Napi::Function jsCallback, AutoConfigInfo *event_data) { - try { - Napi::Object result = Napi::Object::New(env); - - result.Set(Napi::String::New(env, "event"), Napi::String::New(env, event_data->event)); - result.Set(Napi::String::New(env, "description"), Napi::String::New(env, event_data->description)); - - if (event_data->event.compare("error") != 0) { - result.Set(Napi::String::New(env, "percentage"), Napi::Number::New(env, event_data->percentage)); - } - result.Set(Napi::String::New(env, "continent"), Napi::String::New(env, "")); - - jsCallback.Call({result}); - } catch (...) { - } - delete event_data; - }; - - napi_status status = js_thread.NonBlockingCall(data, sources_callback); - if (status != napi_ok) { - delete data; + if (info.Length() < 3 || !info[0].IsString() || !info[1].IsString() || !info[2].IsBoolean()) { + Napi::TypeError::New(info.Env(), "ConfirmAutoConfigProbeIngest expects (sessionId: string, probeId: string, received: boolean)") + .ThrowAsJavaScriptException(); + return info.Env().Undefined(); + } + const std::string sessionId = info[0].As().Utf8Value(); + const std::string probeId = info[1].As().Utf8Value(); + if (sessionId.empty() || probeId.empty()) { + Napi::TypeError::New(info.Env(), "ConfirmAutoConfigProbeIngest expects non-empty sessionId and probeId").ThrowAsJavaScriptException(); + return info.Env().Undefined(); } - release_semaphore(ac_sem); -} - -Napi::Value autoConfig::StartCheckSettings(const Napi::CallbackInfo &info) -{ - AutoConfigInfo *startData = new AutoConfigInfo; - startData->event = "starting_step"; - startData->description = "checking_settings"; - startData->percentage = 0; - ac_queue_task_workers.push_back(new std::thread(&autoConfig::queueTask, startData)); - auto conn = GetConnection(info); if (!conn) return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper("AutoConfig", "StartCheckSettings", {}); - + std::vector response = conn->call_synchronous_helper("AutoConfig", "ConfirmAutoConfigProbeIngest", + {ipc::value(sessionId), ipc::value(probeId), + ipc::value((uint32_t)(info[2].As().Value() ? 1 : 0))}); if (!ValidateResponse(info, response)) return info.Env().Undefined(); - - bool success = (bool)response[1].value_union.ui32; - AutoConfigInfo *stopData = new AutoConfigInfo; - if (!success) { - stopData->event = "error"; - stopData->description = "invalid_settings"; - } else { - stopData->event = "stopping_step"; - stopData->description = "checking_settings"; - } - - stopData->percentage = 100; - ac_queue_task_workers.push_back(new std::thread(&autoConfig::queueTask, stopData)); - return info.Env().Undefined(); } -Napi::Value autoConfig::StartSetDefaultSettings(const Napi::CallbackInfo &info) +Napi::Value GetAutoConfigResult(const Napi::CallbackInfo &info) { + std::string sessionId; + if (!GetSessionArgument(info, "GetAutoConfigResult", sessionId)) + return info.Env().Undefined(); + auto conn = GetConnection(info); if (!conn) return info.Env().Undefined(); - std::vector response = conn->call_synchronous_helper("AutoConfig", "StartSetDefaultSettings", {}); - if (!ValidateResponse(info, response)) + std::vector response = conn->call_synchronous_helper("AutoConfig", "GetAutoConfigResult", {ipc::value(sessionId)}); + if (!ValidateResponse(info, response) || response.size() < 2) return info.Env().Undefined(); - return info.Env().Undefined(); + return Napi::String::New(info.Env(), response[1].value_str); } -Napi::Value autoConfig::StartSaveStreamSettings(const Napi::CallbackInfo &info) +Napi::Value CancelAutoConfigSession(const Napi::CallbackInfo &info) { + std::string sessionId; + if (!GetSessionArgument(info, "CancelAutoConfigSession", sessionId)) + return info.Env().Undefined(); + auto conn = GetConnection(info); if (!conn) return info.Env().Undefined(); - std::vector response = conn->call_synchronous_helper("AutoConfig", "StartSaveStreamSettings", {}); + std::vector response = conn->call_synchronous_helper("AutoConfig", "CancelAutoConfigSession", {ipc::value(sessionId)}); if (!ValidateResponse(info, response)) return info.Env().Undefined(); + // Cancellation is awaitable: polling continues until cleanup emits its + // terminal event, after which the caller closes the session. return info.Env().Undefined(); } -Napi::Value autoConfig::StartSaveSettings(const Napi::CallbackInfo &info) +Napi::Value CloseAutoConfigSession(const Napi::CallbackInfo &info) { + std::string sessionId; + if (!GetSessionArgument(info, "CloseAutoConfigSession", sessionId)) + return info.Env().Undefined(); + auto conn = GetConnection(info); - if (!conn) + if (!conn) { + StopLocalSession(sessionId, CallbackShutdownMode::Release); return info.Env().Undefined(); + } - std::vector response = conn->call_synchronous_helper("AutoConfig", "StartSaveSettings", {}); - if (!ValidateResponse(info, response)) + try { + std::vector response = conn->call_synchronous_helper("AutoConfig", "CloseAutoConfigSession", {ipc::value(sessionId)}); + const bool responseIsValid = ValidateResponse(info, response); + StopLocalSession(sessionId, CallbackShutdownMode::Release); + if (!responseIsValid) + return info.Env().Undefined(); + } catch (const std::exception &error) { + StopLocalSession(sessionId, CallbackShutdownMode::Release); + Napi::Error::New(info.Env(), error.what()).ThrowAsJavaScriptException(); + return info.Env().Undefined(); + } catch (...) { + StopLocalSession(sessionId, CallbackShutdownMode::Release); + Napi::Error::New(info.Env(), "CloseAutoConfigSession IPC call failed").ThrowAsJavaScriptException(); return info.Env().Undefined(); + } return info.Env().Undefined(); } +} -Napi::Value autoConfig::TerminateAutoConfig(const Napi::CallbackInfo &info) +void autoConfig::Shutdown() { - auto conn = GetConnection(info); - if (!conn) - return info.Env().Undefined(); - - std::vector response = conn->call_synchronous_helper("AutoConfig", "TerminateAutoConfig", {}); - - if (!ValidateResponse(info, response)) - return info.Env().Undefined(); + std::lock_guard lock(lifecycleMutex); + const std::string sessionId = GetActiveSessionId(); + if (sessionId.empty()) { + StopWorker(CallbackShutdownMode::Abort); + return; + } - if (isWorkerRunning) - stop_worker(); + // Prevent new poll cycles while the server performs awaitable cancellation. + // An already-running query may finish, so local joining remains mandatory. + workerStop.store(true); + auto conn = Controller::GetInstance().GetConnection(); + BestEffortServerCall(conn, "CancelAutoConfigSession", sessionId); + BestEffortServerCall(conn, "CloseAutoConfigSession", sessionId); - return info.Env().Undefined(); + StopWorker(CallbackShutdownMode::Abort); + SetActiveSessionId(""); } void autoConfig::Init(Napi::Env env, Napi::Object exports) { - exports.Set(Napi::String::New(env, "InitializeAutoConfig"), Napi::Function::New(env, autoConfig::InitializeAutoConfig)); - exports.Set(Napi::String::New(env, "StartBandwidthTest"), Napi::Function::New(env, autoConfig::StartBandwidthTest)); - exports.Set(Napi::String::New(env, "StartStreamEncoderTest"), Napi::Function::New(env, autoConfig::StartStreamEncoderTest)); - exports.Set(Napi::String::New(env, "StartRecordingEncoderTest"), Napi::Function::New(env, autoConfig::StartRecordingEncoderTest)); - exports.Set(Napi::String::New(env, "StartCheckSettings"), Napi::Function::New(env, autoConfig::StartCheckSettings)); - exports.Set(Napi::String::New(env, "StartSetDefaultSettings"), Napi::Function::New(env, autoConfig::StartSetDefaultSettings)); - exports.Set(Napi::String::New(env, "StartSaveStreamSettings"), Napi::Function::New(env, autoConfig::StartSaveStreamSettings)); - exports.Set(Napi::String::New(env, "StartSaveSettings"), Napi::Function::New(env, autoConfig::StartSaveSettings)); - exports.Set(Napi::String::New(env, "TerminateAutoConfig"), Napi::Function::New(env, autoConfig::TerminateAutoConfig)); + env.AddCleanupHook([]() { autoConfig::Shutdown(); }); + exports.Set("GetAutoConfigCapabilities", Napi::Function::New(env, GetAutoConfigCapabilities)); + exports.Set("CreateAutoConfigSession", Napi::Function::New(env, CreateAutoConfigSession)); + exports.Set("StartAutoConfigSession", Napi::Function::New(env, StartAutoConfigSession)); + exports.Set("ConfirmAutoConfigProbeIngest", Napi::Function::New(env, ConfirmAutoConfigProbeIngest)); + exports.Set("GetAutoConfigResult", Napi::Function::New(env, GetAutoConfigResult)); + exports.Set("CancelAutoConfigSession", Napi::Function::New(env, CancelAutoConfigSession)); + exports.Set("CloseAutoConfigSession", Napi::Function::New(env, CloseAutoConfigSession)); } diff --git a/obs-studio-client/source/nodeobs_autoconfig.hpp b/obs-studio-client/source/nodeobs_autoconfig.hpp index c9db0a576..8921bbb9f 100644 --- a/obs-studio-client/source/nodeobs_autoconfig.hpp +++ b/obs-studio-client/source/nodeobs_autoconfig.hpp @@ -16,50 +16,14 @@ ******************************************************************************/ #pragma once -#include -#include -#include "utility-v8.hpp" -#ifdef WIN32 -#include -#else -#include -#endif - -struct AutoConfigInfo { - std::string event; - std::string description; - double percentage = 0; -}; -extern const char *ac_sem_name; -#ifdef WIN32 -extern HANDLE ac_sem; -#else -extern sem_t *ac_sem; -#endif +#include namespace autoConfig { -extern bool isWorkerRunning; -extern bool worker_stop; -extern std::chrono::milliseconds sleepInterval; -extern Napi::ThreadSafeFunction js_thread; -extern std::thread *worker_thread; -extern std::vector ac_queue_task_workers; - -void worker(void); -void start_worker(void); -void stop_worker(void); -void queueTask(AutoConfigInfo *data); - void Init(Napi::Env env, Napi::Object exports); -Napi::Value InitializeAutoConfig(const Napi::CallbackInfo &info); -Napi::Value StartBandwidthTest(const Napi::CallbackInfo &info); -Napi::Value StartStreamEncoderTest(const Napi::CallbackInfo &info); -Napi::Value StartRecordingEncoderTest(const Napi::CallbackInfo &info); -Napi::Value StartCheckSettings(const Napi::CallbackInfo &info); -Napi::Value StartSetDefaultSettings(const Napi::CallbackInfo &info); -Napi::Value StartSaveStreamSettings(const Napi::CallbackInfo &info); -Napi::Value StartSaveSettings(const Napi::CallbackInfo &info); -Napi::Value TerminateAutoConfig(const Napi::CallbackInfo &info); +// Stops client-side polling and releases the callback. If a session is active, +// its server-side cancellation/close is attempted while IPC is still usable. +// This is idempotent and safe to call from disconnect and environment cleanup. +void Shutdown(); } diff --git a/obs-studio-client/source/streaming.cpp b/obs-studio-client/source/streaming.cpp index 133084278..84ac2927e 100644 --- a/obs-studio-client/source/streaming.cpp +++ b/obs-studio-client/source/streaming.cpp @@ -24,6 +24,10 @@ #include "reconnect.hpp" #include "network.hpp" #include "video.hpp" +#include "simple-streaming.hpp" +#include "advanced-streaming.hpp" +#include "enhanced-broadcasting-simple-streaming.hpp" +#include "enhanced-broadcasting-advanced-streaming.hpp" void osn::Streaming::ReleaseObjects() { @@ -448,3 +452,40 @@ Napi::Value osn::Streaming::GetDataOutput(const Napi::CallbackInfo &info) return Napi::Number::New(info.Env(), response[1].value_union.fp64); } + +bool osn::TryUnwrapStreamingUid(const Napi::Value &value, uint64_t &out_uid) +{ + if (!value.IsObject()) + return false; + Napi::Object obj = value.As(); + + if (obj.InstanceOf(osn::SimpleStreaming::constructor.Value())) { + auto *s = Napi::ObjectWrap::Unwrap(obj); + if (s) { + out_uid = s->uid; + return true; + } + } + if (obj.InstanceOf(osn::AdvancedStreaming::constructor.Value())) { + auto *s = Napi::ObjectWrap::Unwrap(obj); + if (s) { + out_uid = s->uid; + return true; + } + } + if (obj.InstanceOf(osn::EnhancedBroadcastingSimpleStreaming::constructor.Value())) { + auto *s = Napi::ObjectWrap::Unwrap(obj); + if (s) { + out_uid = s->uid; + return true; + } + } + if (obj.InstanceOf(osn::EnhancedBroadcastingAdvancedStreaming::constructor.Value())) { + auto *s = Napi::ObjectWrap::Unwrap(obj); + if (s) { + out_uid = s->uid; + return true; + } + } + return false; +} diff --git a/obs-studio-client/source/streaming.hpp b/obs-studio-client/source/streaming.hpp index bda23125b..a670dbe46 100644 --- a/obs-studio-client/source/streaming.hpp +++ b/obs-studio-client/source/streaming.hpp @@ -65,4 +65,6 @@ class Streaming : public WorkerSignals { void Start(const Napi::CallbackInfo &info); void Stop(const Napi::CallbackInfo &info); }; + +bool TryUnwrapStreamingUid(const Napi::Value &value, uint64_t &out_uid); } diff --git a/obs-studio-client/source/video.cpp b/obs-studio-client/source/video.cpp index a769d3b92..bc0c15495 100644 --- a/obs-studio-client/source/video.cpp +++ b/obs-studio-client/source/video.cpp @@ -38,6 +38,7 @@ Napi::Object osn::Video::Init(Napi::Env env, Napi::Object exports) InstanceAccessor("skippedFrames", &osn::Video::GetSkippedFrames, nullptr), InstanceAccessor("encodedFrames", &osn::Video::GetEncodedFrames, nullptr), + InstanceAccessor("canvasId", &osn::Video::GetCanvasId, nullptr), }); exports.Set("Video", func); osn::Video::constructor = Napi::Persistent(func); @@ -105,10 +106,14 @@ void osn::Video::Destroy(const Napi::CallbackInfo &info) auto response = conn->call_synchronous_helper("Video", "RemoveVideoContext", {ipc::value((uint64_t)(this->canvasId))}); ValidateResponse(info, response); - isLastVideoValid = false; return; } +Napi::Value osn::Video::GetCanvasId(const Napi::CallbackInfo &info) +{ + return Napi::Number::New(info.Env(), (double)this->canvasId); +} + inline void CreateVideo(const Napi::CallbackInfo &info, const std::vector &response, Napi::Object &video, uint32_t index) { video.Set("fpsNum", response.at(index++).value_union.ui32); @@ -146,19 +151,16 @@ Napi::Value osn::Video::get(const Napi::CallbackInfo &info) if (!conn) return info.Env().Undefined(); - if (!isLastVideoValid) { - lastVideo = conn->call_synchronous_helper("Video", "GetVideoContext", {ipc::value((uint64_t)this->canvasId)}); + auto response = conn->call_synchronous_helper("Video", "GetVideoContext", {ipc::value((uint64_t)this->canvasId)}); - if (!ValidateResponse(info, lastVideo)) - return info.Env().Undefined(); + if (!ValidateResponse(info, response)) + return info.Env().Undefined(); - if (!(lastVideo.size() == 11 || lastVideo.size() == 12)) - return info.Env().Undefined(); - isLastVideoValid = true; - } + if (!(response.size() == 11 || response.size() == 12)) + return info.Env().Undefined(); Napi::Object video = Napi::Object::New(info.Env()); - CreateVideo(info, lastVideo, video, 1); + CreateVideo(info, response, video, 1); return video; } @@ -182,9 +184,6 @@ void osn::Video::set(const Napi::CallbackInfo &info, const Napi::Value &value) auto response = conn->call_synchronous_helper("Video", "SetVideoContext", args); - lastVideo.resize(0); - isLastVideoValid = false; - if (!ValidateResponse(info, response)) return; } diff --git a/obs-studio-client/source/video.hpp b/obs-studio-client/source/video.hpp index 4e264aabb..8934c431b 100644 --- a/obs-studio-client/source/video.hpp +++ b/obs-studio-client/source/video.hpp @@ -26,11 +26,6 @@ class Video : public Napi::ObjectWrap { uint64_t canvasId = 0; constexpr static uint64_t nonCavasId = std::numeric_limits::max(); -private: - std::vector lastVideo; - bool isLastVideoValid = false; - -public: static Napi::FunctionReference constructor; static Napi::Object Init(Napi::Env env, Napi::Object exports); Video(const Napi::CallbackInfo &info); @@ -46,5 +41,10 @@ class Video : public Napi::ObjectWrap { Napi::Value GetLegacySettings(const Napi::CallbackInfo &info); void SetLegacySettings(const Napi::CallbackInfo &info, const Napi::Value &value); + + // Read-only accessor exposing the server-side canvas id (the same value the + // server's osn::Video::Manager keys this object by). Required so the frontend + // can refer to a canvas in APIs like autoconfig that take ids. + Napi::Value GetCanvasId(const Napi::CallbackInfo &info); }; } diff --git a/obs-studio-client/tests/autoconfig-probe-policy-test.cpp b/obs-studio-client/tests/autoconfig-probe-policy-test.cpp new file mode 100644 index 000000000..ec4515e93 --- /dev/null +++ b/obs-studio-client/tests/autoconfig-probe-policy-test.cpp @@ -0,0 +1,198 @@ +#include "autoconfig-probe-policy.hpp" + +#include + +using autoConfig::probePolicy::YoutubeRampEvidence; +using autoConfig::probePolicy::YoutubeBaselineAssessment; +using autoConfig::probePolicy::YoutubeBaselineDecision; +using autoConfig::probePolicy::YoutubeConfirmationDecision; +using autoConfig::probePolicy::YoutubeProbeSampleClass; +using autoConfig::probePolicy::YoutubeProbeSampleMetrics; +using autoConfig::probePolicy::assessYoutubeBaseline; +using autoConfig::probePolicy::classifyYoutubeProbeSample; +using autoConfig::probePolicy::clampEstimateToObservedSafe; +using autoConfig::probePolicy::decideYoutubeConfirmation; +using autoConfig::probePolicy::effectiveProbeCeilingKbps; +using autoConfig::probePolicy::hasProbeThroughputMetrics; +using autoConfig::probePolicy::makeYoutubeProbeSampleMetrics; +using autoConfig::probePolicy::probeSubstepProgress; +using autoConfig::probePolicy::reachedEffectiveProbeCeiling; +using autoConfig::probePolicy::resolveYoutubeBaseline; +using autoConfig::probePolicy::safeVideoKbps; +using autoConfig::probePolicy::youtubeLowControlRecovered; +using autoConfig::probePolicy::youtubeSampleAccepted; + +TEST_CASE("YouTube probe metrics use deterministic basis-point ratios") +{ + const YoutubeProbeSampleMetrics sample = makeYoutubeProbeSampleMetrics(900, 1000, 3, 150, 10, 2, 100); + + CHECK(sample.throughputBasisPoints == 9000); + CHECK(sample.dropBasisPoints == 200); + CHECK(sample.congestionHighBasisPoints == 1000); + CHECK(sample.congestionSevereBasisPoints == 200); + CHECK(classifyYoutubeProbeSample(sample) == YoutubeProbeSampleClass::Clean); +} + +TEST_CASE("YouTube probe sample classification honors clean and hard boundaries") +{ + CHECK(classifyYoutubeProbeSample({9000, 200, 1000, 200}) == YoutubeProbeSampleClass::Clean); + CHECK(classifyYoutubeProbeSample({8999, 200, 1000, 200}) == YoutubeProbeSampleClass::Marginal); + CHECK(classifyYoutubeProbeSample({7500, 500, 3000, 1000}) == YoutubeProbeSampleClass::Marginal); + CHECK(classifyYoutubeProbeSample({7499, 0, 0, 0}) == YoutubeProbeSampleClass::Hard); + CHECK(classifyYoutubeProbeSample({10000, 501, 0, 0}) == YoutubeProbeSampleClass::Hard); + CHECK(classifyYoutubeProbeSample({10000, 0, 3001, 0}) == YoutubeProbeSampleClass::Hard); + CHECK(classifyYoutubeProbeSample({10000, 0, 0, 1001}) == YoutubeProbeSampleClass::Hard); +} + +TEST_CASE("YouTube probe ignores isolated congestion but rejects sustained congestion") +{ + const YoutubeProbeSampleMetrics isolatedSpike = makeYoutubeProbeSampleMetrics(950, 1000, 0, 150, 1, 0, 100); + const YoutubeProbeSampleMetrics sustainedCongestion = makeYoutubeProbeSampleMetrics(950, 1000, 0, 150, 31, 0, 100); + + CHECK(classifyYoutubeProbeSample(isolatedSpike) == YoutubeProbeSampleClass::Clean); + CHECK(classifyYoutubeProbeSample(sustainedCongestion) == YoutubeProbeSampleClass::Hard); +} + +TEST_CASE("Two YouTube baseline samples select every initial decision") +{ + const YoutubeProbeSampleMetrics cleanA{9500, 100, 500, 100}; + const YoutubeProbeSampleMetrics cleanB{9300, 150, 700, 150}; + const YoutubeProbeSampleMetrics marginalA{8500, 300, 1500, 300}; + const YoutubeProbeSampleMetrics marginalB{8200, 350, 2000, 400}; + const YoutubeProbeSampleMetrics hardA{7000, 300, 1500, 300}; + const YoutubeProbeSampleMetrics hardB{8000, 600, 1500, 300}; + + CHECK(assessYoutubeBaseline(cleanA, cleanB).decision == YoutubeBaselineDecision::Clean); + CHECK(assessYoutubeBaseline(cleanA, {10500, 200, 1000, 200}).decision == YoutubeBaselineDecision::NeedsThird); + CHECK(assessYoutubeBaseline(marginalA, marginalB).decision == YoutubeBaselineDecision::Impaired); + CHECK(assessYoutubeBaseline(cleanA, marginalA).decision == YoutubeBaselineDecision::NeedsThird); + CHECK(assessYoutubeBaseline(hardA, hardB).decision == YoutubeBaselineDecision::Unstable); +} + +TEST_CASE("A third YouTube baseline sample resolves from component medians") +{ + const YoutubeProbeSampleMetrics clean{9500, 100, 500, 100}; + const YoutubeProbeSampleMetrics marginal{8500, 300, 1500, 300}; + const YoutubeProbeSampleMetrics hard{6000, 700, 4000, 1500}; + + const YoutubeBaselineAssessment impaired = resolveYoutubeBaseline(clean, hard, marginal); + CHECK(impaired.decision == YoutubeBaselineDecision::Impaired); + CHECK(impaired.reference.throughputBasisPoints == marginal.throughputBasisPoints); + CHECK(impaired.reference.dropBasisPoints == marginal.dropBasisPoints); + CHECK(impaired.reference.congestionHighBasisPoints == marginal.congestionHighBasisPoints); + CHECK(impaired.reference.congestionSevereBasisPoints == marginal.congestionSevereBasisPoints); + + CHECK(resolveYoutubeBaseline(clean, hard, clean).decision == YoutubeBaselineDecision::Clean); + CHECK(resolveYoutubeBaseline(hard, clean, hard).decision == YoutubeBaselineDecision::Unstable); + CHECK(resolveYoutubeBaseline({7000, 100, 500, 100}, clean, {9500, 600, 500, 100}).decision == YoutubeBaselineDecision::Unstable); +} + +TEST_CASE("Impaired YouTube baseline accepts only bounded relative degradation") +{ + const YoutubeBaselineAssessment baseline{YoutubeBaselineDecision::Impaired, {8500, 300, 1500, 300}}; + + CHECK(youtubeSampleAccepted({8000, 400, 2500, 800}, baseline)); + CHECK_FALSE(youtubeSampleAccepted({7999, 400, 2500, 800}, baseline)); + CHECK_FALSE(youtubeSampleAccepted({8000, 401, 2500, 800}, baseline)); + CHECK_FALSE(youtubeSampleAccepted({8000, 400, 2501, 800}, baseline)); + CHECK_FALSE(youtubeSampleAccepted({8000, 400, 2500, 801}, baseline)); + + const YoutubeBaselineAssessment cleanBaseline{YoutubeBaselineDecision::Clean, {9500, 100, 500, 100}}; + CHECK(youtubeSampleAccepted({9200, 100, 500, 100}, cleanBaseline)); + CHECK_FALSE(youtubeSampleAccepted({8500, 100, 500, 100}, cleanBaseline)); +} + +TEST_CASE("YouTube low control must recover acceptance and the previous rung") +{ + const YoutubeBaselineAssessment baseline{YoutubeBaselineDecision::Impaired, {8500, 300, 1500, 300}}; + const YoutubeProbeSampleMetrics original{8800, 300, 1000, 200}; + + CHECK(youtubeLowControlRecovered({8300, 400, 2000, 700}, original, baseline)); + CHECK_FALSE(youtubeLowControlRecovered({8299, 400, 2000, 700}, original, baseline)); + CHECK_FALSE(youtubeLowControlRecovered({8300, 401, 2000, 700}, original, baseline)); + CHECK_FALSE(youtubeLowControlRecovered({8300, 400, 2001, 700}, original, baseline)); + CHECK_FALSE(youtubeLowControlRecovered({8300, 400, 2000, 701}, original, baseline)); +} + +TEST_CASE("YouTube high-low-high confirmation distinguishes all four outcomes") +{ + CHECK(decideYoutubeConfirmation(true, false) == YoutubeConfirmationDecision::CapacityKnee); + CHECK(decideYoutubeConfirmation(true, true) == YoutubeConfirmationDecision::TransientRecovered); + CHECK(decideYoutubeConfirmation(false, false) == YoutubeConfirmationDecision::PathUnstable); + CHECK(decideYoutubeConfirmation(false, true) == YoutubeConfirmationDecision::Inconsistent); +} + +TEST_CASE("YouTube first-rung failure retains a conservative observed cap") +{ + YoutubeRampEvidence evidence; + evidence.observe(750, false, 1128); + + CHECK_FALSE(evidence.passedStep); + CHECK(evidence.recommendationBasisKbps == 750); + CHECK(evidence.safeVideoKbps(80, 128) == 472); + CHECK(clampEstimateToObservedSafe(6000, evidence.safeVideoKbps(80, 128), 12000) == 472); +} + +TEST_CASE("YouTube failed first rung cannot recommend above the attempted rate after a burst") +{ + YoutubeRampEvidence evidence; + evidence.observe(2000, false, 1128); + + CHECK_FALSE(evidence.passedStep); + CHECK(evidence.recommendationBasisKbps == 2000); + CHECK(evidence.failedUpperBoundKbps == 1128); + CHECK(evidence.safeVideoKbps(80, 128) == 774); + CHECK(clampEstimateToObservedSafe(6000, evidence.safeVideoKbps(80, 128), 12000) == 774); +} + +TEST_CASE("YouTube failed higher rung cannot raise the last passing recommendation") +{ + YoutubeRampEvidence evidence; + evidence.observe(2100, true, 2128); + evidence.observe(1900, false, 4128); + + CHECK(evidence.passedStep); + CHECK(evidence.recommendationBasisKbps == 2100); + CHECK(evidence.safeVideoKbps(80, 128) == 1392); + CHECK(clampEstimateToObservedSafe(1400, evidence.safeVideoKbps(80, 128), 12000) == 1392); +} + +TEST_CASE("Aggregate probe throughput reserves audio before recommending video bitrate") +{ + CHECK(safeVideoKbps(6000, 70, 32) == 4168); + CHECK(safeVideoKbps(128, 80, 128) == 0); +} + +TEST_CASE("Successful zero-safe probe metrics remain present in result provenance") +{ + CHECK(hasProbeThroughputMetrics(true, 0)); + CHECK(hasProbeThroughputMetrics(false, 128)); + CHECK_FALSE(hasProbeThroughputMetrics(false, 0)); +} + +TEST_CASE("YouTube reports the exact effective probe cap as ceiling reached") +{ + const int requestCeiling = effectiveProbeCeilingKbps(12000, 0, 6000); + CHECK(requestCeiling == 6000); + CHECK_FALSE(reachedEffectiveProbeCeiling(4000, requestCeiling)); + CHECK(reachedEffectiveProbeCeiling(6000, requestCeiling)); + + const int platformCeiling = effectiveProbeCeilingKbps(12000, 4500, 6000); + CHECK(platformCeiling == 4500); + CHECK(reachedEffectiveProbeCeiling(4500, platformCeiling)); +} + +TEST_CASE("Probe substeps advance monotonically within their provider progress slot") +{ + double previous = 30.0; + for (size_t index = 0; index < 7; index++) { + const double progress = probeSubstepProgress(30.0, 65.0, index, 7); + CHECK(progress > previous); + CHECK(progress < 65.0); + previous = progress; + } + + CHECK(probeSubstepProgress(30.0, 47.5, 6, 7) < 47.5); + CHECK(probeSubstepProgress(47.5, 65.0, 0, 1) > 47.5); + CHECK(probeSubstepProgress(30.0, 65.0, 0, 0) == 30.0); +} diff --git a/obs-studio-server/source/nodeobs_api.cpp b/obs-studio-server/source/nodeobs_api.cpp index f00dce2b2..6eea8440f 100644 --- a/obs-studio-server/source/nodeobs_api.cpp +++ b/obs-studio-server/source/nodeobs_api.cpp @@ -29,6 +29,7 @@ #include "util/lexer.h" #include "util-crashmanager.h" #include "util-metricsprovider.h" +#include "nodeobs_service.h" #include "osn-streaming.hpp" #include "osn-recording.hpp" @@ -1662,7 +1663,7 @@ void OBS_API::destroyOBS_API(void) #endif OBS_content::OBS_content_shutdownDisplays(); - autoConfig::WaitPendingTests(); + autoConfig::Shutdown(); OBS_service::stopAllOutputs(); OBS_service::waitReleaseWorker(); diff --git a/obs-studio-server/source/nodeobs_api.h b/obs-studio-server/source/nodeobs_api.h index 5a5509eb5..6eb415a0d 100644 --- a/obs-studio-server/source/nodeobs_api.h +++ b/obs-studio-server/source/nodeobs_api.h @@ -30,7 +30,7 @@ #include #include #include "nodeobs_configManager.hpp" -#include "nodeobs_service.h" +//#include "nodeobs_service.h" #include "util-osx.hpp" extern std::string g_moduleDirectory; diff --git a/obs-studio-server/source/nodeobs_autoconfig.cpp b/obs-studio-server/source/nodeobs_autoconfig.cpp index 581155a84..9e4f26096 100644 --- a/obs-studio-server/source/nodeobs_autoconfig.cpp +++ b/obs-studio-server/source/nodeobs_autoconfig.cpp @@ -1,1523 +1,2788 @@ /****************************************************************************** - Copyright (C) 2016-2019 by Streamlabs (General Workings Inc) + Copyright (C) 2026 by Streamlabs This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - ******************************************************************************/ #include "nodeobs_autoconfig.h" -#include -#include + +#include "autoconfig-probe-policy.hpp" +#include "osn-encoders.hpp" #include "osn-error.hpp" #include "shared.hpp" -#include "osn-encoders.hpp" - -enum class Type { Invalid, Streaming, Recording }; -enum class Service { Twitch, Hitbox, Beam, YouTube, Other }; - -enum class Encoder { x264, NVENC, QSV, AMD, Apple, Stream }; +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace autoConfig { +namespace { + +constexpr int kSchemaVersion = 1; +constexpr int kProbeConnectTimeoutMs = 8000; +constexpr int kProbeWarmupMs = 750; +constexpr int kProbeSampleMs = 5000; +constexpr int kProbeStopTimeoutMs = 3000; +constexpr int kYoutubeIngestConfirmationTimeoutMs = 15000; +constexpr int kCancelTimeoutMs = 8000; +constexpr uint64_t kProbeMaxBytes = 25ULL * 1024ULL * 1024ULL; +constexpr uint64_t kYoutubeProbeMaxBytes = 64ULL * 1024ULL * 1024ULL; +constexpr int kProbeMaximumBitrateKbps = 10000; +constexpr int kYoutubeProbeMaximumBitrateKbps = 12000; +constexpr int kYoutubeProbeInitialBitrateKbps = 1000; +constexpr int kYoutubeProbeSettleMs = 500; +constexpr int kYoutubeProbeSampleMs = 5000; +constexpr int kYoutubeProbeSubwindowMs = 1000; +constexpr int kYoutubeProbeTotalTimeoutMs = 100000; +constexpr int kYoutubeProbeBudgetSlackMs = 250; +constexpr int kYoutubeProbeMaximumConfirmationEpisodes = 2; +constexpr int kYoutubeProbeBudgetEstimatePercent = 115; +constexpr float kYoutubeProbeCongestionHigh = 0.20f; +constexpr float kYoutubeProbeCongestionSevere = 0.50f; +constexpr int kTwitchProbeSafeMultiplierPercent = 70; +constexpr int kYoutubeProbeSafeMultiplierPercent = 80; +constexpr int kTwitchProbeAudioBitrateKbps = 32; +constexpr int kYoutubeProbeAudioBitrateKbps = 128; +constexpr int kDefaultEstimatedBitrateKbps = 2500; +constexpr int kHardwareWarmupMs = 500; +constexpr int kHardwareSampleMs = 1500; +constexpr int kHardwareStopTimeoutMs = 1000; +constexpr int kHardwarePhaseTimeoutMs = 12000; +constexpr int kHardwareMaximumLongEdge = 1920; +constexpr int kHardwareMaximumShortEdge = 1080; + +enum class SessionState { Created, Running, Complete, Cancelled, Failed, Closed }; + +struct Limits { + int maxBitrateKbps = 0; + int maxWidth = 0; + int maxHeight = 0; + int maxFpsNum = 0; + int maxFpsDen = 0; + + bool any() const { return maxBitrateKbps > 0 || maxWidth > 0 || maxHeight > 0 || maxFpsNum > 0; } +}; -enum class Quality { Stream, High }; +struct CurrentSettings { + int width = 0; + int height = 0; + int fpsNum = 0; + int fpsDen = 1; + int bitrateKbps = 0; + std::string encoderId; + std::string codec; + std::string preset; +}; -enum class FPSType : int { PreferHighFPS, PreferHighRes, UseCurrent, fps30, fps60 }; +struct EncoderSelection { + std::string id; + bool replaced = false; +}; -enum ThreadedTests : int { BandwidthTest, StreamEncoderTest, RecordingEncoderTest, SaveStreamSettings, SaveSettings, SetDefaultSettings, Count }; +struct HardwareAssessment { + bool attempted = false; + bool passed = false; + bool cancelled = false; + bool constrained = false; + std::string reason; + CurrentSettings value; +}; -class AutoConfigInfo { -public: - AutoConfigInfo(const std::string &a_event, const std::string &a_description, double a_percentage) - { - event = a_event; - description = a_description; - percentage = a_percentage; - }; - ~AutoConfigInfo(){}; +struct Destination { + std::string platform; +}; - std::string event; - std::string description; - double percentage; +struct LegRequest { + std::string legId; + std::string display; + std::vector destinations; + CurrentSettings current; + Limits limits; + std::string estimateReason; }; -std::array, ThreadedTests::Count> asyncTests; -std::mutex eventsMutex; -std::queue events; - -Service serviceSelected = Service::Other; -Quality recordingQuality = Quality::Stream; -Encoder recordingEncoder = Encoder::Stream; -Encoder streamingEncoder = Encoder::x264; -Type type = Type::Streaming; -FPSType fpsType = FPSType::PreferHighFPS; -uint64_t idealBitrate = 4500; -uint64_t baseResolutionCX = 1920; -uint64_t baseResolutionCY = 1080; -uint64_t idealResolutionCX = 1280; -uint64_t idealResolutionCY = 720; -int idealFPSNum = 60; -int idealFPSDen = 1; -std::string serviceName; -std::string serverName; -std::string server; -std::string key; - -bool hardwareEncodingAvailable = false; -bool nvencAvailable = false; -bool qsvAvailable = false; -bool vceAvailable = false; -bool appleAvailable = false; - -int startingBitrate = 4500; -bool customServer = false; -bool bandwidthTest = true; -bool testRegions = true; - -bool regionNA = false; -bool regionSA = false; -bool regionEU = false; -bool regionAS = false; -bool regionOC = false; - -bool preferHighFPS = true; -bool preferHardware = true; -int specificFPSNum = 0; -int specificFPSDen = 0; - -std::condition_variable cv; -std::mutex m; -bool cancel = false; -bool started = false; - -bool softwareTested = false; - -struct ServerInfo { - std::string name; - std::string address; - int bitrate = 0; - int ms = -1; - - inline ServerInfo() {} - - inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {} +struct ProbeRequest { + std::string probeId; + std::string kind; + std::string legId; + std::string serviceName; + std::string server; + std::string streamKey; + std::string provider; + bool eligible = false; + std::string denialReason; }; -void autoConfig::Register(ipc::server &srv) -{ - std::shared_ptr cls = std::make_shared("AutoConfig"); - cls->register_function(std::make_shared("InitializeAutoConfig", std::vector{ipc::type::String, ipc::type::String}, - autoConfig::InitializeAutoConfig)); - cls->register_function(std::make_shared("StartBandwidthTest", std::vector{}, autoConfig::StartBandwidthTest)); - cls->register_function(std::make_shared("StartStreamEncoderTest", std::vector{}, autoConfig::StartStreamEncoderTest)); - cls->register_function(std::make_shared("StartRecordingEncoderTest", std::vector{}, autoConfig::StartRecordingEncoderTest)); - cls->register_function(std::make_shared("StartCheckSettings", std::vector{}, autoConfig::StartCheckSettings)); - cls->register_function(std::make_shared("StartSetDefaultSettings", std::vector{}, autoConfig::StartSetDefaultSettings)); - cls->register_function(std::make_shared("StartSaveStreamSettings", std::vector{}, autoConfig::StartSaveStreamSettings)); - cls->register_function(std::make_shared("StartSaveSettings", std::vector{}, autoConfig::StartSaveSettings)); - cls->register_function(std::make_shared("TerminateAutoConfig", std::vector{}, autoConfig::TerminateAutoConfig)); - cls->register_function(std::make_shared("Query", std::vector{}, autoConfig::Query)); +struct MeasurementProvenance { + std::string provider; + std::string method; + uint64_t measuredKbps = 0; + uint64_t safeKbps = 0; + int headroomPercent = 0; + bool success = false; + bool ceilingReached = false; +}; - srv.register_collection(cls); -} +struct Recommendation { + std::string legId; + std::string display; + std::vector destinations; + Limits limits; + std::string measurementMode = "estimated"; + std::string confidence = "medium"; + std::string reason; + CurrentSettings value; + std::vector probes; +}; -void autoConfig::WaitPendingTests(double timeout) -{ - clock_t start_time = clock(); - while ((float(clock() - start_time) / CLOCKS_PER_SEC) < timeout) { +struct SessionEvent { + uint64_t sequence = 0; + std::string type; + std::string phase; + double progress = 0; + std::string code; + std::string legId; + std::string measurementMode; + std::string probeId; + std::string provider; + uint32_t targetBitrateKbps = 0; +}; - bool all_finished = true; - for (auto &async_test : asyncTests) { - if (async_test.valid()) { - auto status = async_test.wait_for(std::chrono::milliseconds(0)); - if (status != std::future_status::ready) { - all_finished = false; - } - } - } +struct Session : std::enable_shared_from_this { + std::string id; + std::string topology; + std::vector legs; + std::vector probes; + + std::atomic state{SessionState::Created}; + std::atomic cancelRequested{false}; + // Serializes creation and inspection of worker. IPC calls may arrive from + // different client connections, so the atomic state alone is not sufficient + // to protect std::future from concurrent assignment/wait operations. + std::mutex lifecycleMutex; + std::future worker; + + std::mutex mutex; + uint64_t nextSequence = 1; + std::queue events; + std::string resultJson; + + // The worker owns this output. Cancel only borrows it while holding this + // mutex, so it can request a force-stop without racing release. + std::mutex probeMutex; + obs_output_t *activeProbeOutput = nullptr; + std::mutex probeConfirmationMutex; + std::condition_variable probeConfirmationCondition; + // 0 = pending, 1 = accepted, -1 = rejected. Only YouTube probes use + // this gate; Twitch retains its bandwidth-test-key behavior. + std::map probeConfirmations; + std::string activeConfirmationProbeId; +}; - if (all_finished) - break; +std::mutex sessionsMutex; +std::shared_ptr activeSession; +std::atomic nextSessionId{1}; +std::atomic shuttingDown{false}; - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } +static void returnError(std::vector &rval, const char *message) +{ + rval.push_back(ipc::value((uint64_t)ErrorCode::Error)); + rval.push_back(ipc::value(message)); } -void autoConfig::TestHardwareEncoding(void) +static std::string lowerCopy(std::string value) { - size_t idx = 0; - const char *id; - while (obs_enum_encoder_types(idx++, &id)) { - if (strcmp(id, ADVANCED_ENCODER_NVENC) == 0) - hardwareEncodingAvailable = nvencAvailable = true; - else if (strcmp(id, ADVANCED_ENCODER_QSV) == 0) - hardwareEncodingAvailable = qsvAvailable = true; - else if (strcmp(id, ADVANCED_ENCODER_AMD) == 0) - hardwareEncodingAvailable = vceAvailable = true; -#ifdef __APPLE__ - else if (strcmp(id, APPLE_HARDWARE_VIDEO_ENCODER_M1) == 0 -#ifndef __aarch64__ - && os_get_emulation_status() == true -#endif - ) - if (__builtin_available(macOS 13.0, *)) - hardwareEncodingAvailable = appleAvailable = true; -#endif - } + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { return (char)std::tolower(ch); }); + return value; } -static inline void string_depad_key(std::string &key) +static bool hasSuffix(const std::string &value, const std::string &suffix) { - while (!key.empty()) { - char ch = key.back(); - if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') - key.pop_back(); - else - break; - } + return value.size() >= suffix.size() && value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0; } -bool autoConfig::CanTestServer(const char *server) +static bool isOfficialTwitchServer(const std::string &server) { - if (!testRegions || (regionNA && regionSA && regionEU && regionAS && regionOC)) + std::string value = lowerCopy(server); + if (value == "auto") return true; - if (serviceSelected == Service::Twitch) { - if (astrcmp_n(server, "NA:", 3) == 0 || astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 || - astrcmp_n(server, "US Central:", 11) == 0) { - return regionNA; - } else if (astrcmp_n(server, "South America:", 14) == 0) { - return regionSA; - } else if (astrcmp_n(server, "EU:", 3) == 0) { - return regionEU; - } else if (astrcmp_n(server, "Asia:", 5) == 0) { - return regionAS; - } else if (astrcmp_n(server, "Australia:", 10) == 0) { - return regionOC; - } else { - return true; - } - } else if (serviceSelected == Service::Hitbox) { - if (strcmp(server, "Default") == 0) { - return true; - } else if (astrcmp_n(server, "US-West:", 8) == 0 || astrcmp_n(server, "US-East:", 8) == 0) { - return regionNA; - } else if (astrcmp_n(server, "South America:", 14) == 0) { - return regionSA; - } else if (astrcmp_n(server, "EU-", 3) == 0) { - return regionEU; - } else if (astrcmp_n(server, "South Korea:", 12) == 0 || astrcmp_n(server, "Asia:", 5) == 0 || astrcmp_n(server, "China:", 6) == 0) { - return regionAS; - } else if (astrcmp_n(server, "Oceania:", 8) == 0) { - return regionOC; - } else { - return true; - } - } else if (serviceSelected == Service::Beam) { - if (astrcmp_n(server, "US:", 3) == 0 || astrcmp_n(server, "Canada:", 7) || astrcmp_n(server, "Mexico:", 7)) { - return regionNA; - } else if (astrcmp_n(server, "Brazil:", 7) == 0) { - return regionSA; - } else if (astrcmp_n(server, "EU:", 3) == 0) { - return regionEU; - } else if (astrcmp_n(server, "South Korea:", 12) == 0 || astrcmp_n(server, "Asia:", 5) == 0 || astrcmp_n(server, "India:", 6) == 0) { - return regionAS; - } else if (astrcmp_n(server, "Australia:", 10) == 0) { - return regionOC; - } else { - return true; - } - } else { - return true; - } + const size_t scheme = value.find("://"); + if (scheme == std::string::npos || (value.compare(0, 7, "rtmp://") != 0 && value.compare(0, 8, "rtmps://") != 0)) + return false; + + const size_t hostStart = scheme + 3; + const size_t hostEnd = value.find_first_of("/:?#", hostStart); + const std::string host = value.substr(hostStart, hostEnd == std::string::npos ? std::string::npos : hostEnd - hostStart); + if (host.empty() || host.find('@') != std::string::npos) + return false; - return false; + return host == "live.twitch.tv" || hasSuffix(host, ".twitch.tv") || host == "live-video.net" || hasSuffix(host, ".live-video.net"); } -void GetServers(std::vector &servers) +static bool containsWhitespaceOrControl(const std::string &value) { - OBSData settings = obs_data_create(); - obs_data_release(settings); - // obs_data_set_string(settings, "service", wiz->serviceName.c_str()); - //FIX ME - obs_data_set_string(settings, "service", serviceName.c_str()); - - obs_properties_t *ppts = obs_get_service_properties("rtmp_common"); - obs_property_t *p = obs_properties_get(ppts, "service"); - obs_property_modified(p, settings); - - p = obs_properties_get(ppts, "server"); - size_t count = obs_property_list_item_count(p); - servers.reserve(count); - - for (size_t i = 0; i < count; i++) { - const char *name = obs_property_list_item_name(p, i); - const char *server = obs_property_list_item_string(p, i); - - if (autoConfig::CanTestServer(name)) { - ServerInfo info(name, server); - servers.push_back(info); - } - } - - obs_properties_destroy(ppts); + return std::any_of(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch) || std::iscntrl(ch); }); } -void start_next_step(void (*task)(), std::string event, std::string description, int percentage) +static bool isBoundedTwitchKey(const std::string &key) { - /*eventCallbackQueue.work_queue.push_back({cb, event, description, percentage}); - eventCallbackQueue.Signal(); - - if(task) - std::thread(*task).detach();*/ + return !key.empty() && key.size() <= 4096 && !containsWhitespaceOrControl(key); } -void autoConfig::TerminateAutoConfig(void *data, const int64_t id, const std::vector &args, std::vector &rval) +static bool isBoundedYoutubeKey(const std::string &key) { - StopThread(); - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - AUTO_DEBUG; + if (key.empty() || key.size() > 1024 || containsWhitespaceOrControl(key)) + return false; + return key.find_first_of("/\\?#@:") == std::string::npos; } -void autoConfig::Query(void *data, const int64_t id, const std::vector &args, std::vector &rval) +static bool isOfficialYoutubeRtmpsServer(const std::string &server) { - std::unique_lock ulock(eventsMutex); - if (events.empty()) { - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - AUTO_DEBUG; - return; - } + if (server.empty() || server.size() > 2048 || containsWhitespaceOrControl(server)) + return false; - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + const std::string value = lowerCopy(server); + constexpr const char *scheme = "rtmps://"; + if (value.compare(0, std::strlen(scheme), scheme) != 0 || value.find_first_of("?#") != std::string::npos) + return false; - rval.push_back(ipc::value(events.front().event)); - rval.push_back(ipc::value(events.front().description)); - rval.push_back(ipc::value(events.front().percentage)); + const size_t authorityStart = std::strlen(scheme); + const size_t pathStart = value.find('/', authorityStart); + if (pathStart == std::string::npos || value.substr(pathStart) != "/live2") + return false; - events.pop(); + const std::string authority = value.substr(authorityStart, pathStart - authorityStart); + if (authority.empty() || authority.find('@') != std::string::npos) + return false; - AUTO_DEBUG; + std::string host = authority; + const size_t portSeparator = authority.find(':'); + if (portSeparator != std::string::npos) { + if (authority.find(':', portSeparator + 1) != std::string::npos || authority.substr(portSeparator + 1) != "443") + return false; + host = authority.substr(0, portSeparator); + } + return host == "a.rtmps.youtube.com"; } -void autoConfig::StopThread(void) +static void trim(std::string &value) { - std::unique_lock ul(m); - cancel = true; - cv.notify_one(); + while (!value.empty() && std::isspace((unsigned char)value.back())) + value.pop_back(); + size_t first = 0; + while (first < value.size() && std::isspace((unsigned char)value[first])) + first++; + if (first) + value.erase(0, first); } -void autoConfig::InitializeAutoConfig(void *data, const int64_t id, const std::vector &args, std::vector &rval) +static std::string normalizeTwitchBandwidthKey(std::string key) { - serverName = "Auto (Recommended)"; - server = "auto"; - - obs_output_t *streamOutput = OBS_service::getStreamingOutput(StreamServiceId::Main); - if (streamOutput) - OBS_service::setStreamingOutput(nullptr, StreamServiceId::Main); - - cancel = false; + trim(key); + const size_t queryPos = key.find('?'); + const std::string base = key.substr(0, queryPos); + std::vector retained; + + if (queryPos != std::string::npos) { + std::string query = key.substr(queryPos + 1); + size_t offset = 0; + while (offset <= query.size()) { + const size_t next = query.find('&', offset); + std::string item = query.substr(offset, next == std::string::npos ? std::string::npos : next - offset); + const size_t equals = item.find('='); + const std::string name = lowerCopy(item.substr(0, equals)); + if (!item.empty() && name != "bandwidthtest") + retained.push_back(item); + if (next == std::string::npos) + break; + offset = next + 1; + } + } - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - AUTO_DEBUG; + std::string result = base + "?"; + for (const auto &item : retained) { + result += item; + result += "&"; + } + result += "bandwidthtest=true"; + return result; } -void autoConfig::StartBandwidthTest(void *data, const int64_t id, const std::vector &args, std::vector &rval) +static std::string defaultEstimateReason(const std::string &topology, const LegRequest &leg) { - asyncTests[ThreadedTests::BandwidthTest] = std::async(std::launch::async, TestBandwidthThread); - - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - AUTO_DEBUG; + if (!leg.estimateReason.empty()) + return leg.estimateReason; + if (topology == "custom-rtmp") + return "custom_rtmp"; + if (topology == "cloud-multistream") + return "cloud_multistream"; + if (topology == "dual-output") + return "dual_output"; + if (topology == "enhanced-broadcasting") + return "enhanced_broadcasting"; + if (topology == "stream-shift") + return "stream_shift"; + if (topology == "mixed") + return "mixed_topology"; + return "non_twitch"; } -void autoConfig::StartStreamEncoderTest(void *data, const int64_t id, const std::vector &args, std::vector &rval) +static bool isKnownDisplay(const std::string &display) { - asyncTests[ThreadedTests::StreamEncoderTest] = std::async(std::launch::async, TestStreamEncoderThread); - - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - AUTO_DEBUG; + return display == "horizontal" || display == "vertical" || display == "both"; } -void autoConfig::StartRecordingEncoderTest(void *data, const int64_t id, const std::vector &args, std::vector &rval) +static bool isKnownPlatform(const std::string &platform) { - asyncTests[ThreadedTests::RecordingEncoderTest] = std::async(std::launch::async, TestRecordingEncoderThread); - - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - AUTO_DEBUG; + static const std::set known = {"twitch", "youtube", "facebook", "kick", "tiktok", "custom", "other"}; + return known.count(platform) != 0; } -void autoConfig::StartSaveStreamSettings(void *data, const int64_t id, const std::vector &args, std::vector &rval) +static bool isKnownTopology(const std::string &topology) { - asyncTests[ThreadedTests::SaveStreamSettings] = std::async(std::launch::async, SaveStreamSettings); - - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - AUTO_DEBUG; + static const std::set known = {"direct-single", "cloud-multistream", "custom-rtmp", "dual-output", + "enhanced-broadcasting", "stream-shift", "mixed"}; + return known.count(topology) != 0; } -void autoConfig::StartSaveSettings(void *data, const int64_t id, const std::vector &args, std::vector &rval) +static bool parseRequest(const std::string &json, Session &session, std::string &error) { - asyncTests[ThreadedTests::SaveSettings] = std::async(std::launch::async, SaveSettings); - - cancel = false; - - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - AUTO_DEBUG; -} + obs_data_t *root = obs_data_create_from_json(json.c_str()); + if (!root) { + error = "invalid_autoconfig_request_json"; + return false; + } -void autoConfig::StartCheckSettings(void *data, const int64_t id, const std::vector &args, std::vector &rval) -{ - bool sucess = CheckSettings(); + bool valid = true; + if ((int)obs_data_get_int(root, "schemaVersion") != kSchemaVersion) { + error = "unsupported_autoconfig_schema"; + valid = false; + } - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - rval.push_back(ipc::value((uint32_t)sucess)); - AUTO_DEBUG; -} + session.topology = obs_data_get_string(root, "topology"); + if (valid && !isKnownTopology(session.topology)) { + error = "invalid_autoconfig_topology"; + valid = false; + } -void autoConfig::StartSetDefaultSettings(void *data, const int64_t id, const std::vector &args, std::vector &rval) -{ - asyncTests[ThreadedTests::SetDefaultSettings] = std::async(std::launch::async, SetDefaultSettings); + obs_data_array_t *legs = obs_data_get_array(root, "legs"); + const size_t legCount = legs ? obs_data_array_count(legs) : 0; + if (valid && (legCount == 0 || legCount > 8)) { + error = "invalid_autoconfig_legs"; + valid = false; + } - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - AUTO_DEBUG; -} + std::set legIds; + for (size_t i = 0; valid && i < legCount; i++) { + obs_data_t *item = obs_data_array_item(legs, i); + LegRequest leg; + leg.legId = obs_data_get_string(item, "legId"); + leg.display = obs_data_get_string(item, "display"); + leg.estimateReason = obs_data_get_string(item, "estimateReason"); + + if (leg.legId.empty() || leg.legId.size() > 128 || !legIds.insert(leg.legId).second || !isKnownDisplay(leg.display)) { + error = "invalid_autoconfig_leg_identity"; + valid = false; + } -int EvaluateBandwidth(ServerInfo &server, bool &connected, bool &stopped, bool &success, bool &errorOnStop, OBSData &service_settings, OBSService &service, - OBSOutput &output, OBSData &vencoder_settings) -{ - connected = false; - stopped = false; - errorOnStop = false; + obs_data_t *current = obs_data_get_obj(item, "current"); + if (!current) { + error = "missing_autoconfig_current_settings"; + valid = false; + } else { + leg.current.width = (int)obs_data_get_int(current, "width"); + leg.current.height = (int)obs_data_get_int(current, "height"); + leg.current.fpsNum = (int)obs_data_get_int(current, "fpsNum"); + leg.current.fpsDen = (int)obs_data_get_int(current, "fpsDen"); + leg.current.bitrateKbps = (int)obs_data_get_int(current, "bitrateKbps"); + leg.current.encoderId = obs_data_get_string(current, "encoderId"); + leg.current.codec = obs_data_get_string(current, "codec"); + leg.current.preset = obs_data_get_string(current, "preset"); + if (leg.current.width < 64 || leg.current.width > 8192 || leg.current.height < 64 || leg.current.height > 8192 || + leg.current.fpsNum <= 0 || leg.current.fpsNum > 240000 || leg.current.fpsDen <= 0 || leg.current.fpsDen > 10000 || + leg.current.bitrateKbps < 0 || leg.current.bitrateKbps > 100000) { + error = "invalid_autoconfig_current_settings"; + valid = false; + } + obs_data_release(current); + } - obs_data_set_string(service_settings, "server", server.address.c_str()); - obs_service_update(service, service_settings); + obs_data_t *limits = obs_data_get_obj(item, "limits"); + if (limits) { + leg.limits.maxBitrateKbps = (int)obs_data_get_int(limits, "maxBitrateKbps"); + leg.limits.maxWidth = (int)obs_data_get_int(limits, "maxWidth"); + leg.limits.maxHeight = (int)obs_data_get_int(limits, "maxHeight"); + leg.limits.maxFpsNum = (int)obs_data_get_int(limits, "maxFpsNum"); + leg.limits.maxFpsDen = (int)obs_data_get_int(limits, "maxFpsDen"); + if (leg.limits.maxFpsNum > 0 && leg.limits.maxFpsDen <= 0) + leg.limits.maxFpsDen = 1; + obs_data_release(limits); + } - if (!obs_output_start(output)) - return -1; + obs_data_array_t *destinations = obs_data_get_array(item, "destinations"); + const size_t destinationCount = destinations ? obs_data_array_count(destinations) : 0; + if (destinationCount == 0 || destinationCount > 16) { + error = "invalid_autoconfig_destinations"; + valid = false; + } else { + for (size_t di = 0; di < destinationCount; di++) { + obs_data_t *destination = obs_data_array_item(destinations, di); + Destination parsed{lowerCopy(obs_data_get_string(destination, "platform"))}; + obs_data_release(destination); + if (!isKnownPlatform(parsed.platform)) { + error = "invalid_autoconfig_platform"; + valid = false; + break; + } + leg.destinations.push_back(std::move(parsed)); + } + } + if (destinations) + obs_data_array_release(destinations); - std::unique_lock ul(m); - if (cancel) { - ul.unlock(); - obs_output_force_stop(output); - return -1; + obs_data_release(item); + if (valid) + session.legs.push_back(std::move(leg)); } - if (!stopped && !connected) - cv.wait(ul); - if (cancel) { - ul.unlock(); - obs_output_force_stop(output); - return -1; + if (legs) + obs_data_array_release(legs); + + obs_data_array_t *probes = obs_data_get_array(root, "activeProbes"); + const size_t probeCount = probes ? obs_data_array_count(probes) : 0; + if (valid && probeCount > 16) { + error = "invalid_autoconfig_active_probes"; + valid = false; } - if (!connected) { - return -1; + std::set probeIds; + for (size_t i = 0; valid && i < probeCount; i++) { + obs_data_t *item = obs_data_array_item(probes, i); + ProbeRequest probe; + probe.probeId = obs_data_get_string(item, "probeId"); + probe.kind = obs_data_get_string(item, "kind"); + probe.legId = obs_data_get_string(item, "legId"); + probe.serviceName = obs_data_get_string(item, "serviceName"); + probe.server = obs_data_get_string(item, "server"); + probe.streamKey = obs_data_get_string(item, "streamKey"); + obs_data_release(item); + + if (probe.probeId.empty() || probe.probeId.size() > 128 || probe.legId.empty() || probe.legId.size() > 128 || + !probeIds.insert(probe.probeId).second) { + error = "invalid_autoconfig_probe_identity"; + valid = false; + break; + } + if (probe.kind == "twitch-standard") + probe.provider = "twitch"; + else if (probe.kind == "youtube-unbound") + probe.provider = "youtube"; + session.probes.push_back(std::move(probe)); } + if (probes) + obs_data_array_release(probes); + obs_data_release(root); - uint64_t t_start = os_gettime_ns(); + if (!valid) + return false; - //wait for start signal from output - cv.wait_for(ul, std::chrono::seconds(10)); - if (stopped) - return -1; - if (cancel) { - ul.unlock(); - obs_output_force_stop(output); - return -1; + // Active probing is deliberately default-deny. Credentials survive parsing + // only when the probe, upload leg, topology, and destination all agree. + const bool multipleDualOutputLegs = session.topology == "dual-output" && session.legs.size() > 1; + std::map probePairCounts; + for (const auto &probe : session.probes) + probePairCounts[probe.legId + "\n" + probe.provider]++; + + for (auto &probe : session.probes) { + const auto legIt = std::find_if(session.legs.begin(), session.legs.end(), [&](const LegRequest &leg) { return leg.legId == probe.legId; }); + const bool legFound = legIt != session.legs.end(); + const bool destinationFound = legFound && std::any_of(legIt->destinations.begin(), legIt->destinations.end(), + [&](const Destination &destination) { return destination.platform == probe.provider; }); + const bool directEligible = session.topology == "direct-single" && session.legs.size() == 1 && legFound && legIt->destinations.size() == 1; + const bool dualEligible = session.topology == "dual-output" && session.legs.size() == 1 && legFound && legIt->destinations.size() == 1; + const bool cloudEligible = session.topology == "cloud-multistream" && session.legs.size() == 1 && legFound; + const bool providerValid = (probe.provider == "twitch" && probe.serviceName == "Twitch" && isOfficialTwitchServer(probe.server) && + isBoundedTwitchKey(probe.streamKey)) || + (probe.provider == "youtube" && probe.serviceName == "YouTube - RTMPS" && + isOfficialYoutubeRtmpsServer(probe.server) && isBoundedYoutubeKey(probe.streamKey)); + + probe.eligible = !probe.provider.empty() && destinationFound && (directEligible || dualEligible || cloudEligible) && providerValid && + probePairCounts[probe.legId + "\n" + probe.provider] == 1; + if (!probe.eligible) { + probe.denialReason = multipleDualOutputLegs ? "dual_output_multiple_active_legs" : "active_probe_not_eligible"; + probe.streamKey.clear(); + probe.server.clear(); + } + if (probe.eligible && probe.provider == "youtube") + session.probeConfirmations.emplace(probe.probeId, 0); } - obs_output_stop(output); - - while (!obs_output_active(output)) { - if (errorOnStop) { - ul.unlock(); - obs_output_force_stop(output); - return -1; + // A shared cloud upload is active-measured only when every Twitch/YouTube + // destination has exactly one eligible probe. Otherwise a partial provider + // sample could recommend a bitrate that is unsafe for the unmeasured peer. + if (session.topology == "cloud-multistream" && session.legs.size() == 1) { + const LegRequest &leg = session.legs.front(); + bool completeProviderSet = true; + for (const auto &destination : leg.destinations) { + if (destination.platform != "twitch" && destination.platform != "youtube") + continue; + const size_t eligibleCount = std::count_if(session.probes.begin(), session.probes.end(), [&](const ProbeRequest &probe) { + return probe.eligible && probe.legId == leg.legId && probe.provider == destination.platform; + }); + completeProviderSet = completeProviderSet && eligibleCount == 1; + } + if (!completeProviderSet) { + for (auto &probe : session.probes) { + if (probe.legId == leg.legId) { + probe.eligible = false; + probe.denialReason = "active_probe_set_incomplete"; + probe.streamKey.clear(); + probe.server.clear(); + } + } } - - std::this_thread::sleep_for(std::chrono::milliseconds(500)); } - //wait for stop signal from output - cv.wait(ul); + return true; +} + +static void pushEvent(const std::shared_ptr &session, const char *type, const char *phase, double progress, const std::string &code = {}, + const std::string &legId = {}, const std::string &measurementMode = {}, const std::string &probeId = {}, const std::string &provider = {}, + uint32_t targetBitrateKbps = 0) +{ + std::lock_guard lock(session->mutex); + SessionEvent event; + event.sequence = session->nextSequence++; + event.type = type; + event.phase = phase; + event.progress = progress; + event.code = code; + event.legId = legId; + event.measurementMode = measurementMode; + event.probeId = probeId; + event.provider = provider; + event.targetBitrateKbps = targetBitrateKbps; + session->events.push(std::move(event)); +} - uint64_t total_time = os_gettime_ns() - t_start; - int total_bytes = (int)obs_output_get_total_bytes(output); - uint64_t bitrate = 0; +static std::shared_ptr findSession(const std::string &id) +{ + std::lock_guard lock(sessionsMutex); + if (activeSession && activeSession->id == id) + return activeSession; + return nullptr; +} - if (total_time > 0) { - bitrate = (uint64_t)total_bytes * 8U * 1000000000U / total_time / 1000U; +static std::string resolveEncoderId(const std::string &id) +{ + if (id.empty()) + return {}; + // The software VideoToolbox implementation is not a hardware-capacity + // signal and Desktop cannot currently apply Apple encoder-family metadata. + // Registered hardware VideoToolbox IDs are still preserved when already + // selected, but this software implementation must never be recommended. + if (id == "com.apple.videotoolbox.videoencoder.h264") + return {}; + if (obs_get_encoder_codec(id.c_str())) + return id; + for (const auto &option : osn::EncoderUtils::videoEncoderOptions) { + if (option.simple_name == id) { + const std::string internal = osn::EncoderUtils::getInternalEncoderFromSimple(id.c_str()); + return osn::EncoderUtils::isEncoderRegistered(internal) ? internal : std::string{}; + } } + return {}; +} - startingBitrate = (int)obs_data_get_int(vencoder_settings, "bitrate"); - if (obs_output_get_frames_dropped(output) || (int)bitrate < (startingBitrate * 75 / 100)) { - server.bitrate = (int)bitrate * 70 / 100; - } else { - server.bitrate = startingBitrate; +static EncoderSelection chooseEncoder(const CurrentSettings ¤t) +{ + if (!resolveEncoderId(current.encoderId).empty()) + return {current.encoderId, false}; + + // Apple/VideoToolbox is intentionally not an automatic fallback. Desktop's + // current encoder metadata has no Apple family mapping, so returning one here + // would produce a recommendation it cannot apply. A currently selected and + // registered Apple encoder is still preserved by the branch above. + const char *preferred[] = {ENCODER_NVENC_H264_TEX, ADVANCED_ENCODER_QSV_V2, ADVANCED_ENCODER_QSV, ADVANCED_ENCODER_AMD, ADVANCED_ENCODER_X264}; + for (const char *candidate : preferred) { + if (candidate && osn::EncoderUtils::isEncoderRegistered(candidate)) + return {candidate, candidate != current.encoderId}; } + return {{}, !current.encoderId.empty()}; +} - server.ms = obs_output_get_connect_time_ms(output); - success = true; +static std::string scratchEncoderId(const std::string &recommendationId) +{ + const std::string encoder = resolveEncoderId(recommendationId); + if (encoder == ENCODER_NVENC_H264_TEX) + return "obs_nvenc_h264_soft"; + if (encoder == ENCODER_NVENC_HEVC_TEX) + return "obs_nvenc_hevc_soft"; + if (encoder == ENCODER_NVENC_AV1_TEX) + return "obs_nvenc_av1_soft"; + if (encoder == ADVANCED_ENCODER_QSV) + return "obs_qsv11_soft"; + if (encoder == ADVANCED_ENCODER_QSV_V2) + return "obs_qsv11_soft_v2"; + if (encoder == ADVANCED_ENCODER_QSV_AV1) + return "obs_qsv11_av1_soft"; + if (encoder == ADVANCED_ENCODER_QSV_HEVC) + return "obs_qsv11_hevc_soft"; + if (encoder == ADVANCED_ENCODER_AMD) + return "h264_fallback_amf"; + if (encoder == ADVANCED_ENCODER_AMD_HEVC) + return "h265_fallback_amf"; + if (encoder == ADVANCED_ENCODER_AMD_AV1) + return "av1_fallback_amf"; + return encoder; +} - //wait for deactivate signal from output - cv.wait(ul); +static bool isX264Preset(const std::string &preset) +{ + static const std::set supported = {"ultrafast", "superfast", "veryfast", "faster", "fast", + "medium", "slow", "slower", "veryslow", "placebo"}; + return supported.count(lowerCopy(preset)) != 0; +} - return 0; +static int offlinePlatformCapKbps(const std::string &platform) +{ + const char *serviceName = nullptr; + if (platform == "twitch") + serviceName = "Twitch"; + else if (platform == "youtube") + serviceName = "YouTube - RTMPS"; + else if (platform == "facebook") + serviceName = "Facebook Live"; + else + return 0; + + // This only loads the bundled rtmp-services metadata and invokes its encoder + // constraints. No output is created and no DNS/network operation is possible. + obs_data_t *serviceSettings = obs_data_create(); + obs_data_set_string(serviceSettings, "service", serviceName); + obs_service_t *service = obs_service_create_private("rtmp_common", "auto_optimizer_offline_cap", serviceSettings); + obs_data_release(serviceSettings); + if (!service) + return 0; + + constexpr int probeValue = 100000; + obs_data_t *encoderSettings = obs_data_create(); + obs_data_set_int(encoderSettings, "bitrate", probeValue); + obs_service_apply_encoder_settings(service, encoderSettings, nullptr); + const int value = (int)obs_data_get_int(encoderSettings, "bitrate"); + obs_data_release(encoderSettings); + obs_service_release(service); + return value > 0 && value < probeValue ? value : 0; } -void sendErrorMessage(const std::string &message) +static LegRequest withOfflinePlatformCaps(const LegRequest &input) { - eventsMutex.lock(); - events.push(AutoConfigInfo("error", message, 0)); - eventsMutex.unlock(); + LegRequest leg = input; + int strictest = 0; + for (const auto &destination : leg.destinations) { + const int cap = offlinePlatformCapKbps(destination.platform); + if (cap > 0 && (strictest == 0 || cap < strictest)) + strictest = cap; + } + if (strictest > 0 && (leg.limits.maxBitrateKbps == 0 || strictest < leg.limits.maxBitrateKbps)) + leg.limits.maxBitrateKbps = strictest; + return leg; } -void autoConfig::TestBandwidthThread(void) +static bool fitWithin(CurrentSettings &value, int maxWidth, int maxHeight) { - eventsMutex.lock(); - events.push(AutoConfigInfo("starting_step", "bandwidth_test", 0)); - eventsMutex.unlock(); + maxWidth = std::max(64, maxWidth); + maxHeight = std::max(64, maxHeight); + const double scale = std::min({1.0, (double)maxWidth / (double)value.width, (double)maxHeight / (double)value.height}); + const int width = std::max(64, ((int)std::floor((double)value.width * scale)) & ~1); + const int height = std::max(64, ((int)std::floor((double)value.height * scale)) & ~1); + const bool changed = width != value.width || height != value.height; + value.width = width; + value.height = height; + return changed; +} - bool connected = false; - bool stopped = false; - bool errorOnStop = false; - bool gotError = false; +static bool capFps(CurrentSettings &value, int maxNum, int maxDen) +{ + maxDen = maxDen > 0 ? maxDen : 1; + if ((int64_t)value.fpsNum * maxDen <= (int64_t)maxNum * value.fpsDen) + return false; + value.fpsNum = maxNum; + value.fpsDen = maxDen; + return true; +} - obs_video_info video = {0}; - bool have_users_info = obs_get_video_info(&video); +static void applyEncoderSelection(CurrentSettings &value, const EncoderSelection &selection) +{ + if (selection.id != value.encoderId) { + value.encoderId = selection.id; + // Presets are encoder-family-specific. Never carry a preset from an + // unavailable/failed encoder into its replacement; encoder defaults are + // safer than a syntactically valid preset for the wrong family. + value.preset.clear(); + const std::string internal = resolveEncoderId(value.encoderId); + const char *codec = internal.empty() ? nullptr : obs_get_encoder_codec(internal.c_str()); + value.codec = codec ? codec : "h264"; + } + if (value.codec.empty()) { + const std::string internal = resolveEncoderId(value.encoderId); + const char *codec = internal.empty() ? nullptr : obs_get_encoder_codec(internal.c_str()); + value.codec = codec ? codec : "h264"; + } +} - obs_video_info *ovi = obs_create_video_info(); +static CurrentSettings baseRecommendation(const LegRequest &leg) +{ + CurrentSettings value = leg.current; + if (value.bitrateKbps <= 0) + value.bitrateKbps = kDefaultEstimatedBitrateKbps; + if (leg.limits.maxBitrateKbps > 0) + value.bitrateKbps = std::min(value.bitrateKbps, leg.limits.maxBitrateKbps); + fitWithin(value, leg.limits.maxWidth > 0 ? leg.limits.maxWidth : value.width, leg.limits.maxHeight > 0 ? leg.limits.maxHeight : value.height); + if (leg.limits.maxFpsNum > 0) + capFps(value, leg.limits.maxFpsNum, leg.limits.maxFpsDen); + applyEncoderSelection(value, chooseEncoder(value)); + return value; +} - if (!have_users_info) { - video = *ovi; - video.fps_num = 60; - video.fps_den = 1; - } else { - video.fps_num = ovi->fps_num; - video.fps_den = ovi->fps_den; +static CurrentSettings estimateRecommendation(const LegRequest &leg, const HardwareAssessment &hardware) +{ + CurrentSettings value = baseRecommendation(leg); + if (hardware.attempted) { + value.width = hardware.value.width; + value.height = hardware.value.height; + value.fpsNum = hardware.value.fpsNum; + value.fpsDen = hardware.value.fpsDen; + applyEncoderSelection(value, {hardware.value.encoderId, hardware.value.encoderId != value.encoderId}); + value.preset = hardware.value.preset; } + return value; +} - video.base_width = 1280; - video.base_height = 720; - video.output_width = 128; - video.output_height = 128; - - int ret = obs_set_video_info(ovi, &video); - if (ret != OBS_VIDEO_SUCCESS) { - eventsMutex.lock(); - events.push(AutoConfigInfo("error", "invalid_video_settings", 0)); - eventsMutex.unlock(); - obs_remove_video_info(ovi); +static void putLimits(obs_data_t *parent, const Limits &limits) +{ + if (!limits.any()) return; + obs_data_t *obj = obs_data_create(); + if (limits.maxBitrateKbps > 0) + obs_data_set_int(obj, "maxBitrateKbps", limits.maxBitrateKbps); + if (limits.maxWidth > 0) + obs_data_set_int(obj, "maxWidth", limits.maxWidth); + if (limits.maxHeight > 0) + obs_data_set_int(obj, "maxHeight", limits.maxHeight); + if (limits.maxFpsNum > 0) { + obs_data_set_int(obj, "maxFpsNum", limits.maxFpsNum); + obs_data_set_int(obj, "maxFpsDen", limits.maxFpsDen > 0 ? limits.maxFpsDen : 1); + } + obs_data_set_obj(parent, "limits", obj); + obs_data_release(obj); +} + +static std::string serializeResult(const Session &session, const char *status, const std::vector &recommendations, + const std::string &errorCode = {}) +{ + obs_data_t *root = obs_data_create(); + obs_data_set_int(root, "schemaVersion", kSchemaVersion); + obs_data_set_string(root, "sessionId", session.id.c_str()); + obs_data_set_string(root, "status", status); + if (!errorCode.empty()) { + obs_data_t *error = obs_data_create(); + obs_data_set_string(error, "code", errorCode.c_str()); + obs_data_set_obj(root, "error", error); + obs_data_release(error); } - const char *serverType = "rtmp_common"; - - OBSEncoder vencoder = obs_video_encoder_create(ADVANCED_ENCODER_X264, "test_x264", nullptr, nullptr); - OBSEncoder aencoder = obs_audio_encoder_create("ffmpeg_aac", "test_aac", nullptr, 0, nullptr); - OBSService service = obs_service_create(serverType, "test_service", nullptr, nullptr); - OBSOutput output = obs_output_create("rtmp_output", "test_stream", nullptr, nullptr); - - /* -----------------------------------*/ - /* configure settings */ - - // service: "service", "server", "key" - // vencoder: "bitrate", "rate_control", - // obs_service_apply_encoder_settings - // aencoder: "bitrate" - // output: "bind_ip" via main config -> "Output", "BindIP" - // obs_output_set_service - - OBSData service_settings = obs_data_create(); - OBSData vencoder_settings = obs_data_create(); - OBSData aencoder_settings = obs_data_create(); - OBSData output_settings = obs_data_create(); - obs_data_release(service_settings); - obs_data_release(vencoder_settings); - obs_data_release(aencoder_settings); - obs_data_release(output_settings); - - obs_service_t *currentService = OBS_service::getService(StreamServiceId::Main); - if (currentService) { - obs_data_t *currentServiceSettings = obs_service_get_settings(currentService); - if (currentServiceSettings) { - if (serviceName.compare("") == 0) - serviceName = obs_data_get_string(currentServiceSettings, "service"); - - key = obs_service_get_connect_info(currentService, OBS_SERVICE_CONNECT_INFO_STREAM_KEY); - if (key.empty()) { - sendErrorMessage("invalid_stream_settings"); - gotError = true; + obs_data_array_t *legs = obs_data_array_create(); + for (const auto &recommendation : recommendations) { + obs_data_t *leg = obs_data_create(); + obs_data_set_string(leg, "legId", recommendation.legId.c_str()); + obs_data_set_string(leg, "display", recommendation.display.c_str()); + + obs_data_array_t *destinations = obs_data_array_create(); + for (const auto &destination : recommendation.destinations) { + obs_data_t *item = obs_data_create(); + obs_data_set_string(item, "platform", destination.platform.c_str()); + obs_data_array_push_back(destinations, item); + obs_data_release(item); + } + obs_data_set_array(leg, "destinations", destinations); + obs_data_array_release(destinations); + + obs_data_t *measurement = obs_data_create(); + obs_data_set_string(measurement, "mode", recommendation.measurementMode.c_str()); + obs_data_set_string(measurement, "confidence", recommendation.confidence.c_str()); + if (!recommendation.reason.empty()) + obs_data_set_string(measurement, "reason", recommendation.reason.c_str()); + if (!recommendation.probes.empty()) { + obs_data_array_t *probes = obs_data_array_create(); + for (const auto &provenance : recommendation.probes) { + obs_data_t *probe = obs_data_create(); + obs_data_set_string(probe, "provider", provenance.provider.c_str()); + obs_data_set_string(probe, "method", provenance.method.c_str()); + obs_data_set_bool(probe, "success", provenance.success); + if (probePolicy::hasProbeThroughputMetrics(provenance.success, provenance.measuredKbps)) { + obs_data_set_int(probe, "measuredKbps", (long long)provenance.measuredKbps); + obs_data_set_int(probe, "safeKbps", (long long)provenance.safeKbps); + } + if (provenance.success || provenance.headroomPercent > 0) + obs_data_set_int(probe, "headroomPercent", provenance.headroomPercent); + obs_data_set_bool(probe, "ceilingReached", provenance.ceilingReached); + obs_data_array_push_back(probes, probe); + obs_data_release(probe); } - } else { - sendErrorMessage("invalid_stream_settings"); - gotError = true; + obs_data_set_array(measurement, "probes", probes); + obs_data_array_release(probes); } - } else { - sendErrorMessage("invalid_stream_settings"); - gotError = true; + obs_data_set_obj(leg, "measurement", measurement); + obs_data_release(measurement); + + obs_data_t *value = obs_data_create(); + obs_data_set_int(value, "width", recommendation.value.width); + obs_data_set_int(value, "height", recommendation.value.height); + obs_data_set_int(value, "fpsNum", recommendation.value.fpsNum); + obs_data_set_int(value, "fpsDen", recommendation.value.fpsDen); + obs_data_set_int(value, "bitrateKbps", recommendation.value.bitrateKbps); + obs_data_set_string(value, "encoderId", recommendation.value.encoderId.c_str()); + obs_data_set_string(value, "codec", recommendation.value.codec.c_str()); + if (!recommendation.value.preset.empty()) + obs_data_set_string(value, "preset", recommendation.value.preset.c_str()); + obs_data_set_obj(leg, "recommendation", value); + obs_data_release(value); + + putLimits(leg, recommendation.limits); + obs_data_array_push_back(legs, leg); + obs_data_release(leg); } + obs_data_set_array(root, "legs", legs); + obs_data_array_release(legs); - if (gotError) { - obs_output_release(output); - obs_encoder_release(vencoder); - obs_encoder_release(aencoder); - obs_service_release(service); - obs_remove_video_info(ovi); - return; - } + std::string json = obs_data_get_json(root); + obs_data_release(root); + return json; +} - if (!customServer) { - if (serviceName == "Twitch") - serviceSelected = Service::Twitch; - else if (serviceName == "hitbox.tv") - serviceSelected = Service::Hitbox; - else if (serviceName == "beam.pro") - serviceSelected = Service::Beam; - else if (serviceName.find("YouTube") != std::string::npos) - serviceSelected = Service::YouTube; - else - serviceSelected = Service::Other; - } else { - serviceSelected = Service::Other; - } - std::string keyToEvaluate = key; +enum class ProbeStability { Stable, Degraded, Variable, Unstable }; - if (serviceSelected == Service::Twitch) { - string_depad_key(key); - keyToEvaluate += "?bandwidthtest"; +static const char *probeStabilityName(ProbeStability stability) +{ + switch (stability) { + case ProbeStability::Stable: + return "stable"; + case ProbeStability::Degraded: + return "degraded"; + case ProbeStability::Variable: + return "variable"; + case ProbeStability::Unstable: + return "unstable"; } + return "unknown"; +} - if (serviceSelected == Service::YouTube) { - serverName = "Stream URL"; - server = obs_service_get_connect_info(currentService, OBS_SERVICE_CONNECT_INFO_SERVER_URL); - } +struct ProbeResult { + bool success = false; + bool cancelled = false; + uint64_t measuredKbps = 0; + uint64_t safeKbps = 0; + int platformCapKbps = 0; + int headroomPercent = 0; + bool ceilingReached = false; + std::string provider; + std::string method; + std::string legId; + std::string errorCode; + ProbeStability stability = ProbeStability::Stable; + bool observedThroughputReliable = true; +}; - obs_data_set_string(service_settings, "service", serviceName.c_str()); - obs_data_set_string(service_settings, "key", keyToEvaluate.c_str()); +static bool silentAudioCallback(void *, uint64_t startTimestamp, uint64_t, uint64_t *outputTimestamp, uint32_t, struct audio_data_mixes_outputs *) +{ + *outputTimestamp = startTimestamp; + return true; +} - //Setting starting bitrate - OBSData service_settingsawd = obs_data_create(); - obs_data_release(service_settingsawd); +static float nextAudioNoiseSample(uint32_t &state) +{ + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + return ((float)(state & 0xffffU) / 32767.5f - 1.0f) * 0.25f; +} - obs_data_set_string(service_settingsawd, "service", serviceName.c_str()); +static bool noiseAudioCallback(void *param, uint64_t startTimestamp, uint64_t, uint64_t *outputTimestamp, uint32_t activeMixers, + struct audio_data_mixes_outputs *mixes) +{ + *outputTimestamp = startTimestamp; + if (!param || !mixes) + return false; - OBSService servicewad = obs_service_create(serverType, "temp_service", service_settingsawd, nullptr); - obs_service_release(servicewad); + uint32_t &state = *static_cast(param); + for (size_t canvas = 0; canvas < mixes->outputs.num; canvas++) { + for (size_t mix = 0; mix < MAX_AUDIO_MIXES; mix++) { + if (!(activeMixers & (1U << mix))) + continue; + + for (size_t channel = 0; channel < 2; channel++) { + float *plane = mixes->outputs.array[canvas].output[mix].data[channel]; + if (!plane) + continue; + for (size_t frame = 0; frame < AUDIO_OUTPUT_FRAMES; frame++) + plane[frame] = nextAudioNoiseSample(state); + } + } + } + return true; +} - int bitrate = 10000; +class ScratchResources { +public: + explicit ScratchResources(Session &session_, int stopTimeoutMs_ = kProbeStopTimeoutMs) : session(session_), stopTimeoutMs(stopTimeoutMs_) {} + ~ScratchResources() { cleanup(); } + + Session &session; + int stopTimeoutMs; + uint32_t videoWidth = 0; + uint32_t videoHeight = 0; + uint32_t videoFpsNum = 0; + uint32_t videoFpsDen = 1; + video_t *syntheticVideo = nullptr; + obs_view_t *scratchView = nullptr; + obs_core_video_mix_t *scratchMix = nullptr; + std::unique_ptr scratchViewInfo; + bool coreVideoMix = false; + audio_t *syntheticAudio = nullptr; + obs_encoder_t *videoEncoder = nullptr; + obs_encoder_t *audioEncoder = nullptr; + obs_service_t *service = nullptr; + obs_output_t *output = nullptr; + std::atomic stopFeeder{false}; + std::atomic scheduledFrames{0}; + std::atomic submittedFrames{0}; + std::atomic lockFailedFrames{0}; + std::atomic lateFrames{0}; + std::thread feeder; + std::vector framePatternA; + std::vector framePatternB; + uint32_t audioNoiseState = 0xa341316cU; + + bool createSyntheticVideo(uint32_t width, uint32_t height, uint32_t fpsNum, uint32_t fpsDen, bool useCoreVideoMix = false) + { + videoWidth = width; + videoHeight = height; + videoFpsNum = fpsNum; + videoFpsDen = fpsDen; + coreVideoMix = useCoreVideoMix; + if (coreVideoMix) { + // Hardware texture encoders require a real OBS video mix. Create an + // isolated, source-free view instead of attaching to any user canvas. + // obs_view_add2 creates only a private mix and does not reset the + // application's existing video contexts. + scratchViewInfo = std::make_unique(); + scratchViewInfo->base_width = width; + scratchViewInfo->base_height = height; + scratchViewInfo->output_width = width; + scratchViewInfo->output_height = height; + scratchViewInfo->fps_num = fpsNum; + scratchViewInfo->fps_den = fpsDen; + scratchViewInfo->fps_type = 1; + scratchViewInfo->output_format = VIDEO_FORMAT_NV12; + scratchViewInfo->colorspace = VIDEO_CS_709; + scratchViewInfo->range = VIDEO_RANGE_PARTIAL; + scratchViewInfo->scale_type = OBS_SCALE_BILINEAR; + scratchViewInfo->adapter = 0; + scratchViewInfo->gpu_conversion = true; + scratchView = obs_view_create(); + syntheticVideo = scratchView ? obs_view_add2(scratchView, scratchViewInfo.get()) : nullptr; + scratchMix = syntheticVideo ? obs_video_mix_get(scratchViewInfo.get(), OBS_MAIN_VIDEO_RENDERING) : nullptr; + if (!syntheticVideo || !scratchMix) { + if (scratchView) + obs_view_remove(scratchView); + if (scratchView) + obs_view_destroy(scratchView); + scratchView = nullptr; + scratchMix = nullptr; + scratchViewInfo.reset(); + return false; + } + return true; + } - OBSData settings = obs_data_create(); - obs_data_release(settings); - obs_data_set_int(settings, "bitrate", bitrate); - obs_service_apply_encoder_settings(servicewad, settings, nullptr); + video_output_info info{}; + info.name = "auto_optimizer_synthetic_video"; + info.format = VIDEO_FORMAT_NV12; + info.fps_num = fpsNum; + info.fps_den = fpsDen; + info.width = width; + info.height = height; + info.cache_size = 3; + info.colorspace = VIDEO_CS_709; + info.range = VIDEO_RANGE_PARTIAL; + if (video_output_open(&syntheticVideo, &info) != VIDEO_OUTPUT_SUCCESS) + return false; - int awstartingBitrate = (int)obs_data_get_int(settings, "bitrate"); - obs_data_set_int(vencoder_settings, "bitrate", awstartingBitrate); - obs_data_set_string(vencoder_settings, "rate_control", "CBR"); - obs_data_set_string(vencoder_settings, "preset", "veryfast"); - obs_data_set_int(vencoder_settings, "keyint_sec", 2); + const size_t frameBytes = (size_t)width * (size_t)height * 3U / 2U; + framePatternA.resize(frameBytes); + framePatternB.resize(frameBytes); + uint64_t random = 0x9e3779b97f4a7c15ULL ^ ((uint64_t)width << 32) ^ ((uint64_t)height << 16) ^ fpsNum; + for (size_t offset = 0; offset < frameBytes; offset++) { + random ^= random << 7; + random ^= random >> 9; + random ^= random << 8; + framePatternA[offset] = (uint8_t)random; + framePatternB[offset] = (uint8_t)(random >> 8) ^ (uint8_t)(offset * 31U); + } + return true; + } - obs_data_set_int(aencoder_settings, "bitrate", 32); + bool createSyntheticAudio(bool useNoise = false) + { + audio_output_info info{}; + info.name = "auto_optimizer_synthetic_audio"; + info.samples_per_sec = 48000; + info.format = AUDIO_FORMAT_FLOAT_PLANAR; + info.speakers = SPEAKERS_STEREO; + info.input_callback = useNoise ? noiseAudioCallback : silentAudioCallback; + info.input_param = useNoise ? &audioNoiseState : nullptr; + return audio_output_open(&syntheticAudio, &info) == AUDIO_OUTPUT_SUCCESS; + } - const char *bind_ip = config_get_string(ConfigManager::getInstance().getBasic(), "Output", "BindIP"); - obs_data_set_string(output_settings, "bind_ip", bind_ip); + void startFeeder() + { + if (coreVideoMix) + return; + feeder = std::thread([this]() { + const auto frameDuration = std::chrono::nanoseconds((1000000000ULL * videoFpsDen) / videoFpsNum); + auto nextFrame = std::chrono::steady_clock::now(); + uint64_t timestamp = os_gettime_ns(); + bool alternate = false; + while (!stopFeeder.load()) { + scheduledFrames.fetch_add(1, std::memory_order_relaxed); + video_frame frame{}; + if (video_output_lock_frame(syntheticVideo, &frame, 1, timestamp)) { + const std::vector &pattern = alternate ? framePatternB : framePatternA; + const uint8_t *luma = pattern.data(); + const uint8_t *chroma = pattern.data() + (size_t)videoWidth * videoHeight; + for (uint32_t y = 0; y < videoHeight; y++) + std::memcpy(frame.data[0] + y * frame.linesize[0], luma + (size_t)y * videoWidth, videoWidth); + for (uint32_t y = 0; y < videoHeight / 2; y++) + std::memcpy(frame.data[1] + y * frame.linesize[1], chroma + (size_t)y * videoWidth, videoWidth); + video_output_unlock_frame(syntheticVideo); + submittedFrames.fetch_add(1, std::memory_order_relaxed); + alternate = !alternate; + } else { + lockFailedFrames.fetch_add(1, std::memory_order_relaxed); + } + timestamp += (uint64_t)frameDuration.count(); + nextFrame += frameDuration; + const auto now = std::chrono::steady_clock::now(); + if (nextFrame < now) { + // Skip missed schedule slots instead of submitting a burst of + // catch-up frames that would distort encoder throughput. + lateFrames.fetch_add(1, std::memory_order_relaxed); + nextFrame = now + frameDuration; + timestamp = os_gettime_ns() + (uint64_t)frameDuration.count(); + } + std::this_thread::sleep_until(nextFrame); + } + }); + } - /* -----------------------------------*/ - /* determine which servers to test */ + void publishOutput() + { + std::lock_guard lock(session.probeMutex); + session.activeProbeOutput = output; + } - std::vector servers; - if (customServer) - servers.emplace_back(server.c_str(), server.c_str()); - else - GetServers(servers); + void cleanup() + { + if (output && obs_output_active(output)) { + obs_output_force_stop(output); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(stopTimeoutMs); + while (obs_output_active(output) && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + if (obs_output_active(output)) { + // Never release an active output or media objects it may still + // reference. The public cancel call has its own bounded wait and + // reports cleanup_timeout; this worker remains alive solely to + // finish safe teardown if OBS takes longer than expected. + while (obs_output_active(output)) { + obs_output_force_stop(output); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + } + } - /* just use the first server if it only has one alternate server */ - if (servers.size() < 3) - servers.resize(1); + { + std::lock_guard lock(session.probeMutex); + if (session.activeProbeOutput == output) + session.activeProbeOutput = nullptr; + if (output) { + obs_output_release(output); + output = nullptr; + } + } + stopFeeder.store(true); + if (feeder.joinable()) + feeder.join(); - /* -----------------------------------*/ - /* apply settings */ + if (videoEncoder) { + obs_encoder_release(videoEncoder); + videoEncoder = nullptr; + } + if (audioEncoder) { + obs_encoder_release(audioEncoder); + audioEncoder = nullptr; + } + if (syntheticVideo && !coreVideoMix) { + video_output_stop(syntheticVideo); + video_output_close(syntheticVideo); + syntheticVideo = nullptr; + } + if (scratchView) { + obs_view_remove(scratchView); + obs_view_destroy(scratchView); + scratchView = nullptr; + scratchMix = nullptr; + syntheticVideo = nullptr; + // The render thread removes orphaned mixes on its next tick. Keep + // the video-info snapshot alive until that tick has elapsed. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + scratchViewInfo.reset(); + } + if (syntheticAudio) { + audio_output_close(syntheticAudio); + syntheticAudio = nullptr; + } + if (service) { + obs_service_release(service); + service = nullptr; + } + } +}; - obs_service_update(service, service_settings); - obs_service_apply_encoder_settings(service, vencoder_settings, aencoder_settings); +static bool waitForOutputInactive(obs_output_t *output, int timeoutMs) +{ + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs); + while (output && obs_output_active(output) && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + return !output || !obs_output_active(output); +} - obs_encoder_update(vencoder, vencoder_settings); - obs_encoder_update(aencoder, aencoder_settings); +struct HardwareAttempt { + bool success = false; + bool cancelled = false; + bool timedOut = false; + uint32_t totalFrames = 0; + uint32_t skippedFrames = 0; + uint32_t encodedFrames = 0; + uint32_t scheduledFrames = 0; + uint32_t submittedFrames = 0; + uint32_t lockFailedFrames = 0; + uint32_t lateFrames = 0; + std::string errorCode; +}; - obs_encoder_set_video_mix(vencoder, obs_video_mix_get(ovi, OBS_MAIN_VIDEO_RENDERING)); - obs_encoder_set_audio(aencoder, obs_get_audio()); +static bool waitForScratchInterval(const std::shared_ptr &session, obs_output_t *output, std::chrono::steady_clock::time_point intervalDeadline, + std::chrono::steady_clock::time_point phaseDeadline, HardwareAttempt &result) +{ + while (std::chrono::steady_clock::now() < intervalDeadline) { + if (session->cancelRequested.load()) { + result.cancelled = true; + if (output) + obs_output_force_stop(output); + return false; + } + if (std::chrono::steady_clock::now() >= phaseDeadline) { + result.timedOut = true; + if (output) + obs_output_force_stop(output); + return false; + } + if (output && !obs_output_active(output)) { + result.errorCode = "hardware_benchmark_output_stopped"; + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + return true; +} - /* -----------------------------------*/ - /* connect encoders/services/outputs */ +static HardwareAttempt runEncoderWorkload(const std::shared_ptr &session, const CurrentSettings &candidate, + std::chrono::steady_clock::time_point phaseDeadline) +{ + HardwareAttempt result; + if (session->cancelRequested.load()) { + result.cancelled = true; + return result; + } + if (std::chrono::steady_clock::now() >= phaseDeadline) { + result.timedOut = true; + return result; + } - obs_output_set_video_encoder(output, vencoder); - obs_output_set_audio_encoder(output, aencoder, 0); + const std::string resolvedEncoderId = resolveEncoderId(candidate.encoderId); + const bool useCoreVideoMix = resolvedEncoderId != ADVANCED_ENCODER_X264; + const std::string encoderId = useCoreVideoMix ? resolvedEncoderId : scratchEncoderId(candidate.encoderId); + if (encoderId.empty() || !obs_get_encoder_codec(encoderId.c_str())) { + result.errorCode = "hardware_benchmark_encoder_unavailable"; + return result; + } - obs_output_update(output, output_settings); + ScratchResources resources(*session, kHardwareStopTimeoutMs); + if (!resources.createSyntheticVideo((uint32_t)candidate.width, (uint32_t)candidate.height, (uint32_t)candidate.fpsNum, (uint32_t)candidate.fpsDen, + useCoreVideoMix)) { + result.errorCode = "hardware_benchmark_video_create_failed"; + return result; + } + if (!resources.createSyntheticAudio()) { + result.errorCode = "hardware_benchmark_audio_create_failed"; + return result; + } - obs_output_set_service(output, service); + obs_data_t *encoderSettings = obs_data_create(); + obs_data_set_int(encoderSettings, "bitrate", std::clamp(candidate.bitrateKbps, 500, kProbeMaximumBitrateKbps)); + obs_data_set_string(encoderSettings, "rate_control", "CBR"); + obs_data_set_int(encoderSettings, "keyint_sec", 2); + if (encoderId == ADVANCED_ENCODER_X264 && isX264Preset(candidate.preset)) + obs_data_set_string(encoderSettings, "preset", candidate.preset.c_str()); + resources.videoEncoder = obs_video_encoder_create(encoderId.c_str(), "auto_optimizer_hardware_benchmark_encoder", encoderSettings, nullptr); + obs_data_release(encoderSettings); + if (!resources.videoEncoder) { + result.errorCode = "hardware_benchmark_encoder_create_failed"; + return result; + } + if (useCoreVideoMix) + obs_encoder_set_video_mix(resources.videoEncoder, resources.scratchMix); + else + obs_encoder_set_video(resources.videoEncoder, resources.syntheticVideo); + + obs_data_t *audioSettings = obs_data_create(); + obs_data_set_int(audioSettings, "bitrate", 32); + resources.audioEncoder = obs_audio_encoder_create("ffmpeg_aac", "auto_optimizer_hardware_benchmark_audio", audioSettings, 0, nullptr); + obs_data_release(audioSettings); + if (!resources.audioEncoder) { + result.errorCode = "hardware_benchmark_audio_encoder_create_failed"; + return result; + } + obs_encoder_set_audio(resources.audioEncoder, resources.syntheticAudio); - /* -----------------------------------*/ - /* connect signals */ + resources.output = obs_output_create("null_output", "auto_optimizer_hardware_benchmark_output", nullptr, nullptr); + if (!resources.output) { + result.errorCode = "hardware_benchmark_output_create_failed"; + return result; + } + obs_output_set_video_encoder(resources.output, resources.videoEncoder); + obs_output_set_audio_encoder(resources.output, resources.audioEncoder, 0); + resources.publishOutput(); + resources.startFeeder(); + + if (session->cancelRequested.load()) { + result.cancelled = true; + return result; + } + if (!obs_output_start(resources.output)) { + result.errorCode = "hardware_benchmark_start_failed"; + return result; + } - auto on_started = [&]() { - std::unique_lock lock(m); - connected = true; - stopped = false; - cv.notify_one(); - }; + const auto warmupDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(kHardwareWarmupMs); + if (!waitForScratchInterval(session, resources.output, warmupDeadline, phaseDeadline, result)) + return result; + + const uint32_t startTotal = video_output_get_total_frames(resources.syntheticVideo); + const uint32_t startSkipped = video_output_get_skipped_frames(resources.syntheticVideo); + const uint32_t startEncoded = obs_encoder_get_encoded_frames(resources.videoEncoder); + const uint32_t startScheduled = resources.scheduledFrames.load(std::memory_order_relaxed); + const uint32_t startSubmitted = resources.submittedFrames.load(std::memory_order_relaxed); + const uint32_t startLockFailed = resources.lockFailedFrames.load(std::memory_order_relaxed); + const uint32_t startLate = resources.lateFrames.load(std::memory_order_relaxed); + const auto sampleStart = std::chrono::steady_clock::now(); + const auto sampleDeadline = sampleStart + std::chrono::milliseconds(kHardwareSampleMs); + if (!waitForScratchInterval(session, resources.output, sampleDeadline, phaseDeadline, result)) + return result; + + const auto sampleEnd = std::chrono::steady_clock::now(); + result.totalFrames = video_output_get_total_frames(resources.syntheticVideo) - startTotal; + result.skippedFrames = video_output_get_skipped_frames(resources.syntheticVideo) - startSkipped; + result.encodedFrames = obs_encoder_get_encoded_frames(resources.videoEncoder) - startEncoded; + result.scheduledFrames = resources.scheduledFrames.load(std::memory_order_relaxed) - startScheduled; + result.submittedFrames = resources.submittedFrames.load(std::memory_order_relaxed) - startSubmitted; + result.lockFailedFrames = resources.lockFailedFrames.load(std::memory_order_relaxed) - startLockFailed; + result.lateFrames = resources.lateFrames.load(std::memory_order_relaxed) - startLate; + + obs_output_stop(resources.output); + if (!waitForOutputInactive(resources.output, kHardwareStopTimeoutMs)) { + obs_output_force_stop(resources.output); + if (!waitForOutputInactive(resources.output, kHardwareStopTimeoutMs)) { + result.errorCode = "hardware_benchmark_cleanup_timeout"; + return result; + } + } - auto on_stopped = [&]() { - const char *output_error = obs_output_get_last_error(output); + const double elapsedSeconds = std::chrono::duration(sampleEnd - sampleStart).count(); + const double requestedFps = (double)candidate.fpsNum / (double)candidate.fpsDen; + const uint32_t expectedFrames = (uint32_t)std::max(1.0, std::floor(requestedFps * elapsedSeconds)); + const uint32_t pipelineAllowance = std::min(4U, expectedFrames); + const uint32_t minimumEncoded = std::max(3U, (expectedFrames - pipelineAllowance) * 85U / 100U); + const uint32_t allowedSkipped = std::max(1U, result.totalFrames * 5U / 100U); + const uint32_t minimumSubmitted = std::max(3U, expectedFrames * 85U / 100U); + const uint32_t allowedLockFailed = std::max(1U, result.scheduledFrames * 5U / 100U); + const uint32_t allowedLate = std::max(1U, result.scheduledFrames * 5U / 100U); + const bool feederHealthy = useCoreVideoMix || (result.submittedFrames >= minimumSubmitted && result.lockFailedFrames <= allowedLockFailed && + result.lateFrames <= allowedLate); + result.success = feederHealthy && result.encodedFrames >= minimumEncoded && result.skippedFrames <= allowedSkipped; + if (!result.success) + result.errorCode = "hardware_benchmark_overloaded"; + return result; +} - if (output_error == nullptr) { - std::unique_lock lock(m); - connected = false; - stopped = true; - cv.notify_one(); - } else { - errorOnStop = true; - } - }; - - auto on_deactivate = [&]() { cv.notify_one(); }; +static bool sameHardwareWorkload(const CurrentSettings &left, const CurrentSettings &right) +{ + return left.width == right.width && left.height == right.height && left.fpsNum == right.fpsNum && left.fpsDen == right.fpsDen && + resolveEncoderId(left.encoderId) == resolveEncoderId(right.encoderId); +} - using on_started_t = decltype(on_started); - using on_stopped_t = decltype(on_stopped); - using on_deactivate_t = decltype(on_deactivate); +static CurrentSettings lowerHardwareCandidate(CurrentSettings value, int longEdge, int shortEdge) +{ + const bool landscape = value.width >= value.height; + fitWithin(value, landscape ? longEdge : shortEdge, landscape ? shortEdge : longEdge); + capFps(value, 30, 1); + return value; +} - auto pre_on_started = [](void *data, calldata_t *) { - on_started_t &on_started = *reinterpret_cast(data); - on_started(); - }; +static bool isInfrastructureFailure(const HardwareAttempt &attempt) +{ + return !attempt.success && !attempt.cancelled && attempt.errorCode != "hardware_benchmark_overloaded"; +} - auto pre_on_stopped = [](void *data, calldata_t *) { - on_stopped_t &on_stopped = *reinterpret_cast(data); - on_stopped(); - }; +static HardwareAssessment assessHardware(const std::shared_ptr &session, const LegRequest &leg, std::chrono::steady_clock::time_point phaseDeadline) +{ + HardwareAssessment assessment; + assessment.attempted = true; + const EncoderSelection initialSelection = chooseEncoder(leg.current); + CurrentSettings target = baseRecommendation(leg); + + const bool landscape = target.width >= target.height; + bool ceilingConstrained = fitWithin(target, landscape ? kHardwareMaximumLongEdge : kHardwareMaximumShortEdge, + landscape ? kHardwareMaximumShortEdge : kHardwareMaximumLongEdge); + ceilingConstrained = capFps(target, 60, 1) || ceilingConstrained; + if (target.encoderId.empty()) { + assessment.constrained = true; + assessment.reason = "hardware_no_usable_encoder"; + assessment.value = target; + return assessment; + } - auto pre_on_deactivate = [](void *data, calldata_t *) { - on_deactivate_t &on_deactivate = *reinterpret_cast(data); - on_deactivate(); + auto unavailable = [&](const HardwareAttempt &failedAttempt) { + assessment.passed = false; + assessment.constrained = true; + assessment.reason = failedAttempt.timedOut || std::chrono::steady_clock::now() >= phaseDeadline ? "hardware_benchmark_timeout" + : "hardware_benchmark_unavailable"; + // An infrastructure failure provides no evidence for a downgrade. Keep + // the capped current recommendation instead of returning an untested + // resolution or encoder. + assessment.value = target; }; - signal_handler *sh = obs_output_get_signal_handler(output); - signal_handler_connect(sh, "start", pre_on_started, &on_started); - signal_handler_connect(sh, "stop", pre_on_stopped, &on_stopped); - signal_handler_connect(sh, "deactivate", pre_on_deactivate, &on_deactivate); - - /* -----------------------------------*/ - /* test servers */ - - int bestBitrate = 0; - int bestMS = 0x7FFFFFFF; - std::string bestServer; - std::string bestServerName; - bool success = false; - - if (serverName.compare("") != 0) { - ServerInfo info(serverName.c_str(), server.c_str()); - - if (EvaluateBandwidth(info, connected, stopped, success, errorOnStop, service_settings, service, output, vencoder_settings) < 0) { - eventsMutex.lock(); - events.push(AutoConfigInfo("error", "invalid_stream_settings", 0)); - eventsMutex.unlock(); - gotError = true; - } else { - bestServer = info.address; - bestServerName = info.name; - bestBitrate = info.bitrate; + HardwareAttempt attempt = runEncoderWorkload(session, target, phaseDeadline); + if (attempt.cancelled) { + assessment.cancelled = true; + return assessment; + } + if (attempt.success) { + assessment.passed = true; + assessment.value = target; + assessment.constrained = initialSelection.replaced || ceilingConstrained; + if (initialSelection.replaced) + assessment.reason = "hardware_encoder_unavailable_fallback"; + else if (ceilingConstrained) + assessment.reason = "hardware_benchmark_ceiling"; + return assessment; + } + if (isInfrastructureFailure(attempt)) { + unavailable(attempt); + return assessment; + } - eventsMutex.lock(); - events.push(AutoConfigInfo("progress", "bandwidth_test", 100)); - eventsMutex.unlock(); + // A genuine overload is the only reason to test lower settings. Preserve the + // selected hardware encoder through the first downgrade before considering + // a software fallback. + CurrentSettings lowerSelected = lowerHardwareCandidate(target, 1280, 720); + if (!sameHardwareWorkload(target, lowerSelected) && std::chrono::steady_clock::now() < phaseDeadline) { + attempt = runEncoderWorkload(session, lowerSelected, phaseDeadline); + if (attempt.cancelled) { + assessment.cancelled = true; + return assessment; } - } else { - for (size_t i = 0; i < servers.size(); i++) { - EvaluateBandwidth(servers[i], connected, stopped, success, errorOnStop, service_settings, service, output, vencoder_settings); - eventsMutex.lock(); - events.push(AutoConfigInfo("progress", "bandwidth_test", (double)(i + 1) * 100 / servers.size())); - eventsMutex.unlock(); + if (attempt.success) { + assessment.passed = true; + assessment.constrained = true; + assessment.reason = "hardware_benchmark_resolution_fallback"; + assessment.value = lowerSelected; + return assessment; + } + if (isInfrastructureFailure(attempt)) { + unavailable(attempt); + return assessment; } } - if (!success && !gotError) { - eventsMutex.lock(); - events.push(AutoConfigInfo("error", "invalid_stream_settings", 0)); - eventsMutex.unlock(); - gotError = true; + CurrentSettings softwareCandidate = lowerSelected; + if (osn::EncoderUtils::isEncoderRegistered(ADVANCED_ENCODER_X264) && resolveEncoderId(softwareCandidate.encoderId) != ADVANCED_ENCODER_X264) { + applyEncoderSelection(softwareCandidate, {ADVANCED_ENCODER_X264, true}); + attempt = runEncoderWorkload(session, softwareCandidate, phaseDeadline); + if (attempt.cancelled) { + assessment.cancelled = true; + return assessment; + } + if (attempt.success) { + assessment.passed = true; + assessment.constrained = true; + assessment.reason = "hardware_benchmark_encoder_fallback"; + assessment.value = softwareCandidate; + return assessment; + } + if (isInfrastructureFailure(attempt)) { + unavailable(attempt); + return assessment; + } } - if (!gotError) { - for (auto &server : servers) { - bool close = abs(server.bitrate - bestBitrate) < 400; - - if ((!close && server.bitrate > bestBitrate) || (close && server.ms < bestMS)) { - bestServer = server.address; - bestServerName = server.name; - bestBitrate = server.bitrate; - bestMS = server.ms; - } + CurrentSettings conservative = lowerHardwareCandidate(softwareCandidate, 640, 360); + if (!sameHardwareWorkload(softwareCandidate, conservative) && std::chrono::steady_clock::now() < phaseDeadline) { + attempt = runEncoderWorkload(session, conservative, phaseDeadline); + if (attempt.cancelled) { + assessment.cancelled = true; + return assessment; + } + if (attempt.success) { + assessment.passed = true; + assessment.constrained = true; + assessment.reason = "hardware_benchmark_resolution_fallback"; + assessment.value = conservative; + return assessment; + } + if (isInfrastructureFailure(attempt)) { + unavailable(attempt); + return assessment; } - server = bestServer; - serverName = bestServerName; - idealBitrate = bestBitrate; } - obs_output_release(output); - obs_encoder_release(vencoder); - obs_encoder_release(aencoder); - obs_service_release(service); - ret = obs_remove_video_info(ovi); - if (ret != OBS_VIDEO_SUCCESS) { - blog(LOG_ERROR, "[VIDEO_CANVAS] failed to remove video canvas %08X", ovi); - } + // No candidate passed. Do not present the last failed candidate as a safe + // recommendation; retain capped current settings and make the low-confidence + // outcome explicit to Desktop. + assessment.passed = false; + assessment.constrained = true; + assessment.reason = std::chrono::steady_clock::now() >= phaseDeadline || attempt.timedOut ? "hardware_benchmark_timeout" + : "hardware_benchmark_overloaded"; + assessment.value = target; + return assessment; +} - if (!gotError) { - eventsMutex.lock(); - events.push(AutoConfigInfo("stopping_step", "bandwidth_test", 100)); - eventsMutex.unlock(); +static bool waitForProbeInterval(const std::shared_ptr &session, obs_output_t *output, std::chrono::steady_clock::time_point deadline, + ProbeResult &result) +{ + while (std::chrono::steady_clock::now() < deadline) { + if (session->cancelRequested.load()) { + result.cancelled = true; + obs_output_force_stop(output); + return false; + } + if (!obs_output_active(output)) { + result.errorCode = result.provider + "_probe_disconnected"; + return false; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); } + return true; } -/* this is used to estimate the lower bitrate limit for a given - * resolution/fps. yes, it is a totally arbitrary equation that gets - * the closest to the expected values */ -static long double EstimateBitrateVal(int cx, int cy, int fps_num, int fps_den) +static bool waitForYoutubeIngestConfirmation(const std::shared_ptr &session, const ProbeRequest &probe, obs_output_t *output, + std::chrono::steady_clock::time_point deadline, double progress, ProbeResult &result) { - long fps = long((long double)fps_num / (long double)fps_den); - long double areaVal = pow((long double)(cx * cy), 0.85l); - return areaVal * sqrt(pow(fps, 1.1l)); + { + std::lock_guard lock(session->probeConfirmationMutex); + session->activeConfirmationProbeId = probe.probeId; + } + pushEvent(session, "progress", "bandwidth", progress, "youtube_probe_waiting_for_ingest", probe.legId, "active", probe.probeId, probe.provider); + std::unique_lock lock(session->probeConfirmationMutex); + const bool signalled = session->probeConfirmationCondition.wait_until(lock, deadline, [&]() { + const auto found = session->probeConfirmations.find(probe.probeId); + return session->cancelRequested.load() || found == session->probeConfirmations.end() || found->second != 0; + }); + if (session->cancelRequested.load()) { + result.cancelled = true; + lock.unlock(); + obs_output_force_stop(output); + return false; + } + const auto found = session->probeConfirmations.find(probe.probeId); + if (!signalled || found == session->probeConfirmations.end() || found->second == 0) { + result.errorCode = "youtube_probe_ingest_confirmation_timeout"; + return false; + } + if (found->second < 0) { + result.errorCode = "youtube_probe_ingest_not_received"; + return false; + } + return true; } -static long double EstimateMinBitrate(int cx, int cy, int fps_num, int fps_den) +struct YoutubeProbeSample { + int targetVideoKbps = 0; + uint64_t expectedAggregateKbps = 0; + uint64_t measuredAggregateKbps = 0; + uint64_t medianSubwindowAggregateKbps = 0; + uint64_t wholeWindowAggregateKbps = 0; + uint64_t sampleBytes = 0; + uint32_t frames = 0; + uint32_t droppedFrames = 0; + uint32_t congestionSamples = 0; + uint32_t congestionHighSamples = 0; + uint32_t congestionSevereSamples = 0; + float maximumCongestion = 0.0f; + float p95Congestion = 0.0f; + long long elapsedMs = 0; + probePolicy::YoutubeProbeSampleMetrics metrics; +}; + +static const char *youtubeSampleClassName(probePolicy::YoutubeProbeSampleClass sampleClass) { - long double val = EstimateBitrateVal((int)baseResolutionCX, (int)baseResolutionCY, 60, 1) / 5800.0l; - if (val < std::numeric_limits::epsilon() && val > -std::numeric_limits::epsilon()) { - return 0.0; + switch (sampleClass) { + case probePolicy::YoutubeProbeSampleClass::Clean: + return "clean"; + case probePolicy::YoutubeProbeSampleClass::Marginal: + return "marginal"; + case probePolicy::YoutubeProbeSampleClass::Hard: + return "hard"; } - - return EstimateBitrateVal(cx, cy, fps_num, fps_den) / val; + return "unknown"; } -static long double EstimateUpperBitrate(int cx, int cy, int fps_num, int fps_den) +static const char *youtubeBaselineDecisionName(probePolicy::YoutubeBaselineDecision decision) { - long double val = EstimateBitrateVal(1280, 720, 30, 1) / 3000.0l; - if (val < std::numeric_limits::epsilon() && val > -std::numeric_limits::epsilon()) { - return 0.0; + switch (decision) { + case probePolicy::YoutubeBaselineDecision::Clean: + return "clean"; + case probePolicy::YoutubeBaselineDecision::Impaired: + return "impaired"; + case probePolicy::YoutubeBaselineDecision::NeedsThird: + return "needs_third"; + case probePolicy::YoutubeBaselineDecision::Unstable: + return "unstable"; } - - return EstimateBitrateVal(cx, cy, fps_num, fps_den) / val; + return "unknown"; } -struct Result { - int cx; - int cy; - int fps_num; - int fps_den; - - inline Result(int cx_, int cy_, int fps_num_, int fps_den_) : cx(cx_), cy(cy_), fps_num(fps_num_), fps_den(fps_den_) {} -}; - -void autoConfig::FindIdealHardwareResolution() +static const char *youtubeConfirmationDecisionName(probePolicy::YoutubeConfirmationDecision decision) { - int baseCX = (int)baseResolutionCX; - int baseCY = (int)baseResolutionCY; - - std::vector results; - - int pcores = os_get_physical_cores(); - int maxDataRate; - if (pcores >= 4) { - maxDataRate = int(baseResolutionCX * baseResolutionCY * 60 + 1000); - } else { - maxDataRate = 1280 * 720 * 30 + 1000; + switch (decision) { + case probePolicy::YoutubeConfirmationDecision::CapacityKnee: + return "capacity_knee"; + case probePolicy::YoutubeConfirmationDecision::TransientRecovered: + return "transient_recovered"; + case probePolicy::YoutubeConfirmationDecision::PathUnstable: + return "path_unstable"; + case probePolicy::YoutubeConfirmationDecision::Inconsistent: + return "inconsistent"; } + return "unknown"; +} - auto testRes = [&](long double div, int fps_num, int fps_den, bool force) { - if (results.size() >= 3) - return; - - if (!fps_num || !fps_den) { - fps_num = specificFPSNum; - fps_den = specificFPSDen; - } - - long double fps = ((long double)fps_num / (long double)fps_den); - - int cx = int((long double)baseCX / div); - int cy = int((long double)baseCY / div); - - long double rate = (long double)cx * (long double)cy * fps; - if (!force && rate > maxDataRate) - return; - - int minBitrate = int(EstimateMinBitrate(cx, cy, fps_num, fps_den) * 114 / 100); - if (type == Type::Recording) - force = true; - if (force || idealBitrate >= minBitrate) - results.emplace_back(cx, cy, fps_num, fps_den); - }; +static uint64_t medianValue(std::vector values) +{ + if (values.empty()) + return 0; + std::sort(values.begin(), values.end()); + return values[(values.size() - 1) / 2]; +} - if (specificFPSNum && specificFPSDen) { - testRes(1.0, 0, 0, false); - testRes(1.5, 0, 0, false); - testRes(1.0 / 0.6, 0, 0, false); - testRes(2.0, 0, 0, false); - testRes(2.25, 0, 0, true); - } else { - testRes(1.0, 60, 1, false); - testRes(1.0, 30, 1, false); - testRes(1.5, 60, 1, false); - testRes(1.5, 30, 1, false); - testRes(1.0 / 0.6, 60, 1, false); - testRes(1.0 / 0.6, 30, 1, false); - testRes(2.0, 60, 1, false); - testRes(2.0, 30, 1, false); - testRes(2.25, 60, 1, false); - testRes(2.25, 30, 1, true); - } +static double youtubeProbeStepProgress(double slotStart, double slotEnd, size_t targetIndex, size_t targetCount, double fraction) +{ + const double measurementStart = std::min(slotStart + 2.0, slotEnd); + if (targetCount == 0 || slotEnd <= measurementStart) + return slotEnd; + const double position = ((double)std::min(targetIndex, targetCount - 1) + std::clamp(fraction, 0.0, 0.99)) / (double)targetCount; + return measurementStart + (slotEnd - measurementStart) * position; +} - int minArea = 960 * 540 + 1000; +static uint64_t estimatedYoutubeSampleBytes(int targetVideoKbps, int durationMs) +{ + if (targetVideoKbps <= 0 || durationMs <= 0) + return 0; + const uint64_t aggregateKbps = (uint64_t)targetVideoKbps + kYoutubeProbeAudioBitrateKbps; + const uint64_t payloadBytes = aggregateKbps * (uint64_t)durationMs / 8ULL; + return payloadBytes * kYoutubeProbeBudgetEstimatePercent / 100ULL; +} - if (!specificFPSNum && preferHighFPS && results.size() > 1) { - Result &result1 = results[0]; - Result &result2 = results[1]; +static uint64_t youtubeProbeBytesUsed(obs_output_t *output, uint64_t budgetStartBytes) +{ + const uint64_t currentBytes = output ? obs_output_get_total_bytes(output) : 0; + return currentBytes >= budgetStartBytes ? currentBytes - budgetStartBytes : 0; +} - if (result1.fps_num == 30 && result2.fps_num == 60) { - int nextArea = result2.cx * result2.cy; - if (nextArea >= minArea) - results.erase(results.begin()); +static bool youtubeSampleGroupFits(std::chrono::steady_clock::time_point deadline, uint64_t totalProbeBytes, int activeTarget, const std::vector &targets) +{ + long long requiredMs = kYoutubeProbeBudgetSlackMs; + uint64_t requiredBytes = 0; + int targetBeforeSample = activeTarget; + for (int target : targets) { + if (target <= 0) + return false; + int durationMs = kYoutubeProbeSampleMs; + if (target != targetBeforeSample) { + requiredMs += kYoutubeProbeSettleMs; + durationMs += kYoutubeProbeSettleMs; } + requiredMs += kYoutubeProbeSampleMs; + requiredBytes += estimatedYoutubeSampleBytes(target, durationMs); + targetBeforeSample = target; } - - Result result = results.front(); - idealResolutionCX = result.cx; - idealResolutionCY = result.cy; - - if (idealResolutionCX * idealResolutionCY > 1280 * 720) { - idealResolutionCX = 1280; - idealResolutionCY = 720; - } - - idealFPSNum = result.fps_num; - idealFPSDen = result.fps_den; + return std::chrono::steady_clock::now() + std::chrono::milliseconds(requiredMs) < deadline && totalProbeBytes + requiredBytes <= kYoutubeProbeMaxBytes; } -bool autoConfig::TestSoftwareEncoding() +static bool runYoutubeProbeSample(const std::shared_ptr &session, ScratchResources &resources, const ProbeRequest &probe, int requestedTarget, + int &activeTarget, std::chrono::steady_clock::time_point youtubeDeadline, uint64_t budgetStartBytes, + uint64_t &totalProbeBytes, double progress, const char *eventCode, YoutubeProbeSample &sample, ProbeResult &result) { - OBSEncoder vencoder = obs_video_encoder_create(ADVANCED_ENCODER_X264, "test_x264", nullptr, nullptr); - OBSEncoder aencoder = obs_audio_encoder_create("ffmpeg_aac", "test_aac", nullptr, 0, nullptr); - OBSOutput output = obs_output_create("null_output", "null", nullptr, nullptr); - - /* -----------------------------------*/ - /* configure settings */ - - OBSData aencoder_settings = obs_data_create(); - OBSData vencoder_settings = obs_data_create(); - obs_data_release(aencoder_settings); - obs_data_release(vencoder_settings); - obs_data_set_int(aencoder_settings, "bitrate", 32); - - if (type != Type::Recording) { - obs_data_set_int(vencoder_settings, "keyint_sec", 2); - obs_data_set_int(vencoder_settings, "bitrate", idealBitrate); - obs_data_set_string(vencoder_settings, "rate_control", "CBR"); - obs_data_set_string(vencoder_settings, "profile", "main"); - obs_data_set_string(vencoder_settings, "preset", "veryfast"); - } else { - obs_data_set_int(vencoder_settings, "crf", 20); - obs_data_set_string(vencoder_settings, "rate_control", "CRF"); - obs_data_set_string(vencoder_settings, "profile", "high"); - obs_data_set_string(vencoder_settings, "preset", "veryfast"); + totalProbeBytes = youtubeProbeBytesUsed(resources.output, budgetStartBytes); + if (totalProbeBytes >= kYoutubeProbeMaxBytes) { + result.errorCode = "youtube_probe_byte_budget_exhausted"; + return false; } - /* -----------------------------------*/ - /* apply settings */ - - obs_encoder_update(vencoder, vencoder_settings); - obs_encoder_update(aencoder, aencoder_settings); - - /* -----------------------------------*/ - /* connect encoders/services/outputs */ - - obs_output_set_video_encoder(output, vencoder); - obs_output_set_audio_encoder(output, aencoder, 0); - - /* -----------------------------------*/ - /* connect signals */ - - auto on_stopped = [&]() { - std::unique_lock lock(m); - cv.notify_one(); - }; - - using on_stopped_t = decltype(on_stopped); - - auto pre_on_stopped = [](void *data, calldata_t *) { - on_stopped_t &on_stopped = *reinterpret_cast(data); - on_stopped(); - }; - - signal_handler *sh = obs_output_get_signal_handler(output); - signal_handler_connect(sh, "deactivate", pre_on_stopped, &on_stopped); - - /* -----------------------------------*/ - /* calculate starting resolution */ - - int baseCX = int(baseResolutionCX); - int baseCY = int(baseResolutionCY); - - /* -----------------------------------*/ - /* calculate starting test rates */ - - int pcores = os_get_physical_cores(); - int lcores = os_get_logical_cores(); - int maxDataRate; - if (lcores > 8 || pcores > 4) { - /* superb */ - maxDataRate = int(baseResolutionCX * baseResolutionCY * 60 + 1000); - - } else if (lcores > 4 && pcores == 4) { - /* great */ - maxDataRate = int(baseResolutionCX * baseResolutionCY * 60 + 1000); - - } else if (pcores == 4) { - /* okay */ - maxDataRate = int(baseResolutionCX * baseResolutionCY * 30 + 1000); - - } else { - /* toaster */ - maxDataRate = 960 * 540 * 30 + 1000; + int target = requestedTarget; + const bool targetChanged = target != activeTarget; + if (targetChanged) { + obs_data_t *updatedSettings = obs_data_create(); + obs_data_set_int(updatedSettings, "bitrate", target); + obs_data_set_string(updatedSettings, "rate_control", "CBR"); + obs_data_set_string(updatedSettings, "preset", "veryfast"); + obs_data_set_int(updatedSettings, "keyint_sec", 2); + obs_service_apply_encoder_settings(resources.service, updatedSettings, nullptr); + target = (int)obs_data_get_int(updatedSettings, "bitrate"); + obs_encoder_update(resources.videoEncoder, updatedSettings); + obs_data_release(updatedSettings); + activeTarget = target; } - /* -----------------------------------*/ - /* perform tests */ - - std::vector results; - int i = 0; - int count = 1; - - obs_video_info *ovi = obs_create_video_info(); - - auto testRes = [&](long double div, int fps_num, int fps_den, bool force) { - int per = ++i * 100 / count; - - /* no need for more than 3 tests max */ - if (results.size() >= 3) - return true; - - if (!fps_num || !fps_den) { - fps_num = specificFPSNum; - fps_den = specificFPSDen; - } - - long double fps = ((long double)fps_num / (long double)fps_den); - - int cx = int((long double)baseCX / div); - int cy = int((long double)baseCY / div); - - if (!force && type != Type::Recording) { - int est = int(EstimateMinBitrate(cx, cy, fps_num, fps_den)); - if (est > idealBitrate) - return true; - } + if (target <= 0) { + result.errorCode = "youtube_probe_invalid_applied_target"; + return false; + } - long double rate = (long double)cx * (long double)cy * fps; - if (!force && rate > maxDataRate) - return true; + pushEvent(session, "progress", "bandwidth", progress, eventCode, probe.legId, "active", probe.probeId, probe.provider, (uint32_t)target); + blog(LOG_INFO, + "[Auto Optimizer][YouTube Probe] Starting sample: purpose=%s, requested_video_target=%d Kbps, " + "applied_video_target=%d Kbps", + eventCode, requestedTarget, target); - obs_video_info video = *ovi; - video.base_width = 1280; - video.base_height = 720; - video.output_width = cx; - video.output_height = cy; - video.output_format = VIDEO_FORMAT_NV12; - video.fps_num = fps_num; - video.fps_den = fps_den; - video.initialized = true; - int ret = obs_set_video_info(ovi, &video); - if (ret != OBS_VIDEO_SUCCESS) { - blog(LOG_ERROR, "[VIDEO_CANVAS] Failed to update video info %08X", ovi); + if (targetChanged) { + const auto settleDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(kYoutubeProbeSettleMs); + if (settleDeadline > youtubeDeadline) { + result.errorCode = "youtube_probe_sample_deadline_exhausted"; return false; } - - obs_encoder_set_audio(aencoder, obs_get_audio()); - - obs_encoder_update(vencoder, vencoder_settings); - obs_encoder_set_video_mix(vencoder, obs_video_mix_get(ovi, OBS_MAIN_VIDEO_RENDERING)); - - obs_output_set_audio_encoder(output, aencoder, 0); - obs_output_set_video_encoder(output, vencoder); - - std::unique_lock ul(m); - if (cancel) - return false; - - if (!obs_output_start(output)) { + if (!waitForProbeInterval(session, resources.output, settleDeadline, result)) { + blog(LOG_WARNING, "[Auto Optimizer][YouTube Probe] Sample %d Kbps stopped during settle: %s", target, + result.errorCode.empty() ? (result.cancelled ? "cancelled" : "unknown") : result.errorCode.c_str()); return false; } + } + totalProbeBytes = youtubeProbeBytesUsed(resources.output, budgetStartBytes); + if (totalProbeBytes >= kYoutubeProbeMaxBytes) { + result.errorCode = "youtube_probe_byte_budget_exhausted"; + return false; + } - cv.wait_for(ul, std::chrono::seconds(5)); - - obs_output_stop(output); - cv.wait(ul); - - int skipped = (int)video_output_get_skipped_frames(obs_get_video()); - if (force || skipped <= 10) - results.emplace_back(cx, cy, fps_num, fps_den); - - return !cancel; - }; - - if (specificFPSNum && specificFPSDen) { - count = 5; - if (!testRes(1.0, 0, 0, false)) - return false; - if (!testRes(1.5, 0, 0, false)) - return false; - if (!testRes(1.0 / 0.6, 0, 0, false)) - return false; - if (!testRes(2.0, 0, 0, false)) - return false; - if (!testRes(2.25, 0, 0, true)) - return false; - } else { - count = 10; - if (!testRes(1.0, 60, 1, false)) - return false; - if (!testRes(1.0, 30, 1, false)) - return false; - if (!testRes(1.5, 60, 1, false)) - return false; - if (!testRes(1.5, 30, 1, false)) - return false; - if (!testRes(1.0 / 0.6, 60, 1, false)) - return false; - if (!testRes(1.0 / 0.6, 30, 1, false)) - return false; - if (!testRes(2.0, 60, 1, false)) - return false; - if (!testRes(2.0, 30, 1, false)) - return false; - if (!testRes(2.25, 60, 1, false)) + const uint64_t startBytes = obs_output_get_total_bytes(resources.output); + const uint32_t startDropped = obs_output_get_frames_dropped(resources.output); + const uint32_t startFrames = obs_output_get_total_frames(resources.output); + const auto sampleStart = std::chrono::steady_clock::now(); + const auto sampleDeadline = sampleStart + std::chrono::milliseconds(kYoutubeProbeSampleMs); + if (sampleDeadline > youtubeDeadline) { + result.errorCode = "youtube_probe_sample_deadline_exhausted"; + return false; + } + auto subwindowStart = sampleStart; + auto nextSubwindow = sampleStart + std::chrono::milliseconds(kYoutubeProbeSubwindowMs); + uint64_t subwindowStartBytes = startBytes; + std::vector subwindowThroughputs; + std::vector congestionValues; + + while (std::chrono::steady_clock::now() < sampleDeadline) { + const float congestion = obs_output_get_congestion(resources.output); + congestionValues.push_back(congestion); + const auto tickDeadline = std::min(std::chrono::steady_clock::now() + std::chrono::milliseconds(50), sampleDeadline); + if (!waitForProbeInterval(session, resources.output, tickDeadline, result)) { + blog(LOG_WARNING, "[Auto Optimizer][YouTube Probe] Sample %d Kbps stopped during measurement: %s", target, + result.errorCode.empty() ? (result.cancelled ? "cancelled" : "unknown") : result.errorCode.c_str()); return false; - if (!testRes(2.25, 30, 1, true)) + } + totalProbeBytes = youtubeProbeBytesUsed(resources.output, budgetStartBytes); + if (totalProbeBytes > kYoutubeProbeMaxBytes) { + result.errorCode = "youtube_probe_byte_budget_exhausted"; + blog(LOG_WARNING, "[Auto Optimizer][YouTube Probe] Sample %d Kbps exceeded byte budget: used=%llu, budget=%llu", target, + (unsigned long long)totalProbeBytes, (unsigned long long)kYoutubeProbeMaxBytes); return false; - } - - /* -----------------------------------*/ - /* find preferred settings */ - - int minArea = 960 * 540 + 1000; - - if (!specificFPSNum && preferHighFPS && results.size() > 1) { - Result &result1 = results[0]; - Result &result2 = results[1]; + } - if (result1.fps_num == 30 && result2.fps_num == 60) { - int nextArea = result2.cx * result2.cy; - if (nextArea >= minArea) - results.erase(results.begin()); + const auto now = std::chrono::steady_clock::now(); + if (now >= nextSubwindow || now >= sampleDeadline) { + const uint64_t subwindowEndBytes = obs_output_get_total_bytes(resources.output); + const uint64_t elapsedNs = (uint64_t)std::chrono::duration_cast(now - subwindowStart).count(); + if (elapsedNs > 0) { + const uint64_t bytes = subwindowEndBytes >= subwindowStartBytes ? subwindowEndBytes - subwindowStartBytes : 0; + subwindowThroughputs.push_back(bytes * 8ULL * 1000000000ULL / elapsedNs / 1000ULL); + } + subwindowStart = now; + subwindowStartBytes = subwindowEndBytes; + nextSubwindow += std::chrono::milliseconds(kYoutubeProbeSubwindowMs); } } - Result result = results.front(); - idealResolutionCX = result.cx; - idealResolutionCY = result.cy; - - if (idealResolutionCX * idealResolutionCY > 1280 * 720) { - idealResolutionCX = 1280; - idealResolutionCY = 720; + const auto sampleEnd = std::chrono::steady_clock::now(); + const uint64_t endBytes = obs_output_get_total_bytes(resources.output); + const uint32_t endDropped = obs_output_get_frames_dropped(resources.output); + const uint32_t endFrames = obs_output_get_total_frames(resources.output); + const uint64_t elapsedNs = (uint64_t)std::chrono::duration_cast(sampleEnd - sampleStart).count(); + if (endBytes <= startBytes || elapsedNs == 0 || subwindowThroughputs.empty()) { + result.errorCode = "youtube_probe_no_data"; + blog(LOG_WARNING, + "[Auto Optimizer][YouTube Probe] Sample %d Kbps produced no measurable data: start_bytes=%llu, end_bytes=%llu, " + "elapsed_ns=%llu, subwindows=%llu", + target, (unsigned long long)startBytes, (unsigned long long)endBytes, (unsigned long long)elapsedNs, + (unsigned long long)subwindowThroughputs.size()); + return false; } - idealFPSNum = result.fps_num; - idealFPSDen = result.fps_den; - - long double fUpperBitrate = EstimateUpperBitrate(result.cx, result.cy, result.fps_num, result.fps_den); - - int upperBitrate = int(floor(fUpperBitrate / 50.0l) * 50.0l); - - if (streamingEncoder != Encoder::x264) { - upperBitrate *= 114; - upperBitrate /= 100; + sample.targetVideoKbps = target; + sample.expectedAggregateKbps = (uint64_t)target + kYoutubeProbeAudioBitrateKbps; + sample.medianSubwindowAggregateKbps = medianValue(subwindowThroughputs); + sample.wholeWindowAggregateKbps = (endBytes - startBytes) * 8ULL * 1000000000ULL / elapsedNs / 1000ULL; + // The median filters isolated scheduler/network spikes, while the complete + // window catches multi-second stalls. Use the conservative value for both + // classification and recommendation. + sample.measuredAggregateKbps = std::min(sample.medianSubwindowAggregateKbps, sample.wholeWindowAggregateKbps); + sample.sampleBytes = endBytes - startBytes; + sample.frames = endFrames >= startFrames ? endFrames - startFrames : 0; + sample.droppedFrames = endDropped >= startDropped ? endDropped - startDropped : 0; + sample.elapsedMs = std::chrono::duration_cast(sampleEnd - sampleStart).count(); + sample.congestionSamples = (uint32_t)congestionValues.size(); + for (float congestion : congestionValues) { + sample.maximumCongestion = std::max(sample.maximumCongestion, congestion); + if (congestion >= kYoutubeProbeCongestionHigh) + sample.congestionHighSamples++; + if (congestion >= kYoutubeProbeCongestionSevere) + sample.congestionSevereSamples++; } - - if (idealBitrate > upperBitrate) - idealBitrate = upperBitrate; - - obs_output_release(output); - obs_encoder_release(vencoder); - obs_encoder_release(aencoder); - - int ret = obs_remove_video_info(ovi); - if (ret != OBS_VIDEO_SUCCESS) { - blog(LOG_ERROR, "[VIDEO_CANVAS] Failed to remove video info after TestSoftwareEncoding, %08X", ovi); + std::sort(congestionValues.begin(), congestionValues.end()); + if (!congestionValues.empty()) { + const size_t percentileIndex = std::min(congestionValues.size() - 1, (congestionValues.size() * 95 + 99) / 100 - 1); + sample.p95Congestion = congestionValues[percentileIndex]; } - - softwareTested = true; + sample.metrics = probePolicy::makeYoutubeProbeSampleMetrics((uint32_t)std::min(sample.measuredAggregateKbps, UINT32_MAX), + (uint32_t)std::min(sample.expectedAggregateKbps, UINT32_MAX), + sample.droppedFrames, sample.frames, sample.congestionHighSamples, + sample.congestionSevereSamples, sample.congestionSamples); + totalProbeBytes = youtubeProbeBytesUsed(resources.output, budgetStartBytes); + + const probePolicy::YoutubeProbeSampleClass sampleClass = probePolicy::classifyYoutubeProbeSample(sample.metrics); + blog(LOG_INFO, + "[Auto Optimizer][YouTube Probe] Sample result: purpose=%s, video_target=%d Kbps, expected_aggregate=%llu Kbps, " + "representative_aggregate=%llu Kbps, median_subwindow_aggregate=%llu Kbps, whole_window_aggregate=%llu Kbps, " + "elapsed=%lld ms, sample_bytes=%llu, frames=%u, " + "dropped=%u, max_congestion=%.3f, p95_congestion=%.3f, congestion_high=%u/%u, congestion_severe=%u/%u, " + "throughput_ratio=%.2f%%, drop_ratio=%.2f%%, congestion_high_ratio=%.2f%%, congestion_severe_ratio=%.2f%%, class=%s", + eventCode, target, (unsigned long long)sample.expectedAggregateKbps, (unsigned long long)sample.measuredAggregateKbps, + (unsigned long long)sample.medianSubwindowAggregateKbps, (unsigned long long)sample.wholeWindowAggregateKbps, sample.elapsedMs, + (unsigned long long)sample.sampleBytes, (unsigned int)sample.frames, (unsigned int)sample.droppedFrames, (double)sample.maximumCongestion, + (double)sample.p95Congestion, (unsigned int)sample.congestionHighSamples, (unsigned int)sample.congestionSamples, + (unsigned int)sample.congestionSevereSamples, (unsigned int)sample.congestionSamples, (double)sample.metrics.throughputBasisPoints / 100.0, + (double)sample.metrics.dropBasisPoints / 100.0, (double)sample.metrics.congestionHighBasisPoints / 100.0, + (double)sample.metrics.congestionSevereBasisPoints / 100.0, youtubeSampleClassName(sampleClass)); return true; } -void autoConfig::TestStreamEncoderThread() +static ProbeResult runRtmpProbe(const std::shared_ptr &session, ProbeRequest &probe, const LegRequest &leg, double slotStartProgress, + double slotEndProgress) { - eventsMutex.lock(); - events.push(AutoConfigInfo("starting_step", "streamingEncoder_test", 0)); - eventsMutex.unlock(); - - TestHardwareEncoding(); - - if (!softwareTested) { - if (!preferHardware || !hardwareEncodingAvailable) { - if (!TestSoftwareEncoding()) { - return; - } - } + ProbeResult result; + result.provider = probe.provider; + result.legId = probe.legId; + result.method = probe.provider == "youtube" ? "youtube-unbound-ramp" : "twitch-bandwidth-test"; + result.headroomPercent = 100 - (probe.provider == "youtube" ? kYoutubeProbeSafeMultiplierPercent : kTwitchProbeSafeMultiplierPercent); + ScratchResources resources(*session); + + obs_data_t *serviceSettings = obs_data_create(); + obs_data_set_string(serviceSettings, "service", probe.provider == "youtube" ? "YouTube - RTMPS" : "Twitch"); + obs_data_set_string(serviceSettings, "server", probe.server.c_str()); + std::string serviceKey = probe.provider == "youtube" ? probe.streamKey : normalizeTwitchBandwidthKey(probe.streamKey); + obs_data_set_string(serviceSettings, "key", serviceKey.c_str()); + resources.service = obs_service_create_private("rtmp_common", "auto_optimizer_probe_service", serviceSettings); + obs_data_release(serviceSettings); + + // Drop the only application-owned copies as soon as the disposable service + // has consumed them. Neither value is emitted, serialized, or logged. + probe.streamKey.clear(); + probe.server.clear(); + serviceKey.clear(); + if (!resources.service) { + result.errorCode = result.provider + "_probe_service_create_failed"; + return result; } - if (preferHardware && !softwareTested && hardwareEncodingAvailable) - FindIdealHardwareResolution(); - - if (!softwareTested) { - if (nvencAvailable) - streamingEncoder = Encoder::NVENC; - else if (qsvAvailable) - streamingEncoder = Encoder::QSV; - else if (vceAvailable) - streamingEncoder = Encoder::AMD; - // HW encoding seems to not be stable on Mac - // else if (appleHWAvailable) - // streamingEncoder = Encoder::appleHW; - } else { - streamingEncoder = Encoder::x264; + const int maximumBitrate = probe.provider == "youtube" ? kYoutubeProbeMaximumBitrateKbps : kProbeMaximumBitrateKbps; + const int requested = probe.provider == "youtube" ? kYoutubeProbeInitialBitrateKbps + : std::clamp(std::max(leg.current.bitrateKbps, 6000), 500, maximumBitrate); + obs_data_t *platformProbe = obs_data_create(); + obs_data_set_int(platformProbe, "bitrate", maximumBitrate); + obs_service_apply_encoder_settings(resources.service, platformProbe, nullptr); + const int platformReturned = (int)obs_data_get_int(platformProbe, "bitrate"); + if (platformReturned > 0 && platformReturned < maximumBitrate) + result.platformCapKbps = platformReturned; + obs_data_release(platformProbe); + + int initialBitrate = requested; + if (result.platformCapKbps > 0) + initialBitrate = std::min(initialBitrate, result.platformCapKbps); + if (leg.limits.maxBitrateKbps > 0) + initialBitrate = std::min(initialBitrate, leg.limits.maxBitrateKbps); + obs_data_t *encoderSettings = obs_data_create(); + obs_data_set_int(encoderSettings, "bitrate", initialBitrate); + obs_data_set_string(encoderSettings, "rate_control", "CBR"); + obs_data_set_string(encoderSettings, "preset", "veryfast"); + obs_data_set_int(encoderSettings, "keyint_sec", 2); + obs_service_apply_encoder_settings(resources.service, encoderSettings, nullptr); + initialBitrate = (int)obs_data_get_int(encoderSettings, "bitrate"); + + const uint32_t width = probe.provider == "youtube" ? 640 : 128; + const uint32_t height = probe.provider == "youtube" ? 360 : 128; + if (!resources.createSyntheticVideo(width, height, 30, 1)) { + obs_data_release(encoderSettings); + result.errorCode = result.provider + "_probe_video_create_failed"; + return result; + } + if (!resources.createSyntheticAudio(probe.provider == "youtube")) { + obs_data_release(encoderSettings); + result.errorCode = result.provider + "_probe_audio_create_failed"; + return result; } - eventsMutex.lock(); - events.push(AutoConfigInfo("stopping_step", "streamingEncoder_test", 100)); - eventsMutex.unlock(); -} - -void autoConfig::TestRecordingEncoderThread() -{ - eventsMutex.lock(); - events.push(AutoConfigInfo("starting_step", "recordingEncoder_test", 0)); - eventsMutex.unlock(); + resources.videoEncoder = obs_video_encoder_create(ADVANCED_ENCODER_X264, "auto_optimizer_probe_encoder", encoderSettings, nullptr); + obs_data_release(encoderSettings); + if (!resources.videoEncoder) { + result.errorCode = result.provider + "_probe_encoder_create_failed"; + return result; + } + obs_encoder_set_video(resources.videoEncoder, resources.syntheticVideo); + + obs_data_t *audioEncoderSettings = obs_data_create(); + obs_data_set_int(audioEncoderSettings, "bitrate", probe.provider == "youtube" ? kYoutubeProbeAudioBitrateKbps : kTwitchProbeAudioBitrateKbps); + resources.audioEncoder = obs_audio_encoder_create("ffmpeg_aac", "auto_optimizer_probe_audio_encoder", audioEncoderSettings, 0, nullptr); + obs_data_release(audioEncoderSettings); + if (!resources.audioEncoder) { + result.errorCode = result.provider + "_probe_audio_encoder_create_failed"; + return result; + } + obs_encoder_set_audio(resources.audioEncoder, resources.syntheticAudio); + resources.startFeeder(); - TestHardwareEncoding(); + resources.output = obs_output_create("rtmp_output", "auto_optimizer_probe_output", nullptr, nullptr); + if (!resources.output) { + result.errorCode = result.provider + "_probe_output_create_failed"; + return result; + } + obs_output_set_reconnect_settings(resources.output, 0, 0); + obs_output_set_video_encoder(resources.output, resources.videoEncoder); + obs_output_set_audio_encoder(resources.output, resources.audioEncoder, 0); + obs_output_set_service(resources.output, resources.service); + resources.publishOutput(); + + if (session->cancelRequested.load()) { + result.cancelled = true; + return result; + } + const auto probeStarted = std::chrono::steady_clock::now(); + const auto youtubeDeadline = probeStarted + std::chrono::milliseconds(kYoutubeProbeTotalTimeoutMs); + if (!obs_output_start(resources.output)) { + result.errorCode = result.provider + "_probe_start_failed"; + return result; + } - if (!hardwareEncodingAvailable && !softwareTested) { - if (!TestSoftwareEncoding()) { - return; + const auto connectDeadline = std::min(probeStarted + std::chrono::milliseconds(kProbeConnectTimeoutMs), youtubeDeadline); + while (!obs_output_active(resources.output) && std::chrono::steady_clock::now() < connectDeadline) { + if (session->cancelRequested.load()) { + result.cancelled = true; + obs_output_force_stop(resources.output); + return result; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + if (!obs_output_active(resources.output)) { + result.errorCode = result.provider + "_probe_connect_failed"; + return result; + } + const uint64_t probeBudgetStartBytes = obs_output_get_total_bytes(resources.output); + + if (probe.provider == "youtube") { + blog(LOG_INFO, "[Auto Optimizer][YouTube Probe] RTMP output is active; waiting for ingest confirmation"); + const auto confirmationDeadline = + std::min(std::chrono::steady_clock::now() + std::chrono::milliseconds(kYoutubeIngestConfirmationTimeoutMs), youtubeDeadline); + const double confirmationProgress = std::min(slotStartProgress + 2.0, slotEndProgress); + if (!waitForYoutubeIngestConfirmation(session, probe, resources.output, confirmationDeadline, confirmationProgress, result)) { + blog(LOG_WARNING, "[Auto Optimizer][YouTube Probe] Ingest confirmation failed: reason=%s, output_active=%s", + result.errorCode.empty() ? (result.cancelled ? "cancelled" : "unknown") : result.errorCode.c_str(), + obs_output_active(resources.output) ? "true" : "false"); + return result; } + blog(LOG_INFO, "[Auto Optimizer][YouTube Probe] Ingest confirmed; starting bandwidth ladder"); } - if (type == Type::Recording && hardwareEncodingAvailable) - FindIdealHardwareResolution(); + if (probe.provider == "twitch") { + pushEvent(session, "progress", "bandwidth", probePolicy::probeSubstepProgress(slotStartProgress, slotEndProgress, 0, 1), + "twitch_probe_measuring", probe.legId, "active", probe.probeId, probe.provider, (uint32_t)std::max(0, initialBitrate)); + if (!waitForProbeInterval(session, resources.output, std::chrono::steady_clock::now() + std::chrono::milliseconds(kProbeWarmupMs), result)) + return result; + const uint64_t startBytes = obs_output_get_total_bytes(resources.output); + const auto sampleStart = std::chrono::steady_clock::now(); + const auto sampleDeadline = sampleStart + std::chrono::milliseconds(kProbeSampleMs); + while (std::chrono::steady_clock::now() < sampleDeadline && obs_output_get_total_bytes(resources.output) - startBytes < kProbeMaxBytes) { + if (!waitForProbeInterval(session, resources.output, std::chrono::steady_clock::now() + std::chrono::milliseconds(50), result)) + return result; + } + const auto sampleEnd = std::chrono::steady_clock::now(); + const uint64_t endBytes = obs_output_get_total_bytes(resources.output); + const uint64_t elapsedNs = (uint64_t)std::chrono::duration_cast(sampleEnd - sampleStart).count(); + if (endBytes <= startBytes || elapsedNs == 0) { + result.errorCode = "twitch_probe_no_data"; + return result; + } + result.measuredKbps = ((endBytes - startBytes) * 8ULL * 1000000000ULL) / elapsedNs / 1000ULL; + result.safeKbps = probePolicy::safeVideoKbps(result.measuredKbps, kTwitchProbeSafeMultiplierPercent, kTwitchProbeAudioBitrateKbps); + if (result.platformCapKbps > 0) + result.safeKbps = std::min(result.safeKbps, (uint64_t)result.platformCapKbps); + result.success = result.measuredKbps > 0; + } else { + const int ladder[] = {1000, 2000, 4000, 6000, 8000, 10000, 12000}; + uint64_t totalProbeBytes = youtubeProbeBytesUsed(resources.output, probeBudgetStartBytes); + probePolicy::YoutubeRampEvidence rampEvidence; + const int effectiveCeilingKbps = + probePolicy::effectiveProbeCeilingKbps(kYoutubeProbeMaximumBitrateKbps, result.platformCapKbps, leg.limits.maxBitrateKbps); + std::vector> plannedTargets; + int lastPlannedTarget = 0; + for (int ladderTarget : ladder) { + const int plannedTarget = std::min(ladderTarget, effectiveCeilingKbps); + if (plannedTarget > lastPlannedTarget) { + plannedTargets.emplace_back(ladderTarget, plannedTarget); + lastPlannedTarget = plannedTarget; + } + } + std::string terminationReason = "ladder_exhausted"; + int activeTarget = initialBitrate; + size_t confirmationEpisodes = 0; + bool measurementConclusive = false; + const auto rampStarted = std::chrono::steady_clock::now(); + const auto probeElapsedMs = std::chrono::duration_cast(rampStarted - probeStarted).count(); + const auto remainingMs = std::chrono::duration_cast(youtubeDeadline - rampStarted).count(); + blog(LOG_INFO, + "[Auto Optimizer][YouTube Probe] Adaptive ladder configuration: effective_video_ceiling=%d Kbps, settle=%d ms, " + "sample=%d ms, subwindow=%d ms, total_timeout=%d ms, probe_elapsed=%lld ms, remaining=%lld ms, byte_budget=%llu, " + "maximum_confirmation_episodes=%d", + effectiveCeilingKbps, kYoutubeProbeSettleMs, kYoutubeProbeSampleMs, kYoutubeProbeSubwindowMs, kYoutubeProbeTotalTimeoutMs, + (long long)probeElapsedMs, (long long)remainingMs, (unsigned long long)kYoutubeProbeMaxBytes, kYoutubeProbeMaximumConfirmationEpisodes); + + if (plannedTargets.empty()) { + terminationReason = "no_planned_targets"; + result.errorCode = "youtube_probe_no_passing_step"; + result.observedThroughputReliable = false; + } else { + const int baselineTarget = plannedTargets.front().second; + if (!youtubeSampleGroupFits(youtubeDeadline, totalProbeBytes, activeTarget, {baselineTarget, baselineTarget})) { + terminationReason = "budget_before_baseline"; + result.errorCode = "youtube_probe_baseline_budget_exhausted"; + result.observedThroughputReliable = false; + } else { + YoutubeProbeSample baselineFirst; + YoutubeProbeSample baselineSecond; + if (!runYoutubeProbeSample(session, resources, probe, baselineTarget, activeTarget, youtubeDeadline, probeBudgetStartBytes, + totalProbeBytes, + youtubeProbeStepProgress(slotStartProgress, slotEndProgress, 0, plannedTargets.size(), 0.15), + "youtube_probe_baseline", baselineFirst, result)) + return result; + if (!runYoutubeProbeSample(session, resources, probe, baselineTarget, activeTarget, youtubeDeadline, probeBudgetStartBytes, + totalProbeBytes, + youtubeProbeStepProgress(slotStartProgress, slotEndProgress, 0, plannedTargets.size(), 0.40), + "youtube_probe_baseline", baselineSecond, result)) + return result; + + probePolicy::YoutubeBaselineAssessment baseline = + probePolicy::assessYoutubeBaseline(baselineFirst.metrics, baselineSecond.metrics); + std::vector baselineThroughputs{baselineFirst.measuredAggregateKbps, baselineSecond.measuredAggregateKbps}; + bool usedThirdBaselineSample = false; + if (baseline.decision == probePolicy::YoutubeBaselineDecision::NeedsThird) { + totalProbeBytes = youtubeProbeBytesUsed(resources.output, probeBudgetStartBytes); + if (!youtubeSampleGroupFits(youtubeDeadline, totalProbeBytes, activeTarget, {baselineTarget})) { + terminationReason = "budget_before_third_baseline_sample"; + result.errorCode = "youtube_probe_baseline_confirmation_budget_exhausted"; + result.observedThroughputReliable = false; + } else { + YoutubeProbeSample baselineThird; + if (!runYoutubeProbeSample(session, resources, probe, baselineTarget, activeTarget, youtubeDeadline, + probeBudgetStartBytes, totalProbeBytes, + youtubeProbeStepProgress(slotStartProgress, slotEndProgress, 0, + plannedTargets.size(), 0.65), + "youtube_probe_baseline", baselineThird, result)) + return result; + baselineThroughputs.push_back(baselineThird.measuredAggregateKbps); + baseline = probePolicy::resolveYoutubeBaseline(baselineFirst.metrics, baselineSecond.metrics, + baselineThird.metrics); + usedThirdBaselineSample = true; + } + } - recordingQuality = Quality::High; + if (result.errorCode.empty()) { + const uint64_t baselineBasis = baselineThroughputs.size() == 2 + ? std::min(baselineThroughputs[0], baselineThroughputs[1]) + : medianValue(baselineThroughputs); + result.measuredKbps = baselineBasis; + blog(baseline.decision == probePolicy::YoutubeBaselineDecision::Unstable ? LOG_WARNING : LOG_INFO, + "[Auto Optimizer][YouTube Probe] Baseline decision: decision=%s, samples=%llu, " + "recommendation_basis=%llu Kbps, throughput_reference=%.2f%%, drop_reference=%.2f%%, " + "congestion_high_reference=%.2f%%, congestion_severe_reference=%.2f%%", + youtubeBaselineDecisionName(baseline.decision), (unsigned long long)baselineThroughputs.size(), + (unsigned long long)baselineBasis, (double)baseline.reference.throughputBasisPoints / 100.0, + (double)baseline.reference.dropBasisPoints / 100.0, (double)baseline.reference.congestionHighBasisPoints / 100.0, + (double)baseline.reference.congestionSevereBasisPoints / 100.0); + + if (baseline.decision == probePolicy::YoutubeBaselineDecision::Unstable || + baseline.decision == probePolicy::YoutubeBaselineDecision::NeedsThird) { + terminationReason = "unstable_baseline"; + result.stability = ProbeStability::Unstable; + result.observedThroughputReliable = false; + result.errorCode = "youtube_probe_unstable_connection"; + } else { + if (baseline.decision == probePolicy::YoutubeBaselineDecision::Impaired) + result.stability = ProbeStability::Degraded; + else if (usedThirdBaselineSample) + result.stability = ProbeStability::Variable; + + rampEvidence.observe(baselineBasis, true); + YoutubeProbeSample lastAccepted = baselineFirst; + lastAccepted.targetVideoKbps = baselineTarget; + lastAccepted.expectedAggregateKbps = (uint64_t)baselineTarget + kYoutubeProbeAudioBitrateKbps; + lastAccepted.measuredAggregateKbps = baselineBasis; + lastAccepted.metrics = baseline.reference; + result.ceilingReached = probePolicy::reachedEffectiveProbeCeiling(baselineTarget, effectiveCeilingKbps); + if (result.ceilingReached) { + terminationReason = "effective_ceiling_reached_at_baseline"; + measurementConclusive = true; + } + + for (size_t targetIndex = 1; targetIndex < plannedTargets.size() && !result.ceilingReached; targetIndex++) { + const auto &[ladderTarget, plannedTarget] = plannedTargets[targetIndex]; + totalProbeBytes = youtubeProbeBytesUsed(resources.output, probeBudgetStartBytes); + if (!youtubeSampleGroupFits(youtubeDeadline, totalProbeBytes, activeTarget, {plannedTarget})) { + terminationReason = "budget_before_next_rung"; + break; + } + + YoutubeProbeSample highFirst; + if (!runYoutubeProbeSample(session, resources, probe, plannedTarget, activeTarget, youtubeDeadline, + probeBudgetStartBytes, totalProbeBytes, + youtubeProbeStepProgress(slotStartProgress, slotEndProgress, targetIndex, + plannedTargets.size(), 0.20), + "youtube_probe_measuring", highFirst, result)) + return result; + + if (probePolicy::youtubeSampleAccepted(highFirst.metrics, baseline)) { + rampEvidence.observe(highFirst.measuredAggregateKbps, true); + result.measuredKbps = rampEvidence.recommendationBasisKbps; + lastAccepted = highFirst; + result.ceilingReached = probePolicy::reachedEffectiveProbeCeiling(highFirst.targetVideoKbps, + effectiveCeilingKbps); + if (result.ceilingReached) { + terminationReason = "effective_ceiling_reached"; + measurementConclusive = true; + } + if (highFirst.targetVideoKbps < ladderTarget) { + terminationReason = "provider_or_request_cap_reached"; + measurementConclusive = true; + break; + } + continue; + } + + confirmationEpisodes++; + if (confirmationEpisodes > kYoutubeProbeMaximumConfirmationEpisodes) { + terminationReason = "confirmation_episode_limit_exceeded"; + result.stability = ProbeStability::Unstable; + result.observedThroughputReliable = false; + result.errorCode = "youtube_probe_unstable_connection"; + break; + } + totalProbeBytes = youtubeProbeBytesUsed(resources.output, probeBudgetStartBytes); + if (!youtubeSampleGroupFits(youtubeDeadline, totalProbeBytes, activeTarget, + {lastAccepted.targetVideoKbps, plannedTarget})) { + terminationReason = "confirmation_budget_exhausted_after_unconfirmed_failure"; + if (result.stability == ProbeStability::Stable) + result.stability = ProbeStability::Variable; + break; + } + + YoutubeProbeSample lowControl; + YoutubeProbeSample highRetry; + if (!runYoutubeProbeSample(session, resources, probe, lastAccepted.targetVideoKbps, activeTarget, + youtubeDeadline, probeBudgetStartBytes, totalProbeBytes, + youtubeProbeStepProgress(slotStartProgress, slotEndProgress, targetIndex, + plannedTargets.size(), 0.45), + "youtube_probe_confirming_stability", lowControl, result)) + return result; + if (!runYoutubeProbeSample(session, resources, probe, plannedTarget, activeTarget, youtubeDeadline, + probeBudgetStartBytes, totalProbeBytes, + youtubeProbeStepProgress(slotStartProgress, slotEndProgress, targetIndex, + plannedTargets.size(), 0.70), + "youtube_probe_retrying", highRetry, result)) + return result; + + const bool lowRecovered = + probePolicy::youtubeLowControlRecovered(lowControl.metrics, lastAccepted.metrics, baseline); + const bool highAccepted = probePolicy::youtubeSampleAccepted(highRetry.metrics, baseline); + const probePolicy::YoutubeConfirmationDecision confirmation = + probePolicy::decideYoutubeConfirmation(lowRecovered, highAccepted); + blog(LOG_INFO, + "[Auto Optimizer][YouTube Probe] Confirmation decision: episode=%llu, high_target=%d Kbps, " + "low_recovered=%s, high_retry_accepted=%s, decision=%s", + (unsigned long long)confirmationEpisodes, plannedTarget, lowRecovered ? "true" : "false", + highAccepted ? "true" : "false", youtubeConfirmationDecisionName(confirmation)); + + if (confirmation == probePolicy::YoutubeConfirmationDecision::CapacityKnee) { + const uint64_t confirmedBasis = + std::min(lastAccepted.measuredAggregateKbps, lowControl.measuredAggregateKbps); + rampEvidence.observe(confirmedBasis, true); + result.measuredKbps = rampEvidence.recommendationBasisKbps; + terminationReason = "confirmed_capacity_knee"; + measurementConclusive = true; + break; + } + if (confirmation == probePolicy::YoutubeConfirmationDecision::TransientRecovered) { + if (result.stability == ProbeStability::Stable) + result.stability = ProbeStability::Variable; + rampEvidence.observe(highRetry.measuredAggregateKbps, true); + result.measuredKbps = rampEvidence.recommendationBasisKbps; + lastAccepted = highRetry; + result.ceilingReached = probePolicy::reachedEffectiveProbeCeiling(highRetry.targetVideoKbps, + effectiveCeilingKbps); + if (result.ceilingReached) { + terminationReason = "effective_ceiling_reached_after_retry"; + measurementConclusive = true; + } + if (highRetry.targetVideoKbps < ladderTarget) { + terminationReason = "provider_or_request_cap_reached_after_retry"; + measurementConclusive = true; + break; + } + continue; + } + + terminationReason = confirmation == probePolicy::YoutubeConfirmationDecision::PathUnstable + ? "path_unstable" + : "inconsistent_confirmation"; + result.stability = ProbeStability::Unstable; + result.observedThroughputReliable = false; + result.errorCode = "youtube_probe_unstable_connection"; + break; + } + } + } + } + } - bool recordingOnly = type == Type::Recording; + if (result.stability != ProbeStability::Unstable && rampEvidence.passedStep && !measurementConclusive) { + result.observedThroughputReliable = false; + if (result.errorCode.empty()) + result.errorCode = "youtube_probe_inconclusive"; + } - if (hardwareEncodingAvailable) { - if (nvencAvailable) - recordingEncoder = Encoder::NVENC; - else if (qsvAvailable) - recordingEncoder = Encoder::QSV; - else if (vceAvailable) - recordingEncoder = Encoder::AMD; - // HW encoding seems to not be stable on Mac - // else if (appleHWAvailable) - // recordingEncoder = Encoder::appleHW; - } else { - recordingEncoder = Encoder::x264; + uint64_t uncappedSafeKbps = 0; + if (result.stability != ProbeStability::Unstable && rampEvidence.passedStep && measurementConclusive) { + uncappedSafeKbps = rampEvidence.safeVideoKbps(kYoutubeProbeSafeMultiplierPercent, kYoutubeProbeAudioBitrateKbps); + result.safeKbps = uncappedSafeKbps; + if (result.platformCapKbps > 0) + result.safeKbps = std::min(result.safeKbps, (uint64_t)result.platformCapKbps); + if (leg.limits.maxBitrateKbps > 0) + result.safeKbps = std::min(result.safeKbps, (uint64_t)leg.limits.maxBitrateKbps); + result.success = result.safeKbps > 0; + } else { + result.safeKbps = 0; + result.success = false; + if (result.errorCode.empty()) + result.errorCode = "youtube_probe_no_passing_step"; + } + blog(result.success ? LOG_INFO : LOG_WARNING, + "[Auto Optimizer][YouTube Probe] Adaptive ladder summary: passed_step=%s, stability=%s, " + "observed_throughput_reliable=%s, recommendation_basis=%llu Kbps, safe_multiplier=%d%%, audio_reserve=%d Kbps, " + "uncapped_safe_video=%llu Kbps, final_safe_video=%llu Kbps, platform_cap=%d Kbps, request_cap=%d Kbps, " + "total_output_bytes=%llu, ceiling_reached=%s, conclusive=%s, confirmation_episodes=%llu, termination=%s, error=%s", + rampEvidence.passedStep ? "true" : "false", probeStabilityName(result.stability), result.observedThroughputReliable ? "true" : "false", + (unsigned long long)rampEvidence.recommendationBasisKbps, kYoutubeProbeSafeMultiplierPercent, kYoutubeProbeAudioBitrateKbps, + (unsigned long long)uncappedSafeKbps, (unsigned long long)result.safeKbps, result.platformCapKbps, leg.limits.maxBitrateKbps, + (unsigned long long)totalProbeBytes, result.ceilingReached ? "true" : "false", measurementConclusive ? "true" : "false", + (unsigned long long)confirmationEpisodes, terminationReason.c_str(), result.errorCode.empty() ? "none" : result.errorCode.c_str()); } - if (recordingEncoder != Encoder::NVENC) { - if (!recordingOnly) { - recordingEncoder = Encoder::Stream; - recordingQuality = Quality::Stream; + obs_output_stop(resources.output); + if (!waitForOutputInactive(resources.output, kProbeStopTimeoutMs)) { + obs_output_force_stop(resources.output); + if (!waitForOutputInactive(resources.output, kProbeStopTimeoutMs)) { + result.success = false; + result.errorCode = result.provider + "_probe_cleanup_timeout"; + return result; } } - - eventsMutex.lock(); - events.push(AutoConfigInfo("stopping_step", "recordingEncoder_test", 100)); - eventsMutex.unlock(); + return result; } -inline const char *GetEncoderId(Encoder enc) +static void clearProbeSecrets(Session &session) { - switch (enc) { - case Encoder::NVENC: - return SIMPLE_ENCODER_NVENC; - case Encoder::QSV: - return SIMPLE_ENCODER_QSV; - case Encoder::AMD: - return SIMPLE_ENCODER_AMD; - case Encoder::Apple: - return SIMPLE_ENCODER_APPLE_H264; - default: - return SIMPLE_ENCODER_X264; + for (auto &probe : session.probes) { + probe.streamKey.clear(); + probe.server.clear(); } -}; +} -bool autoConfig::CheckSettings(void) +static void completeCancelled(const std::shared_ptr &session) { - OBSData settings = obs_data_create(); - - obs_data_set_string(settings, "service", serviceName.c_str()); - obs_data_set_string(settings, "server", server.c_str()); - - std::string testKey = key; - - if (serviceName.compare("Twitch") == 0) { - testKey += "?bandwidthtest"; + clearProbeSecrets(*session); + { + std::lock_guard lock(session->mutex); + session->resultJson = serializeResult(*session, "cancelled", {}, "cancelled"); } + session->state.store(SessionState::Cancelled); + pushEvent(session, "cancelled", "cleanup", 100, "cancelled"); +} - obs_data_set_string(settings, "key", testKey.c_str()); - - OBSService service = obs_service_create("rtmp_common", "serviceTest", settings, NULL); - - if (!service) { - eventsMutex.lock(); - events.push(AutoConfigInfo("error", "invalid_service", 100)); - eventsMutex.unlock(); - return false; +static void completeFailed(const std::shared_ptr &session, const char *code) +{ + clearProbeSecrets(*session); + { + std::lock_guard lock(session->mutex); + session->resultJson = serializeResult(*session, "failed", {}, code); } + session->state.store(SessionState::Failed); + pushEvent(session, "error", "cleanup", 100, code); + pushEvent(session, "complete", "cleanup", 100, code); +} - obs_video_info video = {0}; - bool have_users_info = obs_get_video_info(&video); - - obs_video_info *ovi = obs_create_video_info(); - - if (!have_users_info) { - video = *ovi; +static void runSession(const std::shared_ptr &session) +{ + pushEvent(session, "phase", "preflight", 0); + if (session->cancelRequested.load()) { + completeCancelled(session); + return; } - video.base_width = 1280; - video.base_height = 720; - video.output_width = (uint32_t)idealResolutionCX; - video.output_height = (uint32_t)idealResolutionCY; - video.fps_num = idealFPSNum; - video.fps_den = 1; - video.initialized = true; - int ret = obs_set_video_info(ovi, &video); - if (ret != OBS_VIDEO_SUCCESS) { - eventsMutex.lock(); - events.push(AutoConfigInfo("error", "invalid_video_settings", 100)); - eventsMutex.unlock(); - obs_remove_video_info(ovi); - return false; + pushEvent(session, "phase", "hardware", 15, "scratch_encoder_benchmark"); + const auto hardwareDeadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(kHardwarePhaseTimeoutMs); + std::vector preparedLegs; + std::vector hardwareAssessments; + preparedLegs.reserve(session->legs.size()); + hardwareAssessments.reserve(session->legs.size()); + for (size_t index = 0; index < session->legs.size(); index++) { + preparedLegs.push_back(withOfflinePlatformCaps(session->legs[index])); + hardwareAssessments.push_back(assessHardware(session, preparedLegs.back(), hardwareDeadline)); + const HardwareAssessment &assessment = hardwareAssessments.back(); + if (assessment.cancelled || session->cancelRequested.load()) { + completeCancelled(session); + return; + } + const std::string code = assessment.reason.empty() ? "hardware_benchmark_passed" : assessment.reason; + const double progress = 15.0 + (15.0 * (double)(index + 1) / (double)session->legs.size()); + pushEvent(session, "progress", "hardware", progress, code, preparedLegs.back().legId); } - OBSEncoder vencoder = obs_video_encoder_create(GetEncoderId(streamingEncoder), "test_encoder", nullptr, nullptr); - OBSEncoder aencoder = obs_audio_encoder_create("ffmpeg_aac", "test_aac", nullptr, 0, nullptr); - OBSOutput output = obs_output_create("rtmp_output", "test_stream", nullptr, nullptr); - - OBSData service_settings = obs_data_create(); - OBSData vencoder_settings = obs_data_create(); - OBSData aencoder_settings = obs_data_create(); - OBSData output_settings = obs_data_create(); - obs_data_release(service_settings); - obs_data_release(vencoder_settings); - obs_data_release(aencoder_settings); - obs_data_release(output_settings); - - obs_data_set_int(vencoder_settings, "bitrate", idealBitrate); - obs_data_set_string(vencoder_settings, "rate_control", "CBR"); - obs_data_set_string(vencoder_settings, "preset", "veryfast"); - obs_data_set_int(vencoder_settings, "keyint_sec", 2); - - obs_data_set_int(aencoder_settings, "bitrate", 32); - - /* -----------------------------------*/ - /* apply settings */ - - obs_service_apply_encoder_settings(service, vencoder_settings, aencoder_settings); - - obs_encoder_update(vencoder, vencoder_settings); - obs_encoder_update(aencoder, aencoder_settings); - - obs_encoder_set_video_mix(vencoder, obs_video_mix_get(ovi, OBS_MAIN_VIDEO_RENDERING)); - obs_encoder_set_audio(aencoder, obs_get_audio()); - - /* -----------------------------------*/ - /* connect encoders/services/outputs */ + std::vector probeResults; + const size_t eligibleProbeCount = + std::count_if(session->probes.begin(), session->probes.end(), [](const ProbeRequest &probe) { return probe.eligible; }); + size_t completedProbeCount = 0; + for (auto &probe : session->probes) { + if (!probe.eligible) { + pushEvent(session, "progress", "bandwidth", 30, probe.denialReason, probe.legId, "estimated", probe.probeId, probe.provider); + continue; + } + const auto legIt = std::find_if(preparedLegs.begin(), preparedLegs.end(), [&](const LegRequest &leg) { return leg.legId == probe.legId; }); + if (legIt == preparedLegs.end()) + continue; + const double startProgress = 30.0 + (35.0 * (double)completedProbeCount / (double)std::max(1, eligibleProbeCount)); + const double endProgress = 30.0 + (35.0 * (double)(completedProbeCount + 1) / (double)std::max(1, eligibleProbeCount)); + pushEvent(session, "phase", "bandwidth", startProgress, probe.provider + "_probe_started", probe.legId, "active", probe.probeId, + probe.provider); + const auto probeRunStarted = std::chrono::steady_clock::now(); + ProbeResult result = runRtmpProbe(session, probe, *legIt, startProgress, endProgress); + const auto probeRunElapsedMs = + std::chrono::duration_cast(std::chrono::steady_clock::now() - probeRunStarted).count(); + if (result.provider == "youtube") { + blog(result.success ? LOG_INFO : LOG_WARNING, + "[Auto Optimizer][YouTube Probe] Probe summary: success=%s, cancelled=%s, measured_aggregate=%llu Kbps, " + "safe_video=%llu Kbps, ceiling_reached=%s, stability=%s, observed_throughput_reliable=%s, elapsed=%lld ms, error=%s", + result.success ? "true" : "false", result.cancelled ? "true" : "false", (unsigned long long)result.measuredKbps, + (unsigned long long)result.safeKbps, result.ceilingReached ? "true" : "false", probeStabilityName(result.stability), + result.observedThroughputReliable ? "true" : "false", (long long)probeRunElapsedMs, + result.errorCode.empty() ? "none" : result.errorCode.c_str()); + } + if (probe.provider == "youtube") { + std::lock_guard lock(session->probeConfirmationMutex); + session->probeConfirmations.erase(probe.probeId); + if (session->activeConfirmationProbeId == probe.probeId) + session->activeConfirmationProbeId.clear(); + } + completedProbeCount++; + if (result.cancelled || session->cancelRequested.load()) { + completeCancelled(session); + return; + } + const std::string completionCode = result.success ? result.provider + "_probe_completed" + : result.stability == ProbeStability::Unstable ? result.provider + "_probe_unstable_estimate_used" + : result.provider + "_probe_failed_estimate_used"; + pushEvent(session, "progress", "bandwidth", endProgress, completionCode, result.legId, result.success ? "active" : "estimated", probe.probeId, + probe.provider); + probeResults.push_back(std::move(result)); + } + clearProbeSecrets(*session); + if (eligibleProbeCount == 0) + pushEvent(session, "progress", "bandwidth", 65, "estimate_only", {}, "estimated"); - obs_output_set_video_encoder(output, vencoder); - obs_output_set_audio_encoder(output, aencoder, 0); + if (session->cancelRequested.load()) { + completeCancelled(session); + return; + } - obs_output_update(output, output_settings); + pushEvent(session, "phase", "recommendation", 75); + std::vector recommendations; + for (size_t index = 0; index < preparedLegs.size(); index++) { + const LegRequest &leg = preparedLegs[index]; + const HardwareAssessment &hardware = hardwareAssessments[index]; + Recommendation recommendation; + recommendation.legId = leg.legId; + recommendation.display = leg.display; + recommendation.destinations = leg.destinations; + recommendation.limits = leg.limits; + recommendation.value = estimateRecommendation(leg, hardware); + recommendation.reason = defaultEstimateReason(session->topology, leg); + if (!hardware.passed || hardware.constrained) { + recommendation.confidence = hardware.passed ? "medium" : "low"; + recommendation.reason = hardware.reason; + } - obs_output_set_service(output, service); + const size_t requiredProbeCount = std::count_if(session->probes.begin(), session->probes.end(), + [&](const ProbeRequest &probe) { return probe.eligible && probe.legId == leg.legId; }); + std::vector legProbeResults; + for (const auto &probeResult : probeResults) { + if (probeResult.legId == leg.legId) + legProbeResults.push_back(&probeResult); + } + const bool allRequiredProbesPassed = + requiredProbeCount > 0 && legProbeResults.size() == requiredProbeCount && + std::all_of(legProbeResults.begin(), legProbeResults.end(), [](const ProbeResult *result) { return result->success; }); + for (const ProbeResult *result : legProbeResults) { + recommendation.probes.push_back({result->provider, result->method, result->measuredKbps, result->safeKbps, result->headroomPercent, + result->success, result->ceilingReached}); + } - /* -----------------------------------*/ - /* connect signals */ - bool success = true; + if (allRequiredProbesPassed) { + recommendation.measurementMode = "active"; + if (hardware.passed && session->topology == "cloud-multistream") { + recommendation.confidence = "medium"; + recommendation.reason = "indirect_provider_probes"; + } else if (hardware.passed && !hardware.constrained) { + recommendation.confidence = "high"; + recommendation.reason.clear(); + } + uint64_t safeKbps = UINT64_MAX; + for (const ProbeResult *result : legProbeResults) + safeKbps = std::min(safeKbps, result->safeKbps); + if (leg.limits.maxBitrateKbps > 0) + safeKbps = std::min(safeKbps, (uint64_t)leg.limits.maxBitrateKbps); + // Never turn a low measurement into a higher recommendation merely to + // satisfy a nominal bitrate floor. Surface the low-confidence result and + // let Desktop decide how to explain an insufficient connection. + const bool hasDegradedProbe = std::any_of(legProbeResults.begin(), legProbeResults.end(), + [](const ProbeResult *result) { return result->stability == ProbeStability::Degraded; }); + const bool hasVariableProbe = std::any_of(legProbeResults.begin(), legProbeResults.end(), + [](const ProbeResult *result) { return result->stability == ProbeStability::Variable; }); + if (hasDegradedProbe && recommendation.confidence != "low") { + recommendation.confidence = "low"; + recommendation.reason = "unstable_connection"; + } else if (hasVariableProbe && recommendation.confidence == "high") { + recommendation.confidence = "medium"; + recommendation.reason = "connection_variability_detected"; + } + if (safeKbps < 500) { + recommendation.confidence = "low"; + recommendation.reason = "insufficient_bandwidth"; + } + recommendation.value.bitrateKbps = (int)std::clamp(safeKbps, 1, kYoutubeProbeMaximumBitrateKbps); + } else if (requiredProbeCount > 0) { + recommendation.confidence = "low"; + const bool hasUnstableProbe = std::any_of(legProbeResults.begin(), legProbeResults.end(), + [](const ProbeResult *result) { return result->stability == ProbeStability::Unstable; }); + recommendation.reason = hasUnstableProbe ? "unstable_connection" + : session->topology == "cloud-multistream" ? "indirect_provider_probe_failed" + : "probe_failed"; + // A failed probe can still have trustworthy throughput evidence. + // Unstable observations are deliberately excluded: they describe a + // variable path, not a defensible bandwidth ceiling. + uint64_t observedSafeKbps = UINT64_MAX; + bool hasObservedThroughput = false; + for (const ProbeResult *result : legProbeResults) { + if (result->observedThroughputReliable && result->measuredKbps > 0 && result->safeKbps > 0) { + hasObservedThroughput = true; + observedSafeKbps = std::min(observedSafeKbps, result->safeKbps); + } + } + if (hasObservedThroughput) { + const uint64_t representableSafeKbps = std::max(1, observedSafeKbps); + recommendation.value.bitrateKbps = probePolicy::clampEstimateToObservedSafe( + recommendation.value.bitrateKbps, representableSafeKbps, kYoutubeProbeMaximumBitrateKbps); + if (observedSafeKbps < 500) + recommendation.reason = "insufficient_bandwidth"; + } + } - auto on_started = [&]() { - std::unique_lock lock(m); - success = true; - cv.notify_one(); - }; + recommendations.push_back(std::move(recommendation)); + } - auto on_stopped = [&]() { - std::unique_lock lock(m); - cv.notify_one(); - }; + { + std::lock_guard lock(session->mutex); + session->resultJson = serializeResult(*session, "complete", recommendations); + } + session->state.store(SessionState::Complete); + pushEvent(session, "result", "recommendation", 95); + pushEvent(session, "complete", "cleanup", 100); +} - auto on_deactivate = [&]() { cv.notify_one(); }; +static bool requestCancellation(const std::shared_ptr &session) +{ + std::unique_lock lifecycleLock(session->lifecycleMutex); + const SessionState state = session->state.load(); + if (state == SessionState::Created) { + session->cancelRequested.store(true); + completeCancelled(session); + return true; + } + if (state != SessionState::Running) + return true; - using on_started_t = decltype(on_started); - using on_stopped_t = decltype(on_stopped); - using on_deactivate_t = decltype(on_deactivate); + session->cancelRequested.store(true); + session->probeConfirmationCondition.notify_all(); + { + std::lock_guard lock(session->probeMutex); + if (session->activeProbeOutput) + obs_output_force_stop(session->activeProbeOutput); + } - auto pre_on_started = [](void *data, calldata_t *) { - on_started_t &on_started = *reinterpret_cast(data); - on_started(); - }; + if (session->worker.valid() && session->worker.wait_for(std::chrono::milliseconds(kCancelTimeoutMs)) != std::future_status::ready) { + pushEvent(session, "error", "cleanup", 100, "cleanup_timeout"); + return false; + } + return true; +} - auto pre_on_stopped = [](void *data, calldata_t *) { - on_stopped_t &on_stopped = *reinterpret_cast(data); - on_stopped(); - }; +} // namespace - auto pre_on_deactivate = [](void *data, calldata_t *) { - on_deactivate_t &on_deactivate = *reinterpret_cast(data); - on_deactivate(); - }; +void Register(ipc::server &srv) +{ + auto collection = std::make_shared("AutoConfig"); + + collection->register_function(std::make_shared("GetAutoConfigCapabilities", std::vector{}, GetCapabilities)); + collection->register_function(std::make_shared("CreateAutoConfigSession", std::vector{ipc::type::String}, CreateSession)); + collection->register_function(std::make_shared("StartAutoConfigSession", std::vector{ipc::type::String}, StartSession)); + collection->register_function(std::make_shared( + "ConfirmAutoConfigProbeIngest", std::vector{ipc::type::String, ipc::type::String, ipc::type::UInt32}, ConfirmProbeIngest)); + collection->register_function(std::make_shared("QueryAutoConfigSession", std::vector{ipc::type::String}, QuerySession)); + collection->register_function(std::make_shared("GetAutoConfigResult", std::vector{ipc::type::String}, GetResult)); + collection->register_function(std::make_shared("CancelAutoConfigSession", std::vector{ipc::type::String}, CancelSession)); + collection->register_function(std::make_shared("CloseAutoConfigSession", std::vector{ipc::type::String}, CloseSession)); + + srv.register_collection(collection); +} - signal_handler *sh = obs_output_get_signal_handler(output); - signal_handler_connect(sh, "start", pre_on_started, &on_started); - signal_handler_connect(sh, "stop", pre_on_stopped, &on_stopped); - signal_handler_connect(sh, "deactivate", pre_on_deactivate, &on_deactivate); +void GetCapabilities(void *, const int64_t, const std::vector &, std::vector &rval) +{ + static const char *capabilities = + R"({"apiVersion":2,"resultSchemaVersion":1,"previewApplySplit":true,"awaitableCancel":true,"perUploadLegResults":true,"desktopOwnedApply":true,"multipleActiveProbes":true,"bandwidthModes":["twitch-standard-active","youtube-unbound-active","estimate"]})"; + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + rval.push_back(ipc::value(capabilities)); +} - std::unique_lock ul(m); - if (!cancel) { - /* -----------------------------------*/ - /* start and wait to stop */ +void CreateSession(void *, const int64_t, const std::vector &args, std::vector &rval) +{ + if (args.size() != 1) { + returnError(rval, "CreateAutoConfigSession expects request JSON"); + return; + } + if (shuttingDown.load()) { + returnError(rval, "autoconfig_shutting_down"); + return; + } - if (!obs_output_start(output)) { - } else { - cv.wait_for(ul, std::chrono::seconds(4)); + auto session = std::make_shared(); + session->id = "autoconfig-" + std::to_string(os_gettime_ns()) + "-" + std::to_string(nextSessionId.fetch_add(1)); + std::string error; + if (!parseRequest(args[0].value_str, *session, error)) { + returnError(rval, error.c_str()); + return; + } - obs_output_stop(output); - //wait for the output to stop - cv.wait(ul); - //wait for the output to deactivate - cv.wait(ul); + { + std::lock_guard lock(sessionsMutex); + if (shuttingDown.load()) { + returnError(rval, "autoconfig_shutting_down"); + return; } - } else { - success = false; + if (activeSession) { + returnError(rval, "autoconfig_session_busy"); + return; + } + activeSession = session; } - obs_output_release(output); - obs_encoder_release(vencoder); - obs_encoder_release(aencoder); - obs_service_release(service); + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + rval.push_back(ipc::value(session->id)); +} - ret = obs_remove_video_info(ovi); - if (ret != OBS_VIDEO_SUCCESS) { - blog(LOG_ERROR, "[VIDEO_CANVAS] Failed to remove video info after CheckSettings, %08X", ovi); +void StartSession(void *, const int64_t, const std::vector &args, std::vector &rval) +{ + if (args.size() != 1) { + returnError(rval, "StartAutoConfigSession expects sessionId"); + return; + } + auto session = findSession(args[0].value_str); + if (!session) { + returnError(rval, "autoconfig_session_not_found"); + return; } - return success; + std::lock_guard lifecycleLock(session->lifecycleMutex); + SessionState expected = SessionState::Created; + if (!session->state.compare_exchange_strong(expected, SessionState::Running)) { + returnError(rval, "autoconfig_session_already_started"); + return; + } + try { + session->worker = std::async(std::launch::async, [session]() { + try { + runSession(session); + } catch (...) { + completeFailed(session, "autoconfig_worker_failed"); + } + }); + } catch (...) { + completeFailed(session, "autoconfig_worker_launch_failed"); + returnError(rval, "autoconfig_worker_launch_failed"); + return; + } + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); } -void autoConfig::SetDefaultSettings(void) +void ConfirmProbeIngest(void *, const int64_t, const std::vector &args, std::vector &rval) { - eventsMutex.lock(); - events.push(AutoConfigInfo("starting_step", "setting_default_settings", 0)); - eventsMutex.unlock(); - - idealResolutionCX = 1280; - idealResolutionCY = 720; - idealFPSNum = 30; - recordingQuality = Quality::High; - idealBitrate = 4500; - streamingEncoder = Encoder::x264; - recordingEncoder = Encoder::Stream; - - eventsMutex.lock(); - events.push(AutoConfigInfo("stopping_step", "setting_default_settings", 100)); - eventsMutex.unlock(); + if (args.size() != 3) { + returnError(rval, "ConfirmAutoConfigProbeIngest expects sessionId, probeId, and received"); + return; + } + auto session = findSession(args[0].value_str); + if (!session) { + returnError(rval, "autoconfig_session_not_found"); + return; + } + if (session->state.load() != SessionState::Running) { + returnError(rval, "autoconfig_session_not_running"); + return; + } + const std::string &probeId = args[1].value_str; + const auto probe = std::find_if(session->probes.begin(), session->probes.end(), [&](const ProbeRequest &candidate) { + return candidate.eligible && candidate.provider == "youtube" && candidate.probeId == probeId; + }); + if (probe == session->probes.end()) { + returnError(rval, "autoconfig_probe_not_found"); + return; + } + { + std::lock_guard lock(session->probeConfirmationMutex); + auto confirmation = session->probeConfirmations.find(probeId); + if (confirmation == session->probeConfirmations.end() || session->activeConfirmationProbeId != probeId) { + returnError(rval, "autoconfig_probe_not_confirmable"); + return; + } + confirmation->second = args[2].value_union.ui32 ? 1 : -1; + } + session->probeConfirmationCondition.notify_all(); + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); } -void autoConfig::SaveStreamSettings() +void QuerySession(void *, const int64_t, const std::vector &args, std::vector &rval) { - /* ---------------------------------- */ - /* save service */ - - eventsMutex.lock(); - events.push(AutoConfigInfo("starting_step", "saving_service", 0)); - eventsMutex.unlock(); - - const char *service_id = "rtmp_common"; - - obs_service_t *oldService = OBS_service::getService(StreamServiceId::Main); - OBSData hotkeyData = obs_hotkeys_save_service(oldService); - obs_data_release(hotkeyData); - - OBSData settings = obs_data_create(); - - if (!customServer) - obs_data_set_string(settings, "service", serviceName.c_str()); - obs_data_set_string(settings, "server", server.c_str()); - obs_data_set_string(settings, "key", key.c_str()); - - OBSService newService = obs_service_create(service_id, "default_service", settings, hotkeyData); - - if (!newService) + if (args.size() != 1) { + returnError(rval, "QueryAutoConfigSession expects sessionId"); return; + } + auto session = findSession(args[0].value_str); + if (!session) { + returnError(rval, "autoconfig_session_not_found"); + return; + } - OBS_service::setService(newService, StreamServiceId::Main); - OBS_service::saveService(); + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + std::lock_guard lock(session->mutex); + if (session->events.empty()) + return; - /* ---------------------------------- */ - /* save stream settings */ - config_set_int(ConfigManager::getInstance().getBasic(), "SimpleOutput", "VBitrate", idealBitrate); - config_set_string(ConfigManager::getInstance().getBasic(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder)); - config_remove_value(ConfigManager::getInstance().getBasic(), "SimpleOutput", "UseAdvanced"); + const SessionEvent &event = session->events.front(); + rval.push_back(ipc::value((uint32_t)kSchemaVersion)); + rval.push_back(ipc::value(session->id)); + rval.push_back(ipc::value(event.sequence)); + rval.push_back(ipc::value(event.type)); + rval.push_back(ipc::value(event.phase)); + rval.push_back(ipc::value(event.progress)); + rval.push_back(ipc::value(event.code)); + rval.push_back(ipc::value(event.legId)); + rval.push_back(ipc::value(event.measurementMode)); + rval.push_back(ipc::value(event.probeId)); + rval.push_back(ipc::value(event.provider)); + rval.push_back(ipc::value(event.targetBitrateKbps)); + session->events.pop(); +} - config_save_safe(ConfigManager::getInstance().getBasic(), "tmp", nullptr); +void GetResult(void *, const int64_t, const std::vector &args, std::vector &rval) +{ + if (args.size() != 1) { + returnError(rval, "GetAutoConfigResult expects sessionId"); + return; + } + auto session = findSession(args[0].value_str); + if (!session) { + returnError(rval, "autoconfig_session_not_found"); + return; + } + std::lock_guard lock(session->mutex); + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + rval.push_back(ipc::value(session->resultJson)); +} - eventsMutex.lock(); - events.push(AutoConfigInfo("stopping_step", "saving_service", 100)); - eventsMutex.unlock(); +void CancelSession(void *, const int64_t, const std::vector &args, std::vector &rval) +{ + if (args.size() != 1) { + returnError(rval, "CancelAutoConfigSession expects sessionId"); + return; + } + auto session = findSession(args[0].value_str); + if (!session) { + returnError(rval, "autoconfig_session_not_found"); + return; + } + if (!requestCancellation(session)) { + returnError(rval, "autoconfig_cleanup_timeout"); + return; + } + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); } -void autoConfig::SaveSettings() +void CloseSession(void *, const int64_t, const std::vector &args, std::vector &rval) { - eventsMutex.lock(); - events.push(AutoConfigInfo("starting_step", "saving_settings", 0)); - eventsMutex.unlock(); + if (args.size() != 1) { + returnError(rval, "CloseAutoConfigSession expects sessionId"); + return; + } + auto session = findSession(args[0].value_str); + if (!session) { + // Idempotent close: an already-closed/missing session is success. + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + return; + } - if (recordingEncoder != Encoder::Stream) - config_set_string(ConfigManager::getInstance().getBasic(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder)); + if (!requestCancellation(session)) { + returnError(rval, "autoconfig_cleanup_timeout"); + return; + } - const char *quality = recordingQuality == Quality::High ? "Small" : "Stream"; + { + std::lock_guard lifecycleLock(session->lifecycleMutex); + if (session->worker.valid() && session->worker.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) { + returnError(rval, "autoconfig_session_still_running"); + return; + } + session->state.store(SessionState::Closed); + } + { + std::lock_guard lock(sessionsMutex); + if (activeSession == session) + activeSession.reset(); + } + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); +} - config_set_string(ConfigManager::getInstance().getBasic(), "Output", "Mode", "Simple"); - config_set_string(ConfigManager::getInstance().getBasic(), "SimpleOutput", "RecQuality", quality); - config_set_int(ConfigManager::getInstance().getBasic(), "Video", "OutputCX", idealResolutionCX); - config_set_int(ConfigManager::getInstance().getBasic(), "Video", "OutputCY", idealResolutionCY); - config_set_int(ConfigManager::getInstance().getBasic(), "Video", "Canvases", 1); +void Shutdown() +{ + shuttingDown.store(true); + std::shared_ptr session; + { + std::lock_guard lock(sessionsMutex); + session = activeSession; + } + if (!session) + return; - config_set_bool(ConfigManager::getInstance().getBasic(), "Output", "DynamicBitrate", false); + { + std::unique_lock lifecycleLock(session->lifecycleMutex); + const SessionState state = session->state.load(); + if (state == SessionState::Created) { + session->cancelRequested.store(true); + completeCancelled(session); + } else if (state == SessionState::Running) { + session->cancelRequested.store(true); + session->probeConfirmationCondition.notify_all(); + { + std::lock_guard lock(session->probeMutex); + if (session->activeProbeOutput) + obs_output_force_stop(session->activeProbeOutput); + } + } - if (fpsType != FPSType::UseCurrent) { - config_set_uint(ConfigManager::getInstance().getBasic(), "Video", "FPSType", 0); - config_set_string(ConfigManager::getInstance().getBasic(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str()); + // Shutdown is the final safety barrier before libobs teardown. Unlike the + // public cancellation API, it must not continue while scratch resources + // are still owned by the worker, even if cleanup takes longer than the + // normal cancellation deadline. + if (session->worker.valid()) + session->worker.wait(); + clearProbeSecrets(*session); + session->state.store(SessionState::Closed); } - config_save_safe(ConfigManager::getInstance().getBasic(), "tmp", nullptr); - - eventsMutex.lock(); - events.push(AutoConfigInfo("stopping_step", "saving_settings", 100)); - events.push(AutoConfigInfo("done", "", 0)); - eventsMutex.unlock(); + { + std::lock_guard lock(sessionsMutex); + if (activeSession == session) + activeSession.reset(); + } } + +} // namespace autoConfig diff --git a/obs-studio-server/source/nodeobs_autoconfig.h b/obs-studio-server/source/nodeobs_autoconfig.h index c42a7ce93..c051b5aeb 100644 --- a/obs-studio-server/source/nodeobs_autoconfig.h +++ b/obs-studio-server/source/nodeobs_autoconfig.h @@ -1,57 +1,28 @@ /****************************************************************************** - Copyright (C) 2016-2019 by Streamlabs (General Workings Inc) + Copyright (C) 2026 by Streamlabs This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - ******************************************************************************/ #pragma once -#include -#include -#pragma once -#include -#include -#include -#include -#include -#include "nodeobs_api.h" -#include "nodeobs_service.h" + +#include namespace autoConfig { + void Register(ipc::server &srv); -void InitializeAutoConfig(void *data, const int64_t id, const std::vector &args, std::vector &rval); -void StartBandwidthTest(void *data, const int64_t id, const std::vector &args, std::vector &rval); -void StartStreamEncoderTest(void *data, const int64_t id, const std::vector &args, std::vector &rval); -void StartRecordingEncoderTest(void *data, const int64_t id, const std::vector &args, std::vector &rval); -void StartCheckSettings(void *data, const int64_t id, const std::vector &args, std::vector &rval); -void StartSetDefaultSettings(void *data, const int64_t id, const std::vector &args, std::vector &rval); -void StartSaveStreamSettings(void *data, const int64_t id, const std::vector &args, std::vector &rval); -void StartSaveSettings(void *data, const int64_t id, const std::vector &args, std::vector &rval); -void TerminateAutoConfig(void *data, const int64_t id, const std::vector &args, std::vector &rval); -void Query(void *data, const int64_t id, const std::vector &args, std::vector &rval); +void Shutdown(); + +void GetCapabilities(void *data, const int64_t id, const std::vector &args, std::vector &rval); +void CreateSession(void *data, const int64_t id, const std::vector &args, std::vector &rval); +void StartSession(void *data, const int64_t id, const std::vector &args, std::vector &rval); +void ConfirmProbeIngest(void *data, const int64_t id, const std::vector &args, std::vector &rval); +void QuerySession(void *data, const int64_t id, const std::vector &args, std::vector &rval); +void GetResult(void *data, const int64_t id, const std::vector &args, std::vector &rval); +void CancelSession(void *data, const int64_t id, const std::vector &args, std::vector &rval); +void CloseSession(void *data, const int64_t id, const std::vector &args, std::vector &rval); -void StopThread(); -void FindIdealHardwareResolution(); -bool TestSoftwareEncoding(); -void TestBandwidthThread(); -void TestStreamEncoderThread(); -void TestRecordingEncoderThread(); -void SaveStreamSettings(); -void SaveSettings(); -bool CheckSettings(); -void SetDefaultSettings(); -void TestHardwareEncoding(); -bool CanTestServer(const char *server); -void WaitPendingTests(double timeout = 10); -} // namespace autoConfig \ No newline at end of file +} // namespace autoConfig diff --git a/obs-studio-server/source/osn-advanced-replay-buffer.cpp b/obs-studio-server/source/osn-advanced-replay-buffer.cpp index 8a49f8cf3..9c2d329a0 100644 --- a/obs-studio-server/source/osn-advanced-replay-buffer.cpp +++ b/obs-studio-server/source/osn-advanced-replay-buffer.cpp @@ -22,6 +22,7 @@ #include "shared.hpp" #include "osn-audio-track.hpp" #include "osn-encoders.hpp" +#include void osn::IAdvancedReplayBuffer::Register(ipc::server &srv) { diff --git a/obs-studio-server/source/osn-advanced-streaming.cpp b/obs-studio-server/source/osn-advanced-streaming.cpp index 021d254d9..fbdc5633b 100644 --- a/obs-studio-server/source/osn-advanced-streaming.cpp +++ b/obs-studio-server/source/osn-advanced-streaming.cpp @@ -24,6 +24,7 @@ #include "nodeobs_audio_encoders.h" #include "osn-audio-track.hpp" #include "osn-encoders.hpp" +#include "osn-streaming-helpers.hpp" void osn::IAdvancedStreaming::Register(ipc::server &srv) { @@ -393,6 +394,43 @@ osn::AdvancedStreaming::~AdvancedStreaming() } } +void osn::AdvancedStreaming::checkOutput() +{ + const char *type = osn::streaming_helpers::getStreamOutputType(service); + if (!type) + type = "rtmp_output"; + + if (!GetOutput() || strcmp(obs_output_get_id(GetOutput()), type) != 0) + CreateOutput(type, "stream"); +} + +void osn::AdvancedStreaming::start() +{ + UpdateEncoders(); + + if (!setAudioEncoder(this)) + return; + + if (rescaling) + obs_encoder_set_scaled_size(videoEncoder, outputWidth, outputHeight); + + obs_output_set_video_encoder(GetOutput(), videoEncoder); + + if (enableTwitchVOD) { + twitchVODSupported = isTwitchVODSupported(); + if (twitchVODSupported) + SetupTwitchSoundtrackAudio(this); + } + + obs_output_set_service(GetOutput(), service); + + std::string outputSettingsError; + if (!ApplyOutputSettings(GetOutput(), outputSettingsError)) + return; + + StartOutput(); +} + void osn::IAdvancedStreaming::Start(void *data, const int64_t id, const std::vector &args, std::vector &rval) { AdvancedStreaming *streaming = static_cast(osn::IAdvancedStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); @@ -419,7 +457,7 @@ void osn::IAdvancedStreaming::Start(void *data, const int64_t id, const std::vec streaming->UpdateEncoders(); - const char *type = OBS_service::getStreamOutputType(streaming->service); + const char *type = osn::streaming_helpers::getStreamOutputType(streaming->service); if (!type) type = "rtmp_output"; diff --git a/obs-studio-server/source/osn-advanced-streaming.hpp b/obs-studio-server/source/osn-advanced-streaming.hpp index c8ca0442f..e94215a9f 100644 --- a/obs-studio-server/source/osn-advanced-streaming.hpp +++ b/obs-studio-server/source/osn-advanced-streaming.hpp @@ -53,6 +53,8 @@ class AdvancedStreaming : public Streaming { uint32_t outputHeight; void UpdateEncoders(); + void start() override; + void checkOutput() override; }; class IAdvancedStreaming : public IStreaming { diff --git a/obs-studio-server/source/osn-simple-recording.cpp b/obs-studio-server/source/osn-simple-recording.cpp index 2a872d376..996d48a33 100644 --- a/obs-studio-server/source/osn-simple-recording.cpp +++ b/obs-studio-server/source/osn-simple-recording.cpp @@ -24,6 +24,7 @@ #include "nodeobs_audio_encoders.h" #include "osn-file-output.hpp" #include "osn-encoders.hpp" +#include "osn-streaming-helpers.hpp" void osn::ISimpleRecording::Register(ipc::server &srv) { @@ -314,7 +315,7 @@ void osn::SimpleRecording::UpdateEncoders() case RecQuality::Stream: { if (!streaming) return; - streaming->UpdateEncoders(); + streaming->updateEncoders(); videoEncoder = streaming->videoEncoder; audioEncoder = streaming->audioEncoder; if (obs_get_multiple_rendering()) { diff --git a/obs-studio-server/source/osn-simple-replay-buffer.cpp b/obs-studio-server/source/osn-simple-replay-buffer.cpp index 3d18fa382..faed6df88 100644 --- a/obs-studio-server/source/osn-simple-replay-buffer.cpp +++ b/obs-studio-server/source/osn-simple-replay-buffer.cpp @@ -22,6 +22,7 @@ #include "shared.hpp" #include "nodeobs_audio_encoders.h" #include "osn-encoders.hpp" +#include void osn::ISimpleReplayBuffer::Register(ipc::server &srv) { @@ -78,15 +79,15 @@ void osn::ISimpleReplayBuffer::Destroy(void *data, const int64_t id, const std:: static void remove_reserved_file_characters(std::string &s) { - replace(s.begin(), s.end(), '/', '_'); - replace(s.begin(), s.end(), '\\', '_'); - replace(s.begin(), s.end(), '*', '_'); - replace(s.begin(), s.end(), '?', '_'); - replace(s.begin(), s.end(), '"', '_'); - replace(s.begin(), s.end(), '|', '_'); - replace(s.begin(), s.end(), ':', '_'); - replace(s.begin(), s.end(), '>', '_'); - replace(s.begin(), s.end(), '<', '_'); + std::replace(s.begin(), s.end(), '/', '_'); + std::replace(s.begin(), s.end(), '\\', '_'); + std::replace(s.begin(), s.end(), '*', '_'); + std::replace(s.begin(), s.end(), '?', '_'); + std::replace(s.begin(), s.end(), '"', '_'); + std::replace(s.begin(), s.end(), '|', '_'); + std::replace(s.begin(), s.end(), ':', '_'); + std::replace(s.begin(), s.end(), '>', '_'); + std::replace(s.begin(), s.end(), '<', '_'); } void osn::ISimpleReplayBuffer::Start(void *data, const int64_t id, const std::vector &args, std::vector &rval) @@ -105,7 +106,7 @@ void osn::ISimpleReplayBuffer::Start(void *data, const int64_t id, const std::ve if (obs_get_multiple_rendering() && replayBuffer->usesStream) { if (!replayBuffer->streaming) return; - replayBuffer->streaming->UpdateEncoders(); + replayBuffer->streaming->updateEncoders(); audioEncoder = replayBuffer->streaming->audioEncoder; videoEncoder = replayBuffer->streaming->videoEncoder; } else { diff --git a/obs-studio-server/source/osn-simple-streaming.cpp b/obs-studio-server/source/osn-simple-streaming.cpp index 33f8ffbf6..c7957fc20 100644 --- a/obs-studio-server/source/osn-simple-streaming.cpp +++ b/obs-studio-server/source/osn-simple-streaming.cpp @@ -23,6 +23,7 @@ #include "shared.hpp" #include "nodeobs_audio_encoders.h" #include "osn-encoders.hpp" +#include "osn-streaming-helpers.hpp" void osn::ISimpleStreaming::Register(ipc::server &srv) { @@ -262,7 +263,7 @@ void UpdateStreamingSettings_amd(obs_data_t *settings, int bitrate) obs_data_set_int(settings, "bf", 3); } -void osn::SimpleStreaming::UpdateEncoders() +void osn::SimpleStreaming::updateEncoders() { if (!videoEncoder || !audioEncoder) return; @@ -325,6 +326,44 @@ void osn::SimpleStreaming::UpdateEncoders() obs_data_release(audioEncSettings); } +void osn::SimpleStreaming::start() +{ + updateEncoders(); + obs_encoder_set_audio(audioEncoder, obs_get_audio()); + obs_output_set_audio_encoder(GetOutput(), audioEncoder, 0); + obs_encoder_set_video_mix(audioEncoder, obs_video_mix_get(GetCanvas(), OBS_STREAMING_VIDEO_RENDERING)); + + obs_output_set_video_encoder(GetOutput(), videoEncoder); + + if (enableTwitchVOD) { + twitchVODSupported = isTwitchVODSupported(); + if (twitchVODSupported) + SetupTwitchSoundtrackAudio(this); + } + + obs_output_set_service(GetOutput(), service); + + std::string outputSettingsError; + if (!ApplyOutputSettings(GetOutput(), outputSettingsError)) { + blog(LOG_ERROR, "Failed to apply streaming output settings: %s", outputSettingsError.c_str()); + return; + } + + blog(LOG_INFO, "Start Streaming using %s encoder.", obs_encoder_get_id(videoEncoder)); + + StartOutput(); +} + +void osn::SimpleStreaming::checkOutput() +{ + const char *type = osn::streaming_helpers::getStreamOutputType(service); + if (!type) + type = "rtmp_output"; + + if (!GetOutput() || strcmp(obs_output_get_id(GetOutput()), type) != 0) + CreateOutput(type, "stream"); +} + void osn::ISimpleStreaming::Start(void *data, const int64_t id, const std::vector &args, std::vector &rval) { SimpleStreaming *streaming = static_cast(osn::ISimpleStreaming::Manager::GetInstance().find(args[0].value_union.ui64)); @@ -332,16 +371,22 @@ void osn::ISimpleStreaming::Start(void *data, const int64_t id, const std::vecto PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Simple streaming reference is not valid."); } + // Refuse a normal Start() while the streaming object is in autoconfig test mode — + // the test owns the output until CleanTestMode() runs. Note this is the only + // guard on regular Start; the testMode flag is cleared in CleanTestMode. + if (streaming->testMode) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Service in test mode."); + } + if (!streaming->service) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid service."); } - const char *type = OBS_service::getStreamOutputType(streaming->service); - if (!type) - type = "rtmp_output"; + if (!streaming->GetCanvas()) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid main canvas."); + } - if (!streaming->GetOutput() || strcmp(obs_output_get_id(streaming->GetOutput()), type) != 0) - streaming->CreateOutput(type, "stream"); + streaming->checkOutput(); if (!streaming->GetOutput()) { PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Error while creating the streaming output."); @@ -355,10 +400,6 @@ void osn::ISimpleStreaming::Start(void *data, const int64_t id, const std::vecto PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid audio encoder."); } - if (!streaming->GetCanvas()) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid main canvas."); - } - if (!streaming->GetCanvasVideo(obs_get_multiple_rendering() ? OBS_STREAMING_VIDEO_RENDERING : OBS_MAIN_VIDEO_RENDERING)) { PRETTY_ERROR_RETURN(ErrorCode::CriticalError, "Video pipeline not initialized (canvas has no video mix). " "Graphics device may have been lost during startup. Restart the app."); @@ -370,29 +411,19 @@ void osn::ISimpleStreaming::Start(void *data, const int64_t id, const std::vecto PRETTY_ERROR_RETURN(ErrorCode::CriticalError, "The provided encoder is not valid for the current service."); } - streaming->UpdateEncoders(); - obs_encoder_set_audio(streaming->audioEncoder, obs_get_audio()); - obs_output_set_audio_encoder(streaming->GetOutput(), streaming->audioEncoder, 0); - obs_encoder_set_video_mix(streaming->audioEncoder, obs_video_mix_get(streaming->GetCanvas(), OBS_STREAMING_VIDEO_RENDERING)); - - obs_output_set_video_encoder(streaming->GetOutput(), streaming->videoEncoder); - - if (streaming->enableTwitchVOD) { - streaming->twitchVODSupported = streaming->isTwitchVODSupported(); - if (streaming->twitchVODSupported) - SetupTwitchSoundtrackAudio(streaming); + if (!streaming->delay) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid delay."); } - obs_output_set_service(streaming->GetOutput(), streaming->service); - - std::string outputSettingsError; - if (!streaming->ApplyOutputSettings(streaming->GetOutput(), outputSettingsError)) { - PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, outputSettingsError.c_str()); + if (!streaming->reconnect) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid reconnect."); } - blog(LOG_INFO, "Start Streaming using %s encoder.", obs_encoder_get_id(streaming->videoEncoder)); + if (!streaming->network) { + PRETTY_ERROR_RETURN(ErrorCode::InvalidReference, "Invalid network."); + } - streaming->StartOutput(); + streaming->start(); rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); AUTO_DEBUG; diff --git a/obs-studio-server/source/osn-simple-streaming.hpp b/obs-studio-server/source/osn-simple-streaming.hpp index b84a7cd44..0be21ce60 100644 --- a/obs-studio-server/source/osn-simple-streaming.hpp +++ b/obs-studio-server/source/osn-simple-streaming.hpp @@ -41,7 +41,9 @@ class SimpleStreaming : public Streaming { bool useAdvanced; std::string customEncSettings; - void UpdateEncoders(); + void updateEncoders(); + void start() override; + void checkOutput() override; }; class ISimpleStreaming : public IStreaming { diff --git a/obs-studio-server/source/osn-streaming-helpers.hpp b/obs-studio-server/source/osn-streaming-helpers.hpp new file mode 100644 index 000000000..a0ce0b71e --- /dev/null +++ b/obs-studio-server/source/osn-streaming-helpers.hpp @@ -0,0 +1,105 @@ +/****************************************************************************** + Copyright (C) 2016-2022 by Streamlabs (General Workings Inc) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +******************************************************************************/ + +#pragma once +#include + +// Encoder defines - centralized to decouple from old API (nodeobs_service.h) +// These are used across multiple streaming/recording/encoder files +#ifdef WIN32 +#define SIMPLE_ENCODER_X264 "x264" +#elif __APPLE__ +#define SIMPLE_ENCODER_X264 "obs_x264" +#endif +#ifndef SIMPLE_ENCODER_X264 +#define SIMPLE_ENCODER_X264 "x264" +#endif +#define SIMPLE_ENCODER_X264_LOWCPU "x264_lowcpu" +#define SIMPLE_ENCODER_QSV "qsv" +#define SIMPLE_ENCODER_QSV_AV1 "qsv_av1" +#define SIMPLE_ENCODER_NVENC "nvenc" +#define SIMPLE_ENCODER_NVENC_AV1 "nvenc_av1" +#define SIMPLE_ENCODER_NVENC_HEVC "nvenc_hevc" +#define SIMPLE_ENCODER_AMD "amd" +#define SIMPLE_ENCODER_AMD_HEVC "amd_hevc" +#define SIMPLE_ENCODER_AMD_AV1 "amd_av1" +#define SIMPLE_ENCODER_APPLE_H264 "apple_h264" +#define SIMPLE_ENCODER_APPLE_HEVC "apple_hevc" + +#define ADVANCED_ENCODER_X264 "obs_x264" +#define ADVANCED_ENCODER_QSV "obs_qsv11" +#define ADVANCED_ENCODER_NVENC "ffmpeg_nvenc" +#define ADVANCED_ENCODER_AMD "h264_texture_amf" +#define ADVANCED_ENCODER_AMD_HEVC "h265_texture_amf" + +#define ENCODER_NVENC_H264_TEX "obs_nvenc_h264_tex" +#define ENCODER_NVENC_HEVC_TEX "obs_nvenc_hevc_tex" +#define ENCODER_NVENC_AV1_TEX "obs_nvenc_av1_tex" + +// Deprecated encoders +#define ENCODER_JIM_NVENC "jim_nvenc" +#define ENCODER_JIM_HEVC_NVENC "jim_hevc_nvenc" +#define ENCODER_JIM_AV1_NVENC "jim_av1_nvenc" + +#define ENCODER_AV1_SVT_FFMPEG "ffmpeg_svt_av1" +#define ENCODER_AV1_AOM_FFMPEG "ffmpeg_aom_av1" + +#define APPLE_SOFTWARE_VIDEO_ENCODER "com.apple.videotoolbox.videoencoder.h264" +#define APPLE_HARDWARE_VIDEO_ENCODER "com.apple.videotoolbox.videoencoder.h264.gva" +#define APPLE_HARDWARE_VIDEO_ENCODER_M1 "com.apple.videotoolbox.videoencoder.ave.avc" + +namespace osn { +namespace streaming_helpers { + +// Helper function to get stream output type from service +// Replaces OBS_service::getStreamOutputType to decouple from old API +inline const char *getStreamOutputType(obs_service_t *service) +{ + if (!service) + return nullptr; + + const char *protocol = obs_service_get_protocol(service); + if (!protocol) { + blog(LOG_WARNING, "The service '%s' has no protocol set", obs_service_get_id(service)); + return nullptr; + } + + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_WARNING, "The protocol '%s' is not registered", protocol); + return nullptr; + } + + // Check if the service has a preferred output type + const char *output = obs_service_get_preferred_output_type(service); + if (output && (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0) + return output; + + // Prefer first-party output types based on protocol + if (strcmp(protocol, "RTMP") == 0 || strcmp(protocol, "RTMPS") == 0) + return "rtmp_output"; + else if (strcmp(protocol, "HLS") == 0) + return "ffmpeg_hls_muxer"; + else if (strcmp(protocol, "SRT") == 0 || strcmp(protocol, "RIST") == 0) + return "ffmpeg_mpegts_muxer"; + + // Default fallback + return nullptr; +} + +} // namespace streaming_helpers +} // namespace osn diff --git a/obs-studio-server/source/osn-streaming.cpp b/obs-studio-server/source/osn-streaming.cpp index b91ab21d5..d7e78ae33 100644 --- a/obs-studio-server/source/osn-streaming.cpp +++ b/obs-studio-server/source/osn-streaming.cpp @@ -24,6 +24,8 @@ #include "osn-encoders.hpp" //os_gettime_ns #include +#include +#include osn::Streaming::~Streaming() { @@ -32,6 +34,97 @@ osn::Streaming::~Streaming() obs_encoder_release(streamArchive); streamArchive = nullptr; } + if (originalServiceSettings) { + obs_data_release(originalServiceSettings); + originalServiceSettings = nullptr; + } +} + +void osn::Streaming::testBandwidth(bool &gotError, int testBitrate) +{ + if (!service || !GetOutput()) { + gotError = true; + return; + } + + // Override the user's encoder bitrate with the ceiling-search target so the + // bandwidth measurement isn't capped by whatever the user happens to have + // set (often 2500). Restored in CleanTestMode(). + if (testBitrate > 0 && videoEncoder) { + obs_data_t *encSettings = obs_encoder_get_settings(videoEncoder); + originalEncoderBitrate = (int)obs_data_get_int(encSettings, "bitrate"); + obs_data_release(encSettings); + + obs_data_t *override = obs_data_create(); + obs_data_set_int(override, "bitrate", (long long)testBitrate); + obs_encoder_update(videoEncoder, override); + obs_data_release(override); + } + + if (originalServiceSettings) { + obs_data_release(originalServiceSettings); + } + // obs_service_get_settings() returns an owned reference; CleanTestMode() and the + // destructor each release it once, so don't take an extra ref (was a leak). + originalServiceSettings = obs_service_get_settings(service); + + obs_data_t *serviceSettings = obs_data_create(); + obs_data_apply(serviceSettings, originalServiceSettings); + + const char *serviceName = obs_data_get_string(serviceSettings, "service"); + if (serviceName && strcmp(serviceName, "Twitch") == 0) { + std::string key = obs_service_get_connect_info(service, OBS_SERVICE_CONNECT_INFO_STREAM_KEY); + + while (!key.empty()) { + char ch = key.back(); + if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r') + key.pop_back(); + else + break; + } + + key += "?bandwidthtest"; + obs_data_set_string(serviceSettings, "key", key.c_str()); + } + + obs_service_update(service, serviceSettings); + obs_data_release(serviceSettings); + + testMode = true; + start(); +} + +void osn::Streaming::CleanTestMode() +{ + if (GetOutput()) { + if (obs_output_active(GetOutput())) { + obs_output_stop(GetOutput()); + } + // obs_output_stop is asynchronous: the output stays active until its + // internal stop thread finishes flushing. Block here so callers + // (autoconfig SaveSettings -> applyResults -> obs_set_video_info) + // don't race with OBS_VIDEO_CURRENTLY_ACTIVE (-4). + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (obs_output_active(GetOutput()) && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + } + + if (service && originalServiceSettings) { + obs_service_update(service, originalServiceSettings); + obs_data_release(originalServiceSettings); + originalServiceSettings = nullptr; + } + + if (videoEncoder && originalEncoderBitrate > 0) { + obs_data_t *restore = obs_data_create(); + obs_data_set_int(restore, "bitrate", (long long)originalEncoderBitrate); + obs_encoder_update(videoEncoder, restore); + obs_data_release(restore); + originalEncoderBitrate = 0; + } + + testMode = false; } void osn::Streaming::DeleteOutput() @@ -342,6 +435,13 @@ void osn::IStreaming::Query(void *data, const int64_t id, const std::vectortestMode) { + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + rval.push_back(ipc::value(true)); + AUTO_DEBUG; + return; + } + auto signalOpt = streaming->PopReceivedSignal(); if (!signalOpt.has_value()) { rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); @@ -423,6 +523,15 @@ void osn::Streaming::setNetworkLegacySettings() config_set_bool(ConfigManager::getInstance().getBasic(), "Output", "LowLatencyEnable", network->enableDynamicBitrate); } +std::string osn::Streaming::testQuery() +{ + auto signalOpt = PopReceivedSignal(); + if (!signalOpt.has_value()) { + return ""; + } + return signalOpt.value().signal; +} + void osn::IStreaming::GetDroppedFrames(void *data, const int64_t id, const std::vector &args, std::vector &rval) { Streaming *streaming = osn::IStreaming::Manager::GetInstance().find(args[0].value_union.ui64); diff --git a/obs-studio-server/source/osn-streaming.hpp b/obs-studio-server/source/osn-streaming.hpp index eb9ed1369..25cbacce1 100644 --- a/obs-studio-server/source/osn-streaming.hpp +++ b/obs-studio-server/source/osn-streaming.hpp @@ -50,6 +50,9 @@ class Streaming : public Output { lastBytesSent = 0; lastBytesSentTime = 0; simple = true; + testMode = false; + originalServiceSettings = nullptr; + originalEncoderBitrate = 0; } virtual ~Streaming(); @@ -70,6 +73,12 @@ class Streaming : public Output { uint64_t lastBytesSent; uint64_t lastBytesSentTime; bool simple; + bool testMode; + obs_data_t *originalServiceSettings; + // Bitrate the user had on videoEncoder before testBandwidth() bumped it for + // the ceiling-search measurement. Restored in CleanTestMode(). 0 = nothing + // to restore. + int originalEncoderBitrate; bool isTwitchVODSupported(); bool ApplyOutputSettings(obs_output_t *output, std::string &errorMessage); @@ -79,6 +88,17 @@ class Streaming : public Output { void setDelayLegacySettings(); void setReconnectLegacySettings(); void setNetworkLegacySettings(); + std::string testQuery(); + + // Autoconfig hooks. start() and checkOutput() are subclass-specific (different + // pipelines for simple/advanced); testBandwidth() and CleanTestMode() are shared. + virtual void start() {} + virtual void checkOutput() {} + // testBitrate: bitrate to override videoEncoder with for the duration of the + // measurement. The user's original bitrate is restored by CleanTestMode(). + // Pass 0 to leave the encoder untouched. + void testBandwidth(bool &gotError, int testBitrate = 0); + void CleanTestMode(); }; class IStreaming { diff --git a/obs-studio-server/source/osn-video.cpp b/obs-studio-server/source/osn-video.cpp index 946a50e99..b36ac0c48 100644 --- a/obs-studio-server/source/osn-video.cpp +++ b/obs-studio-server/source/osn-video.cpp @@ -21,11 +21,28 @@ #include #include "osn-error.hpp" #include "shared.hpp" +#include "osn-streaming.hpp" // DELETE ME WHEN REMOVING NODEOBS #include "nodeobs_configManager.hpp" #include "nodeobs_api.h" +namespace { +// APIv2 replacement for the legacy OBS_service::stopConnectingOutputs() guard. +// obs_set_video_info / obs_remove_video_info must not run while a streaming output +// is still connecting, or libobs races on the video pointer. +void stopConnectingStreamingOutputs() +{ + osn::IStreaming::Manager::GetInstance().for_each([](osn::Streaming *streaming) { + if (!streaming) + return; + obs_output_t *output = streaming->GetOutput(); + if (output && obs_output_connecting(output)) + obs_output_force_stop(output); + }); +} +} // namespace + void osn::Video::Register(ipc::server &srv) { std::shared_ptr cls = std::make_shared("Video"); @@ -341,8 +358,8 @@ void osn::Video::SetVideoContext(void *data, const int64_t id, const std::vector int ret = OBS_VIDEO_FAIL; try { - // Cannot disrupt video ptr inside obs while outputs are connecting - OBS_service::stopConnectingOutputs(); + // Cannot disrupt video ptr inside obs while outputs are connecting. + stopConnectingStreamingOutputs(); ret = obs_set_video_info(canvas, &video); } catch (const char *error) { blog(LOG_ERROR, "Failed to set video context %s", error); @@ -401,12 +418,9 @@ void osn::Video::RemoveVideoContext(void *data, const int64_t id, const std::vec int ret = OBS_VIDEO_FAIL; try { - - // Cannot disrupt video ptr inside obs while outputs are connecting - OBS_service::stopConnectingOutputs(); - + // Cannot disrupt video ptr inside obs while outputs are connecting. + stopConnectingStreamingOutputs(); ret = obs_remove_video_info(canvas); - } catch (const char *error) { blog(LOG_ERROR, "Error occurred while removing video %s", error); } diff --git a/obs-studio-server/source/util-crashmanager.cpp b/obs-studio-server/source/util-crashmanager.cpp index 04f7be3ca..d91e9f857 100644 --- a/obs-studio-server/source/util-crashmanager.cpp +++ b/obs-studio-server/source/util-crashmanager.cpp @@ -60,6 +60,7 @@ #endif #include "nodeobs_api.h" +#include "nodeobs_service.h" #include "osn-error.hpp" #include "shared.hpp" diff --git a/source/autoconfig-probe-policy.hpp b/source/autoconfig-probe-policy.hpp new file mode 100644 index 000000000..d8ae3960d --- /dev/null +++ b/source/autoconfig-probe-policy.hpp @@ -0,0 +1,244 @@ +#pragma once + +#include +#include +#include + +namespace autoConfig { +namespace probePolicy { + +constexpr uint32_t kBasisPointsScale = 10000; +constexpr uint32_t kYoutubeCleanThroughputMinimumBasisPoints = 9000; +constexpr uint32_t kYoutubeCleanDropMaximumBasisPoints = 200; +constexpr uint32_t kYoutubeCleanCongestionHighMaximumBasisPoints = 1000; +constexpr uint32_t kYoutubeCleanCongestionSevereMaximumBasisPoints = 200; +constexpr uint32_t kYoutubeHardThroughputMinimumBasisPoints = 7500; +constexpr uint32_t kYoutubeHardDropMaximumBasisPoints = 500; +constexpr uint32_t kYoutubeHardCongestionHighMaximumBasisPoints = 3000; +constexpr uint32_t kYoutubeHardCongestionSevereMaximumBasisPoints = 1000; + +struct YoutubeProbeSampleMetrics { + uint32_t throughputBasisPoints = 0; + uint32_t dropBasisPoints = 0; + uint32_t congestionHighBasisPoints = 0; + uint32_t congestionSevereBasisPoints = 0; +}; + +enum class YoutubeProbeSampleClass { Clean, Marginal, Hard }; + +enum class YoutubeBaselineDecision { Clean, Impaired, NeedsThird, Unstable }; + +struct YoutubeBaselineAssessment { + YoutubeBaselineDecision decision = YoutubeBaselineDecision::NeedsThird; + YoutubeProbeSampleMetrics reference; +}; + +enum class YoutubeConfirmationDecision { CapacityKnee, TransientRecovered, PathUnstable, Inconsistent }; + +inline uint32_t ratioBasisPoints(uint32_t numerator, uint32_t denominator) +{ + if (denominator == 0) + return 0; + + const uint64_t scaled = (uint64_t)numerator * kBasisPointsScale / denominator; + return (uint32_t)std::min(scaled, UINT32_MAX); +} + +inline YoutubeProbeSampleMetrics makeYoutubeProbeSampleMetrics(uint32_t measuredAggregateKbps, uint32_t expectedAggregateKbps, uint32_t droppedFrames, + uint32_t totalFrames, uint32_t congestionHighSamples, uint32_t congestionSevereSamples, + uint32_t congestionSamples) +{ + return {ratioBasisPoints(measuredAggregateKbps, expectedAggregateKbps), ratioBasisPoints(droppedFrames, totalFrames), + ratioBasisPoints(congestionHighSamples, congestionSamples), ratioBasisPoints(congestionSevereSamples, congestionSamples)}; +} + +inline YoutubeProbeSampleClass classifyYoutubeProbeSample(const YoutubeProbeSampleMetrics &sample) +{ + if (sample.throughputBasisPoints < kYoutubeHardThroughputMinimumBasisPoints || sample.dropBasisPoints > kYoutubeHardDropMaximumBasisPoints || + sample.congestionHighBasisPoints > kYoutubeHardCongestionHighMaximumBasisPoints || + sample.congestionSevereBasisPoints > kYoutubeHardCongestionSevereMaximumBasisPoints) + return YoutubeProbeSampleClass::Hard; + + if (sample.throughputBasisPoints >= kYoutubeCleanThroughputMinimumBasisPoints && sample.dropBasisPoints <= kYoutubeCleanDropMaximumBasisPoints && + sample.congestionHighBasisPoints <= kYoutubeCleanCongestionHighMaximumBasisPoints && + sample.congestionSevereBasisPoints <= kYoutubeCleanCongestionSevereMaximumBasisPoints) + return YoutubeProbeSampleClass::Clean; + + return YoutubeProbeSampleClass::Marginal; +} + +inline uint32_t absoluteDifference(uint32_t left, uint32_t right) +{ + return left > right ? left - right : right - left; +} + +inline uint32_t medianOfThree(uint32_t first, uint32_t second, uint32_t third) +{ + return (uint32_t)(((uint64_t)first + second + third) - std::min({first, second, third}) - std::max({first, second, third})); +} + +inline YoutubeProbeSampleMetrics conservativeReference(const YoutubeProbeSampleMetrics &first, const YoutubeProbeSampleMetrics &second) +{ + return {std::min(first.throughputBasisPoints, second.throughputBasisPoints), std::max(first.dropBasisPoints, second.dropBasisPoints), + std::max(first.congestionHighBasisPoints, second.congestionHighBasisPoints), + std::max(first.congestionSevereBasisPoints, second.congestionSevereBasisPoints)}; +} + +inline bool youtubeBaselineSamplesSimilar(const YoutubeProbeSampleMetrics &first, const YoutubeProbeSampleMetrics &second) +{ + return absoluteDifference(first.throughputBasisPoints, second.throughputBasisPoints) <= 500 && + absoluteDifference(first.dropBasisPoints, second.dropBasisPoints) <= 100 && + absoluteDifference(first.congestionHighBasisPoints, second.congestionHighBasisPoints) <= 1000 && + absoluteDifference(first.congestionSevereBasisPoints, second.congestionSevereBasisPoints) <= 500; +} + +inline YoutubeBaselineAssessment assessYoutubeBaseline(const YoutubeProbeSampleMetrics &first, const YoutubeProbeSampleMetrics &second) +{ + const YoutubeProbeSampleClass firstClass = classifyYoutubeProbeSample(first); + const YoutubeProbeSampleClass secondClass = classifyYoutubeProbeSample(second); + const YoutubeProbeSampleMetrics reference = conservativeReference(first, second); + + if (firstClass == YoutubeProbeSampleClass::Hard && secondClass == YoutubeProbeSampleClass::Hard) + return {YoutubeBaselineDecision::Unstable, reference}; + if (firstClass == YoutubeProbeSampleClass::Clean && secondClass == YoutubeProbeSampleClass::Clean) + return {youtubeBaselineSamplesSimilar(first, second) ? YoutubeBaselineDecision::Clean : YoutubeBaselineDecision::NeedsThird, reference}; + if (firstClass == YoutubeProbeSampleClass::Marginal && secondClass == YoutubeProbeSampleClass::Marginal && youtubeBaselineSamplesSimilar(first, second)) + return {YoutubeBaselineDecision::Impaired, reference}; + return {YoutubeBaselineDecision::NeedsThird, reference}; +} + +inline YoutubeBaselineAssessment resolveYoutubeBaseline(const YoutubeProbeSampleMetrics &first, const YoutubeProbeSampleMetrics &second, + const YoutubeProbeSampleMetrics &third) +{ + YoutubeProbeSampleMetrics reference{medianOfThree(first.throughputBasisPoints, second.throughputBasisPoints, third.throughputBasisPoints), + medianOfThree(first.dropBasisPoints, second.dropBasisPoints, third.dropBasisPoints), + medianOfThree(first.congestionHighBasisPoints, second.congestionHighBasisPoints, third.congestionHighBasisPoints), + medianOfThree(first.congestionSevereBasisPoints, second.congestionSevereBasisPoints, + third.congestionSevereBasisPoints)}; + const size_t hardSamples = (classifyYoutubeProbeSample(first) == YoutubeProbeSampleClass::Hard ? 1 : 0) + + (classifyYoutubeProbeSample(second) == YoutubeProbeSampleClass::Hard ? 1 : 0) + + (classifyYoutubeProbeSample(third) == YoutubeProbeSampleClass::Hard ? 1 : 0); + if (hardSamples >= 2) + return {YoutubeBaselineDecision::Unstable, reference}; + const YoutubeProbeSampleClass referenceClass = classifyYoutubeProbeSample(reference); + if (referenceClass == YoutubeProbeSampleClass::Hard) + return {YoutubeBaselineDecision::Unstable, reference}; + if (referenceClass == YoutubeProbeSampleClass::Clean) + return {YoutubeBaselineDecision::Clean, reference}; + return {YoutubeBaselineDecision::Impaired, reference}; +} + +inline bool youtubeSampleAccepted(const YoutubeProbeSampleMetrics &sample, const YoutubeBaselineAssessment &baseline) +{ + if (classifyYoutubeProbeSample(sample) == YoutubeProbeSampleClass::Clean) + return true; + if (baseline.decision != YoutubeBaselineDecision::Impaired) + return false; + + const YoutubeProbeSampleMetrics &reference = baseline.reference; + const uint32_t minimumThroughput = + std::max(kYoutubeHardThroughputMinimumBasisPoints, reference.throughputBasisPoints > 500 ? reference.throughputBasisPoints - 500 : 0); + const uint32_t maximumDrops = std::min(kYoutubeHardDropMaximumBasisPoints, reference.dropBasisPoints + 100); + const uint32_t maximumHighCongestion = std::min(kYoutubeHardCongestionHighMaximumBasisPoints, reference.congestionHighBasisPoints + 1000); + const uint32_t maximumSevereCongestion = + std::min(kYoutubeHardCongestionSevereMaximumBasisPoints, reference.congestionSevereBasisPoints + 500); + return sample.throughputBasisPoints >= minimumThroughput && sample.dropBasisPoints <= maximumDrops && + sample.congestionHighBasisPoints <= maximumHighCongestion && sample.congestionSevereBasisPoints <= maximumSevereCongestion; +} + +inline bool youtubeLowControlRecovered(const YoutubeProbeSampleMetrics &control, const YoutubeProbeSampleMetrics &original, + const YoutubeBaselineAssessment &baseline) +{ + if (!youtubeSampleAccepted(control, baseline)) + return false; + return (uint64_t)control.throughputBasisPoints + 500 >= original.throughputBasisPoints && + control.dropBasisPoints <= (uint64_t)original.dropBasisPoints + 100 && + control.congestionHighBasisPoints <= (uint64_t)original.congestionHighBasisPoints + 1000 && + control.congestionSevereBasisPoints <= (uint64_t)original.congestionSevereBasisPoints + 500; +} + +inline YoutubeConfirmationDecision decideYoutubeConfirmation(bool lowControlRecovered, bool highRetryAccepted) +{ + if (lowControlRecovered) + return highRetryAccepted ? YoutubeConfirmationDecision::TransientRecovered : YoutubeConfirmationDecision::CapacityKnee; + return highRetryAccepted ? YoutubeConfirmationDecision::Inconsistent : YoutubeConfirmationDecision::PathUnstable; +} + +struct YoutubeRampEvidence { + bool passedStep = false; + uint64_t recommendationBasisKbps = 0; + uint64_t failedUpperBoundKbps = 0; + + void observe(uint64_t measuredKbps, bool passed, uint64_t attemptedAggregateKbps = 0) + { + if (passed) { + passedStep = true; + recommendationBasisKbps = measuredKbps; + return; + } + + failedUpperBoundKbps = attemptedAggregateKbps > 0 ? std::min(measuredKbps, attemptedAggregateKbps) : measuredKbps; + if (!passedStep) + recommendationBasisKbps = measuredKbps; + } + + uint64_t safeVideoKbps(int safeMultiplierPercent, uint64_t audioKbps) const + { + if (recommendationBasisKbps == 0 || safeMultiplierPercent <= 0) + return 0; + + uint64_t safe = std::max(1, recommendationBasisKbps * (uint64_t)safeMultiplierPercent / 100ULL); + if (failedUpperBoundKbps > 0) { + const uint64_t failedSafe = std::max(1, failedUpperBoundKbps * (uint64_t)safeMultiplierPercent / 100ULL); + safe = std::min(safe, failedSafe); + } + return safe > audioKbps ? safe - audioKbps : 0; + } +}; + +inline uint64_t safeVideoKbps(uint64_t measuredAggregateKbps, int safeMultiplierPercent, uint64_t audioKbps) +{ + if (measuredAggregateKbps == 0 || safeMultiplierPercent <= 0) + return 0; + const uint64_t safeAggregateKbps = measuredAggregateKbps * (uint64_t)safeMultiplierPercent / 100ULL; + return safeAggregateKbps > audioKbps ? safeAggregateKbps - audioKbps : 0; +} + +inline bool hasProbeThroughputMetrics(bool success, uint64_t measuredKbps) +{ + return success || measuredKbps > 0; +} + +inline int clampEstimateToObservedSafe(int estimatedKbps, uint64_t observedSafeKbps, int maximumKbps) +{ + if (observedSafeKbps == 0 || maximumKbps <= 0) + return estimatedKbps; + return std::min(estimatedKbps, (int)std::min(observedSafeKbps, (uint64_t)maximumKbps)); +} + +inline int effectiveProbeCeilingKbps(int probeMaximumKbps, int platformMaximumKbps, int requestMaximumKbps) +{ + int effective = probeMaximumKbps; + if (platformMaximumKbps > 0) + effective = std::min(effective, platformMaximumKbps); + if (requestMaximumKbps > 0) + effective = std::min(effective, requestMaximumKbps); + return effective; +} + +inline bool reachedEffectiveProbeCeiling(int targetKbps, int effectiveCeilingKbps) +{ + return targetKbps > 0 && effectiveCeilingKbps > 0 && targetKbps >= effectiveCeilingKbps; +} + +inline double probeSubstepProgress(double slotStart, double slotEnd, size_t stepIndex, size_t stepCount) +{ + if (stepCount == 0 || slotEnd <= slotStart) + return slotStart; + + const size_t clampedIndex = std::min(stepIndex, stepCount - 1); + return slotStart + (slotEnd - slotStart) * (double)(clampedIndex + 1) / (double)(stepCount + 1); +} + +} // namespace probePolicy +} // namespace autoConfig diff --git a/tests/osn-tests/src/test_nodeobs_autoconfig.ts b/tests/osn-tests/src/test_nodeobs_autoconfig.ts deleted file mode 100644 index bbcf52c99..000000000 --- a/tests/osn-tests/src/test_nodeobs_autoconfig.ts +++ /dev/null @@ -1,166 +0,0 @@ -import 'mocha'; -import { expect } from 'chai'; -import * as osn from '../osn'; -import { logInfo, logEmptyLine, logWarning } from '../util/logger'; -import { ETestErrorMsg, GetErrorMessage } from '../util/error_messages'; -import { OBSHandler, IConfigProgress } from '../util/obs_handler'; -import { deleteConfigFiles } from '../util/general'; - -const testName = 'nodeobs_autoconfig'; - -describe(testName, function() { - this.timeout(30000) - let obs: OBSHandler; - let hasTestFailed: boolean = false; - - // Initialize OBS process - before(async function() { - logInfo(testName, 'Starting ' + testName + ' tests'); - deleteConfigFiles(); - obs = new OBSHandler(testName, false); - - obs.instantiateUserPool(testName); - - // Reserving user from pool - await obs.reserveUser(); - }); - - // Shutdown OBS process - after(async function() { - - // Releasing user got from pool - await obs.releaseUser(); - - // Closing OBS process - obs.shutdown(); - - if (hasTestFailed === true) { - logInfo(testName, 'One or more test cases failed. Uploading cache'); - await obs.uploadTestCache(); - } - - obs = null; - deleteConfigFiles(); - logInfo(testName, 'Finished ' + testName + ' tests'); - logEmptyLine(); - }); - - afterEach(async function() { - hasTestFailed = (await obs.finalizeRetryableTest(this)) || hasTestFailed; - }); - - it('Run autoconfig', async function() { - if (obs.isDarwin()) { - this.skip(); - } - const start = performance.now(); - let progressInfo: IConfigProgress; - let settingValue: any; - - obs.startAutoconfig(); - - osn.NodeObs.StartBandwidthTest(); - - progressInfo = await obs.getNextProgressInfo('Bandwidth test'); - - if (progressInfo.event != 'error') { - expect(progressInfo.event).to.equal('stopping_step', GetErrorMessage(ETestErrorMsg.BandwidthTest)); - expect(progressInfo.description).to.equal('bandwidth_test', GetErrorMessage(ETestErrorMsg.BandwidthTest)); - expect(progressInfo.percentage).to.equal(100, GetErrorMessage(ETestErrorMsg.BandwidthTest)); - - osn.NodeObs.StartStreamEncoderTest(); - - progressInfo = await obs.getNextProgressInfo('Stream Encoder test'); - expect(progressInfo.event).to.equal('stopping_step', GetErrorMessage(ETestErrorMsg.StreamEncoderTest)); - expect(progressInfo.description).to.equal('streamingEncoder_test', GetErrorMessage(ETestErrorMsg.StreamEncoderTest)); - expect(progressInfo.percentage).to.equal(100, GetErrorMessage(ETestErrorMsg.StreamEncoderTest)); - - osn.NodeObs.StartRecordingEncoderTest(); - - progressInfo = await obs.getNextProgressInfo('Recording Encoder test'); - expect(progressInfo.event).to.equal('stopping_step', GetErrorMessage(ETestErrorMsg.RecordingEncoderTest)); - expect(progressInfo.description).to.equal('recordingEncoder_test', GetErrorMessage(ETestErrorMsg.RecordingEncoderTest)); - expect(progressInfo.percentage).to.equal(100, GetErrorMessage(ETestErrorMsg.RecordingEncoderTest)); - - osn.NodeObs.StartCheckSettings(); - - progressInfo = await obs.getNextProgressInfo('Check Settings'); - expect(progressInfo.event).to.equal('stopping_step', GetErrorMessage(ETestErrorMsg.CheckSettings)); - expect(progressInfo.description).to.equal('checking_settings', GetErrorMessage(ETestErrorMsg.CheckSettings)); - expect(progressInfo.percentage).to.equal(100, GetErrorMessage(ETestErrorMsg.CheckSettings)); -/* - osn.NodeObs.StartSaveStreamSettings(); - - progressInfo = await obs.getNextProgressInfo('Save Stream Settings'); - expect(progressInfo.event).to.equal('stopping_step', GetErrorMessage(ETestErrorMsg.SaveStreamSettings)); - expect(progressInfo.description).to.equal('saving_service', GetErrorMessage(ETestErrorMsg.SaveStreamSettings)); - expect(progressInfo.percentage).to.equal(100, GetErrorMessage(ETestErrorMsg.SaveStreamSettings)); - - osn.NodeObs.StartSaveSettings(); - - progressInfo = await obs.getNextProgressInfo('Save Settings'); - expect(progressInfo.event).to.equal('stopping_step', GetErrorMessage(ETestErrorMsg.SaveSettingsStep)); - expect(progressInfo.description).to.equal('saving_settings', GetErrorMessage(ETestErrorMsg.SaveSettingsStep)); - expect(progressInfo.percentage).to.equal(100, GetErrorMessage(ETestErrorMsg.SaveSettingsStep)); - - progressInfo = await obs.getNextProgressInfo('Autoconfig done'); - expect(progressInfo.event).to.equal('done', GetErrorMessage(ETestErrorMsg.SaveSettingsStep)); - */ - } else { - logWarning(testName, 'Bandwidth test failed with ' + progressInfo.description + '. Setting default settings'); - - osn.NodeObs.StartSetDefaultSettings(); - - progressInfo = await obs.getNextProgressInfo('Set Default Settings'); - expect(progressInfo.event).to.equal('stopping_step', GetErrorMessage(ETestErrorMsg.SetDefaultSettings)); - expect(progressInfo.description).to.equal('setting_default_settings', GetErrorMessage(ETestErrorMsg.SetDefaultSettings)); - expect(progressInfo.percentage).to.equal(100, GetErrorMessage(ETestErrorMsg.SetDefaultSettings)); - - osn.NodeObs.StartSaveStreamSettings(); - - progressInfo = await obs.getNextProgressInfo('Save Stream Settings'); - expect(progressInfo.event).to.equal('stopping_step', GetErrorMessage(ETestErrorMsg.SaveStreamSettings)); - expect(progressInfo.description).to.equal('saving_service', GetErrorMessage(ETestErrorMsg.SaveStreamSettings)); - expect(progressInfo.percentage).to.equal(100, GetErrorMessage(ETestErrorMsg.SaveStreamSettings)); - - osn.NodeObs.StartSaveSettings(); - - progressInfo = await obs.getNextProgressInfo('Save Settings'); - expect(progressInfo.event).to.equal('stopping_step', GetErrorMessage(ETestErrorMsg.SaveSettingsStep)); - expect(progressInfo.description).to.equal('saving_settings', GetErrorMessage(ETestErrorMsg.SaveSettingsStep)); - expect(progressInfo.percentage).to.equal(100, GetErrorMessage(ETestErrorMsg.SaveSettingsStep)); - - progressInfo = await obs.getNextProgressInfo('Autoconfig done'); - expect(progressInfo.event).to.equal('done', GetErrorMessage(ETestErrorMsg.SaveSettingsStep)); - - // Checking default settings - settingValue = obs.getSetting('Output', 'Mode'); - expect(settingValue).to.equal('Simple', GetErrorMessage(ETestErrorMsg.DefaultOutputMode)); - - settingValue = obs.getSetting('Output', 'VBitrate'); - expect(settingValue).to.equal(4500, GetErrorMessage(ETestErrorMsg.DefaultVBitrate)); - - settingValue = obs.getSetting('Output', 'StreamEncoder'); - expect(settingValue).to.equal('x264', GetErrorMessage(ETestErrorMsg.DefaultStreamEncoder)); - - settingValue = obs.getSetting('Output', 'RecQuality'); - expect(settingValue).to.equal('Small', GetErrorMessage(ETestErrorMsg.DefaultRecQuality)); - - settingValue = obs.getSetting('Advanced', 'DynamicBitrate'); - expect(settingValue).to.equal(false, GetErrorMessage(ETestErrorMsg.DefaultDinamicBitrate)); - - settingValue = obs.getSetting('Video', 'Output'); - expect(settingValue).to.equal('1280x720', GetErrorMessage(ETestErrorMsg.DefaultVideoOutput)); - - settingValue = obs.getSetting('Video', 'FPSType'); - expect(settingValue).to.equal('Common FPS Values', GetErrorMessage(ETestErrorMsg.DefaultFPSType)); - - settingValue = obs.getSetting('Video', 'FPSCommon'); - expect(settingValue).to.equal('30', GetErrorMessage(ETestErrorMsg.DefaultFPSCommon)); - const end = performance.now(); - logInfo(testName, `Elapsed time: ${end - start} milliseconds`); - } - - osn.NodeObs.TerminateAutoConfig(); - }); -}); diff --git a/tests/osn-tests/src/test_osn_auto_optimizer.ts b/tests/osn-tests/src/test_osn_auto_optimizer.ts new file mode 100644 index 000000000..4d34dadfc --- /dev/null +++ b/tests/osn-tests/src/test_osn_auto_optimizer.ts @@ -0,0 +1,397 @@ +import 'mocha'; +import { expect } from 'chai'; +import * as osn from '../osn'; +import type { + IAutoConfigCapabilities, + IAutoConfigEvent, + IAutoConfigLegRequest, + IAutoConfigRequest, + IAutoConfigResult, +} from '../../../js/module'; +import * as net from 'net'; +import { OBSHandler } from '../util/obs_handler'; +import { deleteConfigFiles } from '../util/general'; + +const testName = 'osn-auto-optimizer'; +const mockPort = 11937; + +describe(testName, function() { + this.timeout(80000); + + let obs: OBSHandler; + + async function startConnectionSink(port: number) { + let connections = 0; + let bytes = 0; + const server = net.createServer(socket => { + connections++; + socket.on('data', chunk => bytes += chunk.length); + }); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(port, '127.0.0.1', resolve); + }); + return { + getConnections: () => connections, + getBytes: () => bytes, + close: () => new Promise(resolve => server.close(() => resolve())), + }; + } + + before(function() { + deleteConfigFiles(); + obs = new OBSHandler(testName); + }); + + after(function() { + if (obs) { + obs.shutdown(); + obs = null; + } + deleteConfigFiles(); + }); + + function leg(overrides: Partial = {}): IAutoConfigLegRequest { + return { + legId: 'primary', + display: 'horizontal', + destinations: [{ platform: 'custom' }], + current: { + width: 1280, + height: 720, + fpsNum: 30, + fpsDen: 1, + bitrateKbps: 2500, + encoderId: 'obs_x264', + codec: 'h264', + preset: 'veryfast', + }, + ...overrides, + }; + } + + async function run(request: IAutoConfigRequest): Promise<{ + sessionId: string; + events: IAutoConfigEvent[]; + result: IAutoConfigResult; + }> { + const events: IAutoConfigEvent[] = []; + let sessionId = ''; + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (sessionId) { + try { osn.NodeObs.CancelAutoConfigSession(sessionId); } catch (_) { /* best effort */ } + try { osn.NodeObs.CloseAutoConfigSession(sessionId); } catch (_) { /* best effort */ } + } + reject(new Error('Auto Optimizer session timed out')); + }, 60000); + + const onEvent = (event: IAutoConfigEvent) => { + events.push(event); + if (event.type !== 'complete' && event.type !== 'cancelled') return; + + try { + const raw = osn.NodeObs.GetAutoConfigResult(sessionId); + const result = JSON.parse(raw) as IAutoConfigResult; + osn.NodeObs.CloseAutoConfigSession(sessionId); + clearTimeout(timeout); + resolve({ sessionId, events, result }); + } catch (error) { + clearTimeout(timeout); + reject(error); + } + }; + + try { + sessionId = osn.NodeObs.CreateAutoConfigSession(JSON.stringify(request), onEvent); + osn.NodeObs.StartAutoConfigSession(sessionId); + } catch (error) { + clearTimeout(timeout); + reject(error); + } + }); + } + + it('advertises the versioned, Desktop-owned apply contract', function() { + expect(osn.NodeObs.ConfirmAutoConfigProbeIngest).to.be.a('function'); + const capabilities = JSON.parse(osn.NodeObs.GetAutoConfigCapabilities()) as IAutoConfigCapabilities; + expect(capabilities).to.deep.equal({ + apiVersion: 2, + resultSchemaVersion: 1, + previewApplySplit: true, + awaitableCancel: true, + perUploadLegResults: true, + desktopOwnedApply: true, + multipleActiveProbes: true, + bandwidthModes: ['twitch-standard-active', 'youtube-unbound-active', 'estimate'], + }); + }); + + it('never dials an ineligible custom RTMP active-probe request', async function() { + const mock = await startConnectionSink(mockPort); + const secret = 'must-not-appear-in-result'; + try { + const response = await run({ + schemaVersion: 1, + topology: 'custom-rtmp', + legs: [leg()], + activeProbes: [{ + probeId: 'twitch-primary', + kind: 'twitch-standard', + legId: 'primary', + serviceName: 'Twitch', + server: `rtmp://127.0.0.1:${mockPort}/live`, + streamKey: secret, + }], + }); + + expect(mock.getConnections()).to.equal(0); + expect(mock.getBytes()).to.equal(0); + expect(response.result.status).to.equal('complete'); + expect(response.result.legs[0].measurement.mode).to.equal('estimated'); + expect(response.result.legs[0].measurement.reason).to.equal('custom_rtmp'); + expect(response.result.legs[0].recommendation.bitrateKbps).to.equal(2500); + expect(JSON.stringify(response.result)).not.to.contain(secret); + expect(JSON.stringify(response.events)).not.to.contain(secret); + expect(response.events.some(event => event.code === 'active_probe_not_eligible')).to.equal(true); + } finally { + await mock.close(); + } + }); + + it('default-denies non-official YouTube probe endpoints without dialing them', async function() { + const mock = await startConnectionSink(mockPort); + const secret = 'youtube-secret-must-not-appear'; + try { + const response = await run({ + schemaVersion: 1, + topology: 'direct-single', + legs: [leg({ destinations: [{ platform: 'youtube' }] })], + activeProbes: [{ + probeId: 'youtube-primary', + kind: 'youtube-unbound', + legId: 'primary', + serviceName: 'YouTube - RTMPS', + server: `rtmps://127.0.0.1:${mockPort}/live2`, + streamKey: secret, + }], + }); + + expect(mock.getConnections()).to.equal(0); + expect(mock.getBytes()).to.equal(0); + expect(response.result.legs[0].measurement.mode).to.equal('estimated'); + expect(JSON.stringify(response.result)).not.to.contain(secret); + expect(JSON.stringify(response.events)).not.to.contain(secret); + expect(response.events.some(event => event.code === 'active_probe_not_eligible')).to.equal(true); + } finally { + await mock.close(); + } + }); + + it('requires the exact YouTube RTMPS service identity before a probe is eligible', async function() { + const request = { + schemaVersion: 1, + topology: 'direct-single', + legs: [leg({ destinations: [{ platform: 'youtube' }] })], + activeProbes: [{ + probeId: 'youtube-missing-service', + kind: 'youtube-unbound', + legId: 'primary', + server: 'rtmps://a.rtmps.youtube.com/live2', + streamKey: 'not-a-real-key', + }], + } as unknown as IAutoConfigRequest; + + const response = await run(request); + expect(response.result.legs[0].measurement.mode).to.equal('estimated'); + expect(response.events.some(event => event.code === 'active_probe_not_eligible')).to.equal(true); + expect(response.events.some(event => event.code === 'youtube_probe_started')).to.equal(false); + }); + + it('requires a complete Twitch and YouTube probe set for a shared cloud leg', async function() { + const response = await run({ + schemaVersion: 1, + topology: 'cloud-multistream', + legs: [leg({ + destinations: [{ platform: 'twitch' }, { platform: 'youtube' }], + estimateReason: 'cloud_multistream', + })], + activeProbes: [{ + probeId: 'cloud-twitch-only', + kind: 'twitch-standard', + legId: 'primary', + serviceName: 'Twitch', + // The incomplete set must be rejected before any connection + // attempt, so an intentionally unofficial endpoint is safe. + server: `rtmp://127.0.0.1:${mockPort}/live`, + streamKey: 'incomplete-cloud-secret', + }], + }); + expect(response.result.legs[0].measurement.mode).to.equal('estimated'); + expect(response.result.legs[0].measurement.reason).to.equal('cloud_multistream'); + expect(response.events.some(event => event.code === 'active_probe_set_incomplete')).to.equal(true); + expect(response.events.some(event => event.code === 'twitch_probe_started')).to.equal(false); + expect(JSON.stringify(response.result)).not.to.contain('incomplete-cloud-secret'); + expect(JSON.stringify(response.events)).not.to.contain('incomplete-cloud-secret'); + }); + + it('default-denies independent dual-output active probes instead of recommending full uplink per leg', async function() { + const twitchSecret = 'dual-twitch-secret'; + const youtubeSecret = 'dual-youtube-secret'; + const response = await run({ + schemaVersion: 1, + topology: 'dual-output', + legs: [ + leg({ + legId: 'horizontal', + display: 'horizontal', + destinations: [{ platform: 'twitch' }], + current: { ...leg().current, bitrateKbps: 6000 }, + estimateReason: 'dual_output', + }), + leg({ + legId: 'vertical', + display: 'vertical', + destinations: [{ platform: 'youtube' }], + current: { ...leg().current, bitrateKbps: 6000 }, + estimateReason: 'dual_output', + }), + ], + activeProbes: [ + { + probeId: 'dual-twitch', + kind: 'twitch-standard', + legId: 'horizontal', + serviceName: 'Twitch', + server: 'rtmp://live.twitch.tv/app', + streamKey: twitchSecret, + }, + { + probeId: 'dual-youtube', + kind: 'youtube-unbound', + legId: 'vertical', + serviceName: 'YouTube - RTMPS', + server: 'rtmps://a.rtmps.youtube.com/live2', + streamKey: youtubeSecret, + }, + ], + }); + + expect(response.result.legs).to.have.length(2); + expect(response.result.legs.every(resultLeg => resultLeg.measurement.mode === 'estimated')).to.equal(true); + expect(response.result.legs.every(resultLeg => resultLeg.measurement.reason === 'dual_output')).to.equal(true); + expect(response.events.filter(event => event.code === 'dual_output_multiple_active_legs')).to.have.length(2); + expect(response.events.some(event => event.code === 'twitch_probe_started' || event.code === 'youtube_probe_started')).to.equal(false); + expect(JSON.stringify(response.result)).not.to.contain(twitchSecret); + expect(JSON.stringify(response.result)).not.to.contain(youtubeSecret); + expect(JSON.stringify(response.events)).not.to.contain(twitchSecret); + expect(JSON.stringify(response.events)).not.to.contain(youtubeSecret); + }); + + it('clamps estimate-only results to bundled platform caps without raising current bitrate', async function() { + const twitch = await run({ + schemaVersion: 1, + topology: 'direct-single', + legs: [leg({ + destinations: [{ platform: 'twitch' }], + current: { ...leg().current, bitrateKbps: 8000 }, + estimateReason: 'probe_disabled', + })], + }); + expect(twitch.result.legs[0].measurement.mode).to.equal('estimated'); + expect(twitch.result.legs[0].recommendation.bitrateKbps).to.equal(6000); + expect(twitch.result.legs[0].limits.maxBitrateKbps).to.equal(6000); + + const alreadyConservative = await run({ + schemaVersion: 1, + topology: 'direct-single', + legs: [leg({ + destinations: [{ platform: 'twitch' }], + current: { ...leg().current, bitrateKbps: 2500 }, + estimateReason: 'probe_disabled', + })], + }); + expect(alreadyConservative.result.legs[0].recommendation.bitrateKbps).to.equal(2500); + }); + + it('uses the strictest destination/request cap and replaces an unavailable encoder', async function() { + const response = await run({ + schemaVersion: 1, + topology: 'cloud-multistream', + legs: [leg({ + destinations: [{ platform: 'youtube' }, { platform: 'twitch' }], + current: { + ...leg().current, + bitrateKbps: 9000, + encoderId: 'definitely-not-a-real-encoder', + }, + limits: { maxBitrateKbps: 1800 }, + estimateReason: 'cloud_multistream', + })], + }); + + expect(response.result.legs[0].recommendation.bitrateKbps).to.equal(1800); + expect(response.result.legs[0].recommendation.encoderId).not.to.equal('definitely-not-a-real-encoder'); + expect(response.result.legs[0].measurement.confidence).to.equal('medium'); + }); + + it('cancels a prepared session and makes cleanup observable before returning', async function() { + const events: IAutoConfigEvent[] = []; + const sessionId = osn.NodeObs.CreateAutoConfigSession(JSON.stringify({ + schemaVersion: 1, + topology: 'custom-rtmp', + legs: [leg()], + } as IAutoConfigRequest), (event: IAutoConfigEvent) => events.push(event)); + + osn.NodeObs.CancelAutoConfigSession(sessionId); + const result = JSON.parse(osn.NodeObs.GetAutoConfigResult(sessionId)) as IAutoConfigResult; + expect(result.status).to.equal('cancelled'); + expect(result.error.code).to.equal('cancelled'); + osn.NodeObs.CloseAutoConfigSession(sessionId); + }); + + it('cancels a running session only after its scratch worker has cleaned up', async function() { + const events: IAutoConfigEvent[] = []; + const sessionId = osn.NodeObs.CreateAutoConfigSession(JSON.stringify({ + schemaVersion: 1, + topology: 'custom-rtmp', + legs: [leg()], + } as IAutoConfigRequest), (event: IAutoConfigEvent) => events.push(event)); + + osn.NodeObs.StartAutoConfigSession(sessionId); + // Let the worker enter the hardware phase so this covers cancellation + // of an executing scratch workload rather than a prepared session. + await new Promise(resolve => setTimeout(resolve, 100)); + + osn.NodeObs.CancelAutoConfigSession(sessionId); + const result = JSON.parse(osn.NodeObs.GetAutoConfigResult(sessionId)) as IAutoConfigResult; + expect(result.status).to.equal('cancelled'); + expect(result.error.code).to.equal('cancelled'); + osn.NodeObs.CloseAutoConfigSession(sessionId); + }); + + it('rejects malformed versioned requests before creating a session', function() { + expect(() => osn.NodeObs.CreateAutoConfigSession(JSON.stringify({ + schemaVersion: 999, + topology: 'direct-single', + legs: [leg()], + }), () => undefined)).to.throw('unsupported_autoconfig_schema'); + }); + + it('disconnects cleanly while a session worker is running', async function() { + const sessionId = osn.NodeObs.CreateAutoConfigSession(JSON.stringify({ + schemaVersion: 1, + topology: 'custom-rtmp', + legs: [leg()], + } as IAutoConfigRequest), () => undefined); + + osn.NodeObs.StartAutoConfigSession(sessionId); + await new Promise(resolve => setTimeout(resolve, 100)); + + const startedAt = Date.now(); + obs.shutdown(); + obs = null; + expect(Date.now() - startedAt).to.be.lessThan(15000); + }); +}); diff --git a/tests/osn-tests/util/error_messages.ts b/tests/osn-tests/util/error_messages.ts index 76c26e55e..68c3660a4 100644 --- a/tests/osn-tests/util/error_messages.ts +++ b/tests/osn-tests/util/error_messages.ts @@ -12,22 +12,6 @@ export const enum ETestErrorMsg { CoreAudioInputHotkeys = 'Core Audio Input hotkey container is wrong', CoreAudioOutputHotkeys = 'Core Audio Output hotkey container is wrong', - // nodeobs_autoconfig - BandwidthTest = 'Bandwidth test', - StreamEncoderTest = 'Stream encoder test', - RecordingEncoderTest = 'Recording encoder test', - CheckSettings = 'Check settings', - SaveStreamSettings = 'Save stream settings', - SaveSettingsStep = 'Save settings', - SetDefaultSettings = 'Set default settings', - DefaultOutputMode = 'Applied default settings does not have the expected value for output mode', - DefaultVBitrate = 'Applied default settings does not have the expected value for vbitrate', - DefaultStreamEncoder = 'Applied default settings does not have the expected value for stream encoder', - DefaultRecQuality = 'Applied default settings does not have the expected value for rec quality', - DefaultDinamicBitrate = 'Applied default settings does not have the expected value for dinamic bitrate', - DefaultVideoOutput = 'Applied default settings does not have the expected value for video output', - DefaultFPSType = 'Applied default settings does not have the expected value for fps type', - DefaultFPSCommon = 'Applied default settings does not have the expected value for fps common', // nodeobs_service StreamOutput = 'Stream output', RecordingOutput = 'Recording output', @@ -230,4 +214,4 @@ export function GetErrorMessage(message: string, value1?: string, value2?: strin }); return errorMessage; -} \ No newline at end of file +} diff --git a/tests/osn-tests/util/obs_handler.ts b/tests/osn-tests/util/obs_handler.ts index 078a5e373..7d710f6aa 100644 --- a/tests/osn-tests/util/obs_handler.ts +++ b/tests/osn-tests/util/obs_handler.ts @@ -34,13 +34,6 @@ export interface IOBSOutputSignalInfo { service: string; } -export interface IConfigProgress { - event: TConfigEvent; - description: string; - percentage?: number; - continent?: string; -} - export interface IVec2 { x: number; y: number; @@ -66,8 +59,6 @@ export type TOBSHotkey = { HotkeyId: number; }; -export type TConfigEvent = 'starting_step' | 'progress' | 'stopping_step' | 'error' | 'done'; - // OBSHandler class export class OBSHandler { private path = require('path'); @@ -86,7 +77,6 @@ export class OBSHandler { private hasUserFromPool: boolean = false; private osnTestName: string; signals = new WaitQueue(); - private progress = new WaitQueue(); inputTypes: string[]; filterTypes: string[]; transitionTypes: string[]; @@ -480,28 +470,6 @@ export class OBSHandler { throw new Error(timeoutMessage); } - startAutoconfig() { - osn.NodeObs.InitializeAutoConfig((progressInfo: IConfigProgress) => { - if (progressInfo.event === 'stopping_step' || progressInfo.event === 'done' || progressInfo.event === 'error') { - this.progress.push(progressInfo); - } - }, - { - service_name: 'Twitch', - }); - } - - getNextProgressInfo(autoconfigStep: string): Promise { - return new Promise((resolve, reject) => { - this.progress.shift().then( - function (progressInfo) { - resolve(progressInfo) - } - ); - setTimeout(() => reject(new Error(autoconfigStep + ' step timeout')), 50000); - }); - } - createDefaultVideoContext() { logInfo(this.osnTestName, 'createDefaultVideoContext called'); this.defaultVideoContext = osn.VideoFactory.create(); diff --git a/yarn.lock b/yarn.lock index 04a3c1403..4375b7765 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,490 +88,493 @@ __metadata: linkType: hard "@aws-sdk/client-s3@npm:^3.0.0": - version: 3.1009.0 - resolution: "@aws-sdk/client-s3@npm:3.1009.0" + version: 3.1038.0 + resolution: "@aws-sdk/client-s3@npm:3.1038.0" dependencies: "@aws-crypto/sha1-browser": "npm:5.2.0" "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/credential-provider-node": "npm:^3.972.21" - "@aws-sdk/middleware-bucket-endpoint": "npm:^3.972.8" - "@aws-sdk/middleware-expect-continue": "npm:^3.972.8" - "@aws-sdk/middleware-flexible-checksums": "npm:^3.973.6" - "@aws-sdk/middleware-host-header": "npm:^3.972.8" - "@aws-sdk/middleware-location-constraint": "npm:^3.972.8" - "@aws-sdk/middleware-logger": "npm:^3.972.8" - "@aws-sdk/middleware-recursion-detection": "npm:^3.972.8" - "@aws-sdk/middleware-sdk-s3": "npm:^3.972.20" - "@aws-sdk/middleware-ssec": "npm:^3.972.8" - "@aws-sdk/middleware-user-agent": "npm:^3.972.21" - "@aws-sdk/region-config-resolver": "npm:^3.972.8" - "@aws-sdk/signature-v4-multi-region": "npm:^3.996.8" - "@aws-sdk/types": "npm:^3.973.6" - "@aws-sdk/util-endpoints": "npm:^3.996.5" - "@aws-sdk/util-user-agent-browser": "npm:^3.972.8" - "@aws-sdk/util-user-agent-node": "npm:^3.973.7" - "@smithy/config-resolver": "npm:^4.4.11" - "@smithy/core": "npm:^3.23.11" - "@smithy/eventstream-serde-browser": "npm:^4.2.12" - "@smithy/eventstream-serde-config-resolver": "npm:^4.3.12" - "@smithy/eventstream-serde-node": "npm:^4.2.12" - "@smithy/fetch-http-handler": "npm:^5.3.15" - "@smithy/hash-blob-browser": "npm:^4.2.13" - "@smithy/hash-node": "npm:^4.2.12" - "@smithy/hash-stream-node": "npm:^4.2.12" - "@smithy/invalid-dependency": "npm:^4.2.12" - "@smithy/md5-js": "npm:^4.2.12" - "@smithy/middleware-content-length": "npm:^4.2.12" - "@smithy/middleware-endpoint": "npm:^4.4.25" - "@smithy/middleware-retry": "npm:^4.4.42" - "@smithy/middleware-serde": "npm:^4.2.14" - "@smithy/middleware-stack": "npm:^4.2.12" - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/node-http-handler": "npm:^4.4.16" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/smithy-client": "npm:^4.12.5" - "@smithy/types": "npm:^4.13.1" - "@smithy/url-parser": "npm:^4.2.12" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/credential-provider-node": "npm:^3.972.37" + "@aws-sdk/middleware-bucket-endpoint": "npm:^3.972.10" + "@aws-sdk/middleware-expect-continue": "npm:^3.972.10" + "@aws-sdk/middleware-flexible-checksums": "npm:^3.974.14" + "@aws-sdk/middleware-host-header": "npm:^3.972.10" + "@aws-sdk/middleware-location-constraint": "npm:^3.972.10" + "@aws-sdk/middleware-logger": "npm:^3.972.10" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.11" + "@aws-sdk/middleware-sdk-s3": "npm:^3.972.35" + "@aws-sdk/middleware-ssec": "npm:^3.972.10" + "@aws-sdk/middleware-user-agent": "npm:^3.972.36" + "@aws-sdk/region-config-resolver": "npm:^3.972.13" + "@aws-sdk/signature-v4-multi-region": "npm:^3.996.23" + "@aws-sdk/types": "npm:^3.973.8" + "@aws-sdk/util-endpoints": "npm:^3.996.8" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.10" + "@aws-sdk/util-user-agent-node": "npm:^3.973.22" + "@smithy/config-resolver": "npm:^4.4.17" + "@smithy/core": "npm:^3.23.17" + "@smithy/eventstream-serde-browser": "npm:^4.2.14" + "@smithy/eventstream-serde-config-resolver": "npm:^4.3.14" + "@smithy/eventstream-serde-node": "npm:^4.2.14" + "@smithy/fetch-http-handler": "npm:^5.3.17" + "@smithy/hash-blob-browser": "npm:^4.2.15" + "@smithy/hash-node": "npm:^4.2.14" + "@smithy/hash-stream-node": "npm:^4.2.14" + "@smithy/invalid-dependency": "npm:^4.2.14" + "@smithy/md5-js": "npm:^4.2.14" + "@smithy/middleware-content-length": "npm:^4.2.14" + "@smithy/middleware-endpoint": "npm:^4.4.32" + "@smithy/middleware-retry": "npm:^4.5.6" + "@smithy/middleware-serde": "npm:^4.2.20" + "@smithy/middleware-stack": "npm:^4.2.14" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/node-http-handler": "npm:^4.6.1" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" "@smithy/util-base64": "npm:^4.3.2" "@smithy/util-body-length-browser": "npm:^4.2.2" "@smithy/util-body-length-node": "npm:^4.2.3" - "@smithy/util-defaults-mode-browser": "npm:^4.3.41" - "@smithy/util-defaults-mode-node": "npm:^4.2.44" - "@smithy/util-endpoints": "npm:^3.3.3" - "@smithy/util-middleware": "npm:^4.2.12" - "@smithy/util-retry": "npm:^4.2.12" - "@smithy/util-stream": "npm:^4.5.19" + "@smithy/util-defaults-mode-browser": "npm:^4.3.49" + "@smithy/util-defaults-mode-node": "npm:^4.2.54" + "@smithy/util-endpoints": "npm:^3.4.2" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-retry": "npm:^4.3.5" + "@smithy/util-stream": "npm:^4.5.25" "@smithy/util-utf8": "npm:^4.2.2" - "@smithy/util-waiter": "npm:^4.2.13" + "@smithy/util-waiter": "npm:^4.3.0" tslib: "npm:^2.6.2" - checksum: 10c0/45bae01e0079d9680442f3868a4f8b8a2b80cf734d26c31d86ea10294a268188edc4ef6fe3568a53004bc77c723981d19e79b3ca96d3aa813643a41f5c2765a8 + checksum: 10c0/0cc2223947cc11082d6c482563397f907de00423819e7af7b951906b9a566afe46bdda7f8a2789a3bb0ab714294d27519d83efce5d26f67d0d0b382e6ded4715 languageName: node linkType: hard -"@aws-sdk/core@npm:^3.973.20": - version: 3.973.20 - resolution: "@aws-sdk/core@npm:3.973.20" +"@aws-sdk/core@npm:^3.974.6": + version: 3.974.6 + resolution: "@aws-sdk/core@npm:3.974.6" dependencies: - "@aws-sdk/types": "npm:^3.973.6" - "@aws-sdk/xml-builder": "npm:^3.972.11" - "@smithy/core": "npm:^3.23.11" - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/signature-v4": "npm:^5.3.12" - "@smithy/smithy-client": "npm:^4.12.5" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/types": "npm:^3.973.8" + "@aws-sdk/xml-builder": "npm:^3.972.20" + "@smithy/core": "npm:^3.23.17" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/signature-v4": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" "@smithy/util-base64": "npm:^4.3.2" - "@smithy/util-middleware": "npm:^4.2.12" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-retry": "npm:^4.3.5" "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/ee632c2a1a6814911c6fa647c67b3f6ab09f8f6fa3bc4b4202f40e4160d953e7a697e8a1e6983f652a6bd41b159b3173e073fa5280293499027107e4334287f9 + checksum: 10c0/39f33562fefa000c48d19a03caa8791a015c5cd4fd4dbdb034d21486c9c4435aa5670c7b48e50c2a5ccc0ef1ea248567c20e158cfa643a03bf2b63bd1cda8b87 languageName: node linkType: hard -"@aws-sdk/crc64-nvme@npm:^3.972.5": - version: 3.972.5 - resolution: "@aws-sdk/crc64-nvme@npm:3.972.5" +"@aws-sdk/crc64-nvme@npm:^3.972.7": + version: 3.972.7 + resolution: "@aws-sdk/crc64-nvme@npm:3.972.7" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/2d25cc231d36a2292d83668338d778db8db7a3be3c36a097d047df0e97016293c01371401f0fde02b5d3ce52b9c4e0db19bf5746278d9ca89ed689b916a40cfc + checksum: 10c0/c6f23e4e4c06b98009264b511567bb808d4c4f53c1da9b41f5c975f5f9f5e4b11af16e7add850e7bba29731b6efb145eb4dc0538d9639d6a5daaceadb4acf35d languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:^3.972.18": - version: 3.972.18 - resolution: "@aws-sdk/credential-provider-env@npm:3.972.18" +"@aws-sdk/credential-provider-env@npm:^3.972.32": + version: 3.972.32 + resolution: "@aws-sdk/credential-provider-env@npm:3.972.32" dependencies: - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/8c8af7e90ae727ac380c079045700c1cdf0cf2d917a8738315ae83f323dccb6c71a85ef55b5b45604cbaeb180570d158aee6104e6c0bc3f071dbd6ed4f93b05f + checksum: 10c0/e79e908cbbb378c9aab1f600edd3094f3c999c8a119792c80f219b4a90f1c800242a31196198e578aa2290bc1d285bf09c668a032e75f23c64bcd983744dd9ab languageName: node linkType: hard -"@aws-sdk/credential-provider-http@npm:^3.972.20": - version: 3.972.20 - resolution: "@aws-sdk/credential-provider-http@npm:3.972.20" +"@aws-sdk/credential-provider-http@npm:^3.972.34": + version: 3.972.34 + resolution: "@aws-sdk/credential-provider-http@npm:3.972.34" dependencies: - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/fetch-http-handler": "npm:^5.3.15" - "@smithy/node-http-handler": "npm:^4.4.16" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/smithy-client": "npm:^4.12.5" - "@smithy/types": "npm:^4.13.1" - "@smithy/util-stream": "npm:^4.5.19" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/fetch-http-handler": "npm:^5.3.17" + "@smithy/node-http-handler": "npm:^4.6.1" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-stream": "npm:^4.5.25" tslib: "npm:^2.6.2" - checksum: 10c0/7a8475346e5904075d48764f0a653977951240aa7da6ddd5393ff4a64fad7d1338a57f297ee6a20849d8e84e90a22b7b8c23ffaf544412f83517bb36403ea4cd - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-ini@npm:^3.972.20": - version: 3.972.20 - resolution: "@aws-sdk/credential-provider-ini@npm:3.972.20" - dependencies: - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/credential-provider-env": "npm:^3.972.18" - "@aws-sdk/credential-provider-http": "npm:^3.972.20" - "@aws-sdk/credential-provider-login": "npm:^3.972.20" - "@aws-sdk/credential-provider-process": "npm:^3.972.18" - "@aws-sdk/credential-provider-sso": "npm:^3.972.20" - "@aws-sdk/credential-provider-web-identity": "npm:^3.972.20" - "@aws-sdk/nested-clients": "npm:^3.996.10" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/credential-provider-imds": "npm:^4.2.12" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/shared-ini-file-loader": "npm:^4.4.7" - "@smithy/types": "npm:^4.13.1" + checksum: 10c0/d102fb83c9acd09b1d89d8b9bdf2aba092a61109edbae7fe8ec2cdbe0aeeff1963646930f309588e1e9352ff4e081bab3f36d32f36201f14c19a0804632c9f2c + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:^3.972.36": + version: 3.972.36 + resolution: "@aws-sdk/credential-provider-ini@npm:3.972.36" + dependencies: + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/credential-provider-env": "npm:^3.972.32" + "@aws-sdk/credential-provider-http": "npm:^3.972.34" + "@aws-sdk/credential-provider-login": "npm:^3.972.36" + "@aws-sdk/credential-provider-process": "npm:^3.972.32" + "@aws-sdk/credential-provider-sso": "npm:^3.972.36" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.36" + "@aws-sdk/nested-clients": "npm:^3.997.4" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/credential-provider-imds": "npm:^4.2.14" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/f4ce3dd4cee3c48f68d1ab87b115b7b982b3abf47499d375d16c88ce3fa57f677967da962629023ae4687203f6ea278702b48619880f56aa425bbe7d41067065 + checksum: 10c0/e86b7d4896c461b2318f749d7440e68b02d00da7a62a4c594df1b0c78bcc30643bc85eddf43e880416e02532fa26d40411acbff1c1e1673de2e6881305f8d2ca languageName: node linkType: hard -"@aws-sdk/credential-provider-login@npm:^3.972.20": - version: 3.972.20 - resolution: "@aws-sdk/credential-provider-login@npm:3.972.20" +"@aws-sdk/credential-provider-login@npm:^3.972.36": + version: 3.972.36 + resolution: "@aws-sdk/credential-provider-login@npm:3.972.36" dependencies: - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/nested-clients": "npm:^3.996.10" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/shared-ini-file-loader": "npm:^4.4.7" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/nested-clients": "npm:^3.997.4" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/97844214ea37854efd23a7d6a1ae7bdba2ce61533d0c367286c945fa7f3a0bbace53fc9c1f6dbf174a1cb868625c16f66906603fe2f54aad68c23e4f92c25548 + checksum: 10c0/9446602c8d8bdca2ca529b45c91ec24ae4d877b380d7ff2ea1c333b77651de8556dc0632595ab2a33868a66e84fcbbb1cd3f25b2b1985025ab39a9d4fe3ef5b0 languageName: node linkType: hard -"@aws-sdk/credential-provider-node@npm:^3.972.21": - version: 3.972.21 - resolution: "@aws-sdk/credential-provider-node@npm:3.972.21" - dependencies: - "@aws-sdk/credential-provider-env": "npm:^3.972.18" - "@aws-sdk/credential-provider-http": "npm:^3.972.20" - "@aws-sdk/credential-provider-ini": "npm:^3.972.20" - "@aws-sdk/credential-provider-process": "npm:^3.972.18" - "@aws-sdk/credential-provider-sso": "npm:^3.972.20" - "@aws-sdk/credential-provider-web-identity": "npm:^3.972.20" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/credential-provider-imds": "npm:^4.2.12" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/shared-ini-file-loader": "npm:^4.4.7" - "@smithy/types": "npm:^4.13.1" +"@aws-sdk/credential-provider-node@npm:^3.972.37": + version: 3.972.37 + resolution: "@aws-sdk/credential-provider-node@npm:3.972.37" + dependencies: + "@aws-sdk/credential-provider-env": "npm:^3.972.32" + "@aws-sdk/credential-provider-http": "npm:^3.972.34" + "@aws-sdk/credential-provider-ini": "npm:^3.972.36" + "@aws-sdk/credential-provider-process": "npm:^3.972.32" + "@aws-sdk/credential-provider-sso": "npm:^3.972.36" + "@aws-sdk/credential-provider-web-identity": "npm:^3.972.36" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/credential-provider-imds": "npm:^4.2.14" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/cecf760a3048e535dc79e4a24ce0ade784ba2f287d53a7fc63b541687870ded2f1938cd8c415414e794b50a1e0c4f29badcdd39fc8f57c58e8e7a83260ef60f8 + checksum: 10c0/6e87376ee86abbc9e3ff7e92d60031342e9e002d4ebb589150dec43a755f9b3012a6083f293241ff43919938fc35bdc3a21b1e20fe1ffbf2b1c0f982186e1f53 languageName: node linkType: hard -"@aws-sdk/credential-provider-process@npm:^3.972.18": - version: 3.972.18 - resolution: "@aws-sdk/credential-provider-process@npm:3.972.18" +"@aws-sdk/credential-provider-process@npm:^3.972.32": + version: 3.972.32 + resolution: "@aws-sdk/credential-provider-process@npm:3.972.32" dependencies: - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/shared-ini-file-loader": "npm:^4.4.7" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/ddbb1fd6590fd4fcdd0bf8b2a749e3bc356697765747b0aff01a47cdd6da153ffa77b95de5fc2cfbf1acdfbbc49d710572aee3087034c9977923af1ec505783c + checksum: 10c0/9f3d91414a82b88400220898dc2f49f0aaea4ba84099fbc16c618d24f561d76fed7c39fb90264737fe9a249bb3ef2a74311a217dbb28799f84830668f0818b9a languageName: node linkType: hard -"@aws-sdk/credential-provider-sso@npm:^3.972.20": - version: 3.972.20 - resolution: "@aws-sdk/credential-provider-sso@npm:3.972.20" +"@aws-sdk/credential-provider-sso@npm:^3.972.36": + version: 3.972.36 + resolution: "@aws-sdk/credential-provider-sso@npm:3.972.36" dependencies: - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/nested-clients": "npm:^3.996.10" - "@aws-sdk/token-providers": "npm:3.1009.0" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/shared-ini-file-loader": "npm:^4.4.7" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/nested-clients": "npm:^3.997.4" + "@aws-sdk/token-providers": "npm:3.1038.0" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/641796c879a526576d5bc3f82ab405f2883409aea583dba939a642d9ac1a920f64543b5c76cf1748a580db5e26f7789a887197e385fef81f366a4338c2a11785 + checksum: 10c0/610ca7ae5e3f9fdb922a7a90c3a066655129e90de9b4887734dc2aaa4a4569c7539e927eaad8c313e338556345c2794ad42ac9ba4a63aa0796f9f2e0b06dff93 languageName: node linkType: hard -"@aws-sdk/credential-provider-web-identity@npm:^3.972.20": - version: 3.972.20 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.20" +"@aws-sdk/credential-provider-web-identity@npm:^3.972.36": + version: 3.972.36 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.972.36" dependencies: - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/nested-clients": "npm:^3.996.10" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/shared-ini-file-loader": "npm:^4.4.7" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/nested-clients": "npm:^3.997.4" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/581139aa40b2e82f7b8a7aa2b69184aadaf6453c91f79662e6e5647f383884fe3b7cb522205d5d8fdd5985e24f0ee5c320c0d3e813654e99dd41b819a01c4a71 + checksum: 10c0/a7a51795252fc8ca4f28d866c436245b291dd411e12b8da9cebcfc839aea2a3b54ea2c7b905f5cfeabb3ad7ddd13fa8ff86e6e0d467675854dbf3dd2f7d06fc2 languageName: node linkType: hard "@aws-sdk/lib-storage@npm:^3.0.0": - version: 3.1009.0 - resolution: "@aws-sdk/lib-storage@npm:3.1009.0" + version: 3.1038.0 + resolution: "@aws-sdk/lib-storage@npm:3.1038.0" dependencies: - "@smithy/abort-controller": "npm:^4.2.12" - "@smithy/middleware-endpoint": "npm:^4.4.25" - "@smithy/smithy-client": "npm:^4.12.5" + "@smithy/middleware-endpoint": "npm:^4.4.32" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" buffer: "npm:5.6.0" events: "npm:3.3.0" stream-browserify: "npm:3.0.0" tslib: "npm:^2.6.2" peerDependencies: - "@aws-sdk/client-s3": ^3.1009.0 - checksum: 10c0/24a8a133a334ca0403a7899e5897a4ac946abee2dfc18a45e5929c9065df051a5ed74a6c97c13ce455c202b5ba41818ae56d931eda3d08f628723811e6d1deb9 + "@aws-sdk/client-s3": ^3.1038.0 + checksum: 10c0/b269f3245d9284b527c3981e6d88300b5cbdd7bbdb00e8eec5c6308dc52ffd532b35c800cdb161a24d274ed515753786be6ba9b9b72ff9af35f72fd602c04f1a languageName: node linkType: hard -"@aws-sdk/middleware-bucket-endpoint@npm:^3.972.8": - version: 3.972.8 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.972.8" +"@aws-sdk/middleware-bucket-endpoint@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.972.10" dependencies: - "@aws-sdk/types": "npm:^3.973.6" + "@aws-sdk/types": "npm:^3.973.8" "@aws-sdk/util-arn-parser": "npm:^3.972.3" - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" "@smithy/util-config-provider": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/03cb3ae1e28cd1f7abcd6363cbba5f550a1fe3d0daf9752ec758f8928e2dc7a1eed9e21a6c94c31760dc96ca60984910384a3c3599047f44c9148953dc0683bc + checksum: 10c0/5529288142e0ebfbd985a257dbbb0f3510981a6ef56ce449465458ca12f7bcbbb9bfba9e1788c925329443604d4a438275f30254ef1d47e7931da798fb6e6765 languageName: node linkType: hard -"@aws-sdk/middleware-expect-continue@npm:^3.972.8": - version: 3.972.8 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.972.8" +"@aws-sdk/middleware-expect-continue@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.972.10" dependencies: - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/dbfb4b54aea5d43fa49fae9c55c5f3cd9e274c06c9a285795a9ba8cdb8e70062a1f05fa44f4cbc03374cc198f423c5f7c97d888485eb52334658323348449c99 + checksum: 10c0/c91588169621597bed09aa53f9bf858b83a0c8b8b84d6838ed3729c8222a7b7d5819595fd2617ca8f859e8471ca7e7132bb7d1694d5ada17039dea53cc707e4b languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:^3.973.6": - version: 3.973.6 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.973.6" +"@aws-sdk/middleware-flexible-checksums@npm:^3.974.14": + version: 3.974.14 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.974.14" dependencies: "@aws-crypto/crc32": "npm:5.2.0" "@aws-crypto/crc32c": "npm:5.2.0" "@aws-crypto/util": "npm:5.2.0" - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/crc64-nvme": "npm:^3.972.5" - "@aws-sdk/types": "npm:^3.973.6" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/crc64-nvme": "npm:^3.972.7" + "@aws-sdk/types": "npm:^3.973.8" "@smithy/is-array-buffer": "npm:^4.2.2" - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" - "@smithy/util-middleware": "npm:^4.2.12" - "@smithy/util-stream": "npm:^4.5.19" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-stream": "npm:^4.5.25" "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/8f6fc640e590a5d47abefea459249771cd0b8c823902cd620290e452a3bb0ad7314eed8b2a388db39fe31ae76d2bfeecceabcc60560e1311cd4fe5eb0ab0a6e6 + checksum: 10c0/693183ea334354dade00b0469dd85363490ce49c28cce57fdfe0845288a0503b69c89c652fa84b47ba784635c137e38777fa6833d7d752e9190395530b28a0eb languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:^3.972.8": - version: 3.972.8 - resolution: "@aws-sdk/middleware-host-header@npm:3.972.8" +"@aws-sdk/middleware-host-header@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/middleware-host-header@npm:3.972.10" dependencies: - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/f3019810e447a53788c546b94bc40a20c543aa067abf6235643d8e24689f8d4edec211297ac464380fb58c79f99803d1a152027798a3b401eab225e679a85d07 + checksum: 10c0/e631b48f8d8fd40f8977da8d1fc012208f953000dd5645dc0a700c7283af8fafb996c9c1c50e40b8392c402ad98d11e0ddddb949896db5163a7f06e2385e619a languageName: node linkType: hard -"@aws-sdk/middleware-location-constraint@npm:^3.972.8": - version: 3.972.8 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.972.8" +"@aws-sdk/middleware-location-constraint@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.972.10" dependencies: - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/3819ad39a601cccb0a9743b13b37dbbdf3e2f7c3c34d15d4b09ef50f78683489502b1125f31b30ba45924db5c3fbc8b2d1be3cb31a443a53538fc1eb36615eff + checksum: 10c0/ef8ef1f3cf7d28e5b02edcc2b62cab07a380f7a02983bdfcaf24fbea35129c53ac5a1f5846ab28212b649d6c81f437e2a846f1f954fb374509ea174201bf09d4 languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:^3.972.8": - version: 3.972.8 - resolution: "@aws-sdk/middleware-logger@npm:3.972.8" +"@aws-sdk/middleware-logger@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/middleware-logger@npm:3.972.10" dependencies: - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/79240b2a34d020f90f54982a4744b0a6bc5b5a7de6442f3b6657b2f10a76d9a1d3bcc2887a1d96d0aa5da4a09b3ce2a77df7a0d4e7e2973d1797ff6d8e8800a9 + checksum: 10c0/a24e0c98b3cf6c9b7960bf8979d2ab8f839fb89294ba8943136d99dbe7370cc45b010460ed7f5bf14ab6ad8e113ccec6387cf1bda655bcbdb58722df21d9b713 languageName: node linkType: hard -"@aws-sdk/middleware-recursion-detection@npm:^3.972.8": - version: 3.972.8 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.972.8" +"@aws-sdk/middleware-recursion-detection@npm:^3.972.11": + version: 3.972.11 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.972.11" dependencies: - "@aws-sdk/types": "npm:^3.973.6" + "@aws-sdk/types": "npm:^3.973.8" "@aws/lambda-invoke-store": "npm:^0.2.2" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/8d8ef442befd65dd9175294ae292e2b421171c0c9db9389a6f504b97e055dc9c3b51a80c711792fbc31cd3b4976f1d71d30a378063416553e17a59f70e7eb6d1 + checksum: 10c0/e52501b00e79e714218897ddec9edd40ecf33fdb43c19b00195c8ed71c8295d0d148f07465465d464f103a23aa535dc1daf60bbb5b95303e330767a5eba78f54 languageName: node linkType: hard -"@aws-sdk/middleware-sdk-s3@npm:^3.972.20": - version: 3.972.20 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.972.20" +"@aws-sdk/middleware-sdk-s3@npm:^3.972.35": + version: 3.972.35 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.972.35" dependencies: - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/types": "npm:^3.973.6" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/types": "npm:^3.973.8" "@aws-sdk/util-arn-parser": "npm:^3.972.3" - "@smithy/core": "npm:^3.23.11" - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/signature-v4": "npm:^5.3.12" - "@smithy/smithy-client": "npm:^4.12.5" - "@smithy/types": "npm:^4.13.1" + "@smithy/core": "npm:^3.23.17" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/signature-v4": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" "@smithy/util-config-provider": "npm:^4.2.2" - "@smithy/util-middleware": "npm:^4.2.12" - "@smithy/util-stream": "npm:^4.5.19" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-stream": "npm:^4.5.25" "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/b504c38ea983c86a409359d03a60ab00043187c37d13ad6c7221c1fe790d777216fa994d0cab9aacea38b5664adf071235299bb9aeed75d134cd2bdd80834f0a + checksum: 10c0/8e62f0ecea9f7cfad4ef8d737965960f125c60c6d510c0ea1478f1d906812a8fad46832c305da89c4396d4ab4260b94ab42941c3fb0c17c583be1e9db47c9ad7 languageName: node linkType: hard -"@aws-sdk/middleware-ssec@npm:^3.972.8": - version: 3.972.8 - resolution: "@aws-sdk/middleware-ssec@npm:3.972.8" +"@aws-sdk/middleware-ssec@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/middleware-ssec@npm:3.972.10" dependencies: - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/0d90f48273bd668d9aafe233bd4cc7e16dcda52761202ab4af377e94a112bbd4b5f0939e8dee0f85f8d17c36f1b9e565889bd3d20545145787850479bcf82651 + checksum: 10c0/13e11c485e63d6b3d8a5f14888c6b8aea1c0a96a99826e840840b6974b9605b5f528f8869b021036e8615995d9e5ecd33d6e67af1561ed673d37292d356ca441 languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:^3.972.21": - version: 3.972.21 - resolution: "@aws-sdk/middleware-user-agent@npm:3.972.21" - dependencies: - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/types": "npm:^3.973.6" - "@aws-sdk/util-endpoints": "npm:^3.996.5" - "@smithy/core": "npm:^3.23.11" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" - "@smithy/util-retry": "npm:^4.2.12" +"@aws-sdk/middleware-user-agent@npm:^3.972.36": + version: 3.972.36 + resolution: "@aws-sdk/middleware-user-agent@npm:3.972.36" + dependencies: + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/types": "npm:^3.973.8" + "@aws-sdk/util-endpoints": "npm:^3.996.8" + "@smithy/core": "npm:^3.23.17" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-retry": "npm:^4.3.5" tslib: "npm:^2.6.2" - checksum: 10c0/ba94964dfcf123c03cc26beeb741f02e0f9274f4c2b15ad93d1d422af03ee534964dee6f3b6439cebaba531243777a738cc34051ec97f2b9c6fc9cfe96f2c6f4 + checksum: 10c0/abba01666b59387a3db94cbee5a289ddac90a5ab03dd9b58f8617c04110e5da7969e529008b966de6981e0ef4d6cdefda1174ff806cde0a14cae38b18d215459 languageName: node linkType: hard -"@aws-sdk/nested-clients@npm:^3.996.10": - version: 3.996.10 - resolution: "@aws-sdk/nested-clients@npm:3.996.10" +"@aws-sdk/nested-clients@npm:^3.997.4": + version: 3.997.4 + resolution: "@aws-sdk/nested-clients@npm:3.997.4" dependencies: "@aws-crypto/sha256-browser": "npm:5.2.0" "@aws-crypto/sha256-js": "npm:5.2.0" - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/middleware-host-header": "npm:^3.972.8" - "@aws-sdk/middleware-logger": "npm:^3.972.8" - "@aws-sdk/middleware-recursion-detection": "npm:^3.972.8" - "@aws-sdk/middleware-user-agent": "npm:^3.972.21" - "@aws-sdk/region-config-resolver": "npm:^3.972.8" - "@aws-sdk/types": "npm:^3.973.6" - "@aws-sdk/util-endpoints": "npm:^3.996.5" - "@aws-sdk/util-user-agent-browser": "npm:^3.972.8" - "@aws-sdk/util-user-agent-node": "npm:^3.973.7" - "@smithy/config-resolver": "npm:^4.4.11" - "@smithy/core": "npm:^3.23.11" - "@smithy/fetch-http-handler": "npm:^5.3.15" - "@smithy/hash-node": "npm:^4.2.12" - "@smithy/invalid-dependency": "npm:^4.2.12" - "@smithy/middleware-content-length": "npm:^4.2.12" - "@smithy/middleware-endpoint": "npm:^4.4.25" - "@smithy/middleware-retry": "npm:^4.4.42" - "@smithy/middleware-serde": "npm:^4.2.14" - "@smithy/middleware-stack": "npm:^4.2.12" - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/node-http-handler": "npm:^4.4.16" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/smithy-client": "npm:^4.12.5" - "@smithy/types": "npm:^4.13.1" - "@smithy/url-parser": "npm:^4.2.12" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/middleware-host-header": "npm:^3.972.10" + "@aws-sdk/middleware-logger": "npm:^3.972.10" + "@aws-sdk/middleware-recursion-detection": "npm:^3.972.11" + "@aws-sdk/middleware-user-agent": "npm:^3.972.36" + "@aws-sdk/region-config-resolver": "npm:^3.972.13" + "@aws-sdk/signature-v4-multi-region": "npm:^3.996.23" + "@aws-sdk/types": "npm:^3.973.8" + "@aws-sdk/util-endpoints": "npm:^3.996.8" + "@aws-sdk/util-user-agent-browser": "npm:^3.972.10" + "@aws-sdk/util-user-agent-node": "npm:^3.973.22" + "@smithy/config-resolver": "npm:^4.4.17" + "@smithy/core": "npm:^3.23.17" + "@smithy/fetch-http-handler": "npm:^5.3.17" + "@smithy/hash-node": "npm:^4.2.14" + "@smithy/invalid-dependency": "npm:^4.2.14" + "@smithy/middleware-content-length": "npm:^4.2.14" + "@smithy/middleware-endpoint": "npm:^4.4.32" + "@smithy/middleware-retry": "npm:^4.5.6" + "@smithy/middleware-serde": "npm:^4.2.20" + "@smithy/middleware-stack": "npm:^4.2.14" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/node-http-handler": "npm:^4.6.1" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" "@smithy/util-base64": "npm:^4.3.2" "@smithy/util-body-length-browser": "npm:^4.2.2" "@smithy/util-body-length-node": "npm:^4.2.3" - "@smithy/util-defaults-mode-browser": "npm:^4.3.41" - "@smithy/util-defaults-mode-node": "npm:^4.2.44" - "@smithy/util-endpoints": "npm:^3.3.3" - "@smithy/util-middleware": "npm:^4.2.12" - "@smithy/util-retry": "npm:^4.2.12" + "@smithy/util-defaults-mode-browser": "npm:^4.3.49" + "@smithy/util-defaults-mode-node": "npm:^4.2.54" + "@smithy/util-endpoints": "npm:^3.4.2" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-retry": "npm:^4.3.5" "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/fc0083d9991e2e8ab82d5be45a6e40f9f31b183b61450221bd3bcbd55bd55c6d8ac0899dd2c479e8f513f265f776385b0faa20a887d0a78e51ff5793acea3bcf + checksum: 10c0/a5fc46ba3b2fa8204519c24a0341641f368def8bda7a188fb9278dc0612659720ac1a887cdf9419d137340c1cb7e03070e13c7f336ea9473abbd79c8a0b5b46f languageName: node linkType: hard -"@aws-sdk/region-config-resolver@npm:^3.972.8": - version: 3.972.8 - resolution: "@aws-sdk/region-config-resolver@npm:3.972.8" +"@aws-sdk/region-config-resolver@npm:^3.972.13": + version: 3.972.13 + resolution: "@aws-sdk/region-config-resolver@npm:3.972.13" dependencies: - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/config-resolver": "npm:^4.4.11" - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/config-resolver": "npm:^4.4.17" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/9532a6516053a15d317c4b114492fb47c9411a4a7bded7161738c59c85df19a4b14120e376573c48b23d7a6c84c5a9e29b4f6f8e9cf0d22882e12b27001704a5 + checksum: 10c0/8cc3e5433ccf9ec4efb6d12ccd924701cfd6fb018124c9e5106486da10402ea1b83b0855353dae5e0f31ad2c7b6d962ac832350a5cdbeda59a30231525fd1118 languageName: node linkType: hard -"@aws-sdk/signature-v4-multi-region@npm:^3.996.8": - version: 3.996.8 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.996.8" +"@aws-sdk/signature-v4-multi-region@npm:^3.996.23": + version: 3.996.23 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.996.23" dependencies: - "@aws-sdk/middleware-sdk-s3": "npm:^3.972.20" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/signature-v4": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/middleware-sdk-s3": "npm:^3.972.35" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/signature-v4": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/a463afb7b9b425fa513af8875866a06c4b832c914eb2a712f480f75cbfe05680633dc915c2955ce0fc29131d2aad51fe10d9222193c87b01e94f079a0f3b24d2 + checksum: 10c0/4053f41d58b36bc18ec78d64c6cbca1ebf714050d31d9554ed850f2ad7a3e932a1607806cb4e9c966fb8cabf8e8297ccb7037d6222eccf23d85a3c079a2e5fea languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.1009.0": - version: 3.1009.0 - resolution: "@aws-sdk/token-providers@npm:3.1009.0" +"@aws-sdk/token-providers@npm:3.1038.0": + version: 3.1038.0 + resolution: "@aws-sdk/token-providers@npm:3.1038.0" dependencies: - "@aws-sdk/core": "npm:^3.973.20" - "@aws-sdk/nested-clients": "npm:^3.996.10" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/shared-ini-file-loader": "npm:^4.4.7" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/core": "npm:^3.974.6" + "@aws-sdk/nested-clients": "npm:^3.997.4" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/607345ebef04a5d32d53f61d2411da42155268a736b0f12e1094ae1a462d3deba5cab62aba9510987c4ae7d60582fcd8caf44c0ab52d81677cef10a0ca01fc1b + checksum: 10c0/88bbd39f9eeddcbc337853ccbc9e6c16ee47676509c47af80b5bad55852fa209f18b26d287d5431cdaf0b4207431213408ef4e8ed0b27f4eafd4eb77be0a3910 languageName: node linkType: hard -"@aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.973.6": - version: 3.973.6 - resolution: "@aws-sdk/types@npm:3.973.6" +"@aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.973.8": + version: 3.973.8 + resolution: "@aws-sdk/types@npm:3.973.8" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/3a5c65313a3faadf854dd1055e5768c0477ecd10e8a597d0c0041fb69efdcefc399bf263f86fef93754d2d9a91d4f0eb78f5f1de14779657f84a24218a457fc3 + checksum: 10c0/4823579e7dd0f6dcce9e9fea9630091eb46e3596bc468614a072f682b1eab6f048854ccf5e9398459ada175c13dfc636ffa8c1d3aa655cf6f22447f9c1a02f7f languageName: node linkType: hard @@ -584,16 +587,16 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-endpoints@npm:^3.996.5": - version: 3.996.5 - resolution: "@aws-sdk/util-endpoints@npm:3.996.5" +"@aws-sdk/util-endpoints@npm:^3.996.8": + version: 3.996.8 + resolution: "@aws-sdk/util-endpoints@npm:3.996.8" dependencies: - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/types": "npm:^4.13.1" - "@smithy/url-parser": "npm:^4.2.12" - "@smithy/util-endpoints": "npm:^3.3.3" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" + "@smithy/util-endpoints": "npm:^3.4.2" tslib: "npm:^2.6.2" - checksum: 10c0/6356b7b040758af210f6b3d6807c11538e8a6888093ebe8a172949532a170c1f3f0bf93db86f6a75f071749219c3da2a88e63954f53031e8c3f9a092d7d97db9 + checksum: 10c0/66f17e85357ab8e265ab7d1c9a69a064ad3bea99420c786be9ef1f92bc71dca22d328bbceeaf6c64b4a3c37161b78318a3e4a424bf247dcb211d7ae29344db2f languageName: node linkType: hard @@ -606,26 +609,26 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:^3.972.8": - version: 3.972.8 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.972.8" +"@aws-sdk/util-user-agent-browser@npm:^3.972.10": + version: 3.972.10 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.972.10" dependencies: - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/types": "npm:^4.14.1" bowser: "npm:^2.11.0" tslib: "npm:^2.6.2" - checksum: 10c0/b5153800fab17e3e079c87d0668b65625755c91a47646aabcfc434aad18d6fc0c8921b544a234cd89d11a0b29eef1b73087515438c185ea5bcff75ecb8c2e800 + checksum: 10c0/e139dda2cb51a0a3553c80ec6f89c39cb1292876c116f8449f21a7fdbe39bd7b545ef8ea69a447dd9fa2aa43c99f625ee6a55cbf6d8e6cf2094d0ef00ceb1e36 languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:^3.973.7": - version: 3.973.7 - resolution: "@aws-sdk/util-user-agent-node@npm:3.973.7" +"@aws-sdk/util-user-agent-node@npm:^3.973.22": + version: 3.973.22 + resolution: "@aws-sdk/util-user-agent-node@npm:3.973.22" dependencies: - "@aws-sdk/middleware-user-agent": "npm:^3.972.21" - "@aws-sdk/types": "npm:^3.973.6" - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/types": "npm:^4.13.1" + "@aws-sdk/middleware-user-agent": "npm:^3.972.36" + "@aws-sdk/types": "npm:^3.973.8" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/types": "npm:^4.14.1" "@smithy/util-config-provider": "npm:^4.2.2" tslib: "npm:^2.6.2" peerDependencies: @@ -633,18 +636,19 @@ __metadata: peerDependenciesMeta: aws-crt: optional: true - checksum: 10c0/6305796caf8c72279325d6c851a593fe57f02c3159da7292c5afd5048448a483f607c269e7c784c169d6aba066d4d19899937d896d7e8d1013b0f060f575afce + checksum: 10c0/2fdda6636515dd84fcef3ce0009450c7133dd80da287212f574f3543af8a1e1a3fa2df86134d797b6237d8d8a05087390e0d79ec226585531addcbb67831929d languageName: node linkType: hard -"@aws-sdk/xml-builder@npm:^3.972.11": - version: 3.972.11 - resolution: "@aws-sdk/xml-builder@npm:3.972.11" +"@aws-sdk/xml-builder@npm:^3.972.20": + version: 3.972.21 + resolution: "@aws-sdk/xml-builder@npm:3.972.21" dependencies: - "@smithy/types": "npm:^4.13.1" - fast-xml-parser: "npm:5.4.1" + "@nodable/entities": "npm:2.1.0" + "@smithy/types": "npm:^4.14.1" + fast-xml-parser: "npm:5.7.2" tslib: "npm:^2.6.2" - checksum: 10c0/ba933261da8a0ee2d6ee6468229012ee21eb397c7e37848c8a1d8eb029a6ef865c068d0f839158939567fb6b85cca5f77261e92c24bc547e41252358cd9f19f3 + checksum: 10c0/58f0c7a13bd7c67ddf38fedd728b8b78df23997793325811c6980fb2f4dbbe8768be902c0de0527e441e3b23e0d358982d42493a72105f104cab53aab1288886 languageName: node linkType: hard @@ -674,16 +678,7 @@ __metadata: languageName: node linkType: hard -"@gar/promise-retry@npm:^1.0.0": - version: 1.0.2 - resolution: "@gar/promise-retry@npm:1.0.2" - dependencies: - retry: "npm:^0.13.1" - checksum: 10c0/748a84fb0ab962f7867966f21dc24d1872c53c1656dd3352320fe69ad3b2043f2dfdb3be024c7636ce4904c5ba1da22d0f3558e489c3de578f5bb520f062d0fd - languageName: node - linkType: hard - -"@gerrit0/mini-shiki@npm:^3.17.0": +"@gerrit0/mini-shiki@npm:^3.23.0": version: 3.23.0 resolution: "@gerrit0/mini-shiki@npm:3.23.0" dependencies: @@ -719,25 +714,10 @@ __metadata: languageName: node linkType: hard -"@npmcli/agent@npm:^4.0.0": - version: 4.0.0 - resolution: "@npmcli/agent@npm:4.0.0" - dependencies: - agent-base: "npm:^7.1.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - lru-cache: "npm:^11.2.1" - socks-proxy-agent: "npm:^8.0.3" - checksum: 10c0/f7b5ce0f3dd42c3f8c6546e8433573d8049f67ef11ec22aa4704bc41483122f68bf97752e06302c455ead667af5cb753e6a09bff06632bc465c1cfd4c4b75a53 - languageName: node - linkType: hard - -"@npmcli/fs@npm:^5.0.0": - version: 5.0.0 - resolution: "@npmcli/fs@npm:5.0.0" - dependencies: - semver: "npm:^7.3.5" - checksum: 10c0/26e376d780f60ff16e874a0ac9bc3399186846baae0b6e1352286385ac134d900cc5dafaded77f38d77f86898fc923ae1cee9d7399f0275b1aa24878915d722b +"@nodable/entities@npm:2.1.0, @nodable/entities@npm:^2.1.0": + version: 2.1.0 + resolution: "@nodable/entities@npm:2.1.0" + checksum: 10c0/5a4cba2b61a5b6c726328b18b1de6d033cae4a658a118644bf31e0bcbda126ea7b69385043dc556cf1ed859b9ca220e82b81b5e5c48ef1b519fb8ec104575dee languageName: node linkType: hard @@ -800,16 +780,6 @@ __metadata: languageName: node linkType: hard -"@smithy/abort-controller@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/abort-controller@npm:4.2.12" - dependencies: - "@smithy/types": "npm:^4.13.1" - tslib: "npm:^2.6.2" - checksum: 10c0/839bee519c6bc4cf405395f71a07d0b5b42c22ce1c0163a157a61e18804d5dacd4ade1a3b2b69fea26462eecff4c92593726e96318f16ea8adfb419e7f3dab43 - languageName: node - linkType: hard - "@smithy/chunked-blob-reader-native@npm:^4.2.3": version: 4.2.3 resolution: "@smithy/chunked-blob-reader-native@npm:4.2.3" @@ -829,161 +799,161 @@ __metadata: languageName: node linkType: hard -"@smithy/config-resolver@npm:^4.4.11": - version: 4.4.11 - resolution: "@smithy/config-resolver@npm:4.4.11" +"@smithy/config-resolver@npm:^4.4.17": + version: 4.4.17 + resolution: "@smithy/config-resolver@npm:4.4.17" dependencies: - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/types": "npm:^4.14.1" "@smithy/util-config-provider": "npm:^4.2.2" - "@smithy/util-endpoints": "npm:^3.3.3" - "@smithy/util-middleware": "npm:^4.2.12" + "@smithy/util-endpoints": "npm:^3.4.2" + "@smithy/util-middleware": "npm:^4.2.14" tslib: "npm:^2.6.2" - checksum: 10c0/5769c91e4dc82411bb476836fdc1a6f354c03dbcc8a8693b098655c0aadb4f117b0b58a3f2a2397f55991e24247b810cd0994e691ed62083a419ebe49a1550b0 + checksum: 10c0/44e653e2ed31bf765f0b30ca404dc3e02f30ad800971f812a6363b71da8d37de5d7d8901d9db4d86d2493f25037904f35c01bbb1a83a2a72e7e7b201ce5c411a languageName: node linkType: hard -"@smithy/core@npm:^3.23.11": - version: 3.23.11 - resolution: "@smithy/core@npm:3.23.11" +"@smithy/core@npm:^3.23.17": + version: 3.23.17 + resolution: "@smithy/core@npm:3.23.17" dependencies: - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" - "@smithy/url-parser": "npm:^4.2.12" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" "@smithy/util-base64": "npm:^4.3.2" "@smithy/util-body-length-browser": "npm:^4.2.2" - "@smithy/util-middleware": "npm:^4.2.12" - "@smithy/util-stream": "npm:^4.5.19" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-stream": "npm:^4.5.25" "@smithy/util-utf8": "npm:^4.2.2" "@smithy/uuid": "npm:^1.1.2" tslib: "npm:^2.6.2" - checksum: 10c0/29d796cafcb971172253f937934d55d205ca73a504683cddc68a1f88326e47f68cf0c32b61c8d3071a9ff0a4ee1bafa4600c806f1c9fd343176baf253ba93628 + checksum: 10c0/223631835e93c314a8fa394db724673c0940d3ba9e5ffbd73bd7c09854d7e003d19de950b44ce408e099c72ed3c22eb5e41370abea4ac656f7037e6da07be3c1 languageName: node linkType: hard -"@smithy/credential-provider-imds@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/credential-provider-imds@npm:4.2.12" +"@smithy/credential-provider-imds@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/credential-provider-imds@npm:4.2.14" dependencies: - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/types": "npm:^4.13.1" - "@smithy/url-parser": "npm:^4.2.12" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" tslib: "npm:^2.6.2" - checksum: 10c0/23cadc858a8eb16da9212c7741f53bf92e8ff8bbae0c42194ec076c8cac40b7c2f4e2e2079bfedf5b85384a534876693d7631a27ecae2f4a67af313bb0994869 + checksum: 10c0/62ced0249cb1ba64c6dd98a90b35b93dd9e3f1469d020752c46b5a83ecef38280f4b29c2f63e3b0c414a2fa2ec7631a19370415c80ed4d6ea18a9be040803126 languageName: node linkType: hard -"@smithy/eventstream-codec@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/eventstream-codec@npm:4.2.12" +"@smithy/eventstream-codec@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/eventstream-codec@npm:4.2.14" dependencies: "@aws-crypto/crc32": "npm:5.2.0" - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" "@smithy/util-hex-encoding": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/a593745d2a8b2f23bf6d177db3a91c11b49763cdbc26076b3cde6baeccbba68405a63902d217d31172a8f7dfb16ac44f88fd5e8130271b7d74332b6812d8b9cc + checksum: 10c0/c2f9139004b3f75d3621b21e0ef80f850baf4955cce230e6d18faef171c2b4dc62694b1d86d4000a7ec1babb482afeca666520f69cf31455ed63259da6b2a3ee languageName: node linkType: hard -"@smithy/eventstream-serde-browser@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/eventstream-serde-browser@npm:4.2.12" +"@smithy/eventstream-serde-browser@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/eventstream-serde-browser@npm:4.2.14" dependencies: - "@smithy/eventstream-serde-universal": "npm:^4.2.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/eventstream-serde-universal": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/8eb80511c38f4a2f15248b86ba71abfabda67d9459d3f18e438848719ed8176feb3df071f5869c474ab14687c0beb7f497f2114570cbdfe7969e410184a00dfd + checksum: 10c0/b624cf962ec84bbc61c419938de07548cbff3b1049fe5fb0c88097bdb516e6f3af22f6856ab3a5212cf352e7f1c69b0c5d03370cb4fe7f91a29dff975c385da5 languageName: node linkType: hard -"@smithy/eventstream-serde-config-resolver@npm:^4.3.12": - version: 4.3.12 - resolution: "@smithy/eventstream-serde-config-resolver@npm:4.3.12" +"@smithy/eventstream-serde-config-resolver@npm:^4.3.14": + version: 4.3.14 + resolution: "@smithy/eventstream-serde-config-resolver@npm:4.3.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/85fdcf22c19ca16fadfcade3cf53c3ccb75149c726cb94aaa4414da12c89459499107522ee29763a6ce2a3912ec729aa19802b26c4005a06de30b02b2467ea43 + checksum: 10c0/d889a6e12797928c9507e99193f86ba0b7807441b67305643ec6b8b953c54237aa0ae7a4baaf00eb72ff07e3c71e88d6225de3db3d2b33729af38adb81b593f8 languageName: node linkType: hard -"@smithy/eventstream-serde-node@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/eventstream-serde-node@npm:4.2.12" +"@smithy/eventstream-serde-node@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/eventstream-serde-node@npm:4.2.14" dependencies: - "@smithy/eventstream-serde-universal": "npm:^4.2.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/eventstream-serde-universal": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/b775e9d7231afca467442d5d6ba9414fa5734446f02b4277ab274be127a34dd10add79ca845abbb947e6292d1359df6a8c877e7cf4a905f6eb2a18296b3cfd58 + checksum: 10c0/c9dda5011ef3e6565dfa483811567b942fd822dfefb41768213fc9322b5298075c4a9228f8aa745af5bcebb23a2a61e24f091ce2be263da1ca3713aafa89c243 languageName: node linkType: hard -"@smithy/eventstream-serde-universal@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/eventstream-serde-universal@npm:4.2.12" +"@smithy/eventstream-serde-universal@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/eventstream-serde-universal@npm:4.2.14" dependencies: - "@smithy/eventstream-codec": "npm:^4.2.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/eventstream-codec": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/e26efc2e2e0a44e529181a7932bc549ffd26c93393b8b2e5a57054178fb062d92c07318aa6c9ad2f424a1721aec3e791c6439c2aca021e74214f2a58ad1becf6 + checksum: 10c0/82cf563ff67b6543f0981dc3b1ef7fc3b507d5322e611a4f7fde4ae90d1d0eae172298c5bdda3837c5dd5c4d8839d59b72189797e178709be7b1996b3ea71a64 languageName: node linkType: hard -"@smithy/fetch-http-handler@npm:^5.3.15": - version: 5.3.15 - resolution: "@smithy/fetch-http-handler@npm:5.3.15" +"@smithy/fetch-http-handler@npm:^5.3.17": + version: 5.3.17 + resolution: "@smithy/fetch-http-handler@npm:5.3.17" dependencies: - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/querystring-builder": "npm:^4.2.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/querystring-builder": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" "@smithy/util-base64": "npm:^4.3.2" tslib: "npm:^2.6.2" - checksum: 10c0/456f98b8bba5214a01aa9ca73ab4088a529ad6473a72cc74747d676d2c5225748167eb3cddccbc2ef884141965132dab49d19b7599414e899c9c36f71a04ce85 + checksum: 10c0/8adac6bf9d5735f8ddbe9e59dd268f985881b64b03cba7e83401471b3094139189476324fe640bbd19d75f5cd8e098d9a2a7f4925bc9fadecd1ccbe937773c12 languageName: node linkType: hard -"@smithy/hash-blob-browser@npm:^4.2.13": - version: 4.2.13 - resolution: "@smithy/hash-blob-browser@npm:4.2.13" +"@smithy/hash-blob-browser@npm:^4.2.15": + version: 4.2.15 + resolution: "@smithy/hash-blob-browser@npm:4.2.15" dependencies: "@smithy/chunked-blob-reader": "npm:^5.2.2" "@smithy/chunked-blob-reader-native": "npm:^4.2.3" - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/1b28105329b01005f357cc45edefaedc1e49547e2c668da0bd15f42c03b8d0293eece0476d7f1f63d5ea0dc82d73a0964ed066d10c4e43661aa11c2cafbbc238 + checksum: 10c0/0f1ee2fd786306c604c4c08bb3b6ba00bf99fd2ba36743ca5d2695c8d37e02bb84435d5d55ebde6483a506ebcc20c42203750671cdd73618255499ef8cd0852b languageName: node linkType: hard -"@smithy/hash-node@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/hash-node@npm:4.2.12" +"@smithy/hash-node@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/hash-node@npm:4.2.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" "@smithy/util-buffer-from": "npm:^4.2.2" "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/4da4aaf39d1c2c3eec7a93cd02a055532583238ad3e80247cab211a3490cbff6e1e1a51abfd0502ef98be3f9f416a263c1382f28fad1aff38efaf129ce4b8a3d + checksum: 10c0/c2cfc5dac4f2b996c4c5c503d3c26e52fcd0a647e71541182b24a48925801659828c9d378dad6857444b9d997c1655bdb23f6db5994ad8b7c814b2d0a09655b7 languageName: node linkType: hard -"@smithy/hash-stream-node@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/hash-stream-node@npm:4.2.12" +"@smithy/hash-stream-node@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/hash-stream-node@npm:4.2.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/481e8aa1a6baf08276203a9738fe8f103b83efe5443f863ec046e82a68f7880cf54c20d4ec61e5e197d39e5adb544a1e020c51554e60882a91f01dc1baf50a53 + checksum: 10c0/db14c0879527dfc0a184a4958e8f229e1e3f15d0e612ad4596ee92de8f9d672a5ead4a325e517f235129530a8d8472c2bb628d0620af6662abc65adfe71f573e languageName: node linkType: hard -"@smithy/invalid-dependency@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/invalid-dependency@npm:4.2.12" +"@smithy/invalid-dependency@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/invalid-dependency@npm:4.2.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/688f3c312d07ea72ec98c2a58fdb230bd6b43c122f88a411cb9643c0c6085e2a3a27f36f9c3cc0024b32fa831b4b6353e74933a8f746e18acc09c20ca579384e + checksum: 10c0/deba2c21232050de87e7893b86a27a8f080ace391a3cccf22d7aee183bc3b392247f3bb1a0e4f0b6ea6f928c18ba035fd2ed3af257178ba84f3564d47c635d16 languageName: node linkType: hard @@ -1005,216 +975,216 @@ __metadata: languageName: node linkType: hard -"@smithy/md5-js@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/md5-js@npm:4.2.12" +"@smithy/md5-js@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/md5-js@npm:4.2.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/f71707c566a007e41cefe616200112673d3fce5408ed464a1ef2fd459af0a4c90da39e8e0f8872eb3146d7b17120d8db017b36ea7b671a0e697eaeca0997e7da + checksum: 10c0/d0bd6f08a3e37e83685fc1eba6621ee0f19576466ba23e7239dc201c97f83d46f6b9f7ddecc48ab74535b1f498d9facc43f18018f168dc26d2b5d0f9c72af761 languageName: node linkType: hard -"@smithy/middleware-content-length@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/middleware-content-length@npm:4.2.12" +"@smithy/middleware-content-length@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/middleware-content-length@npm:4.2.14" dependencies: - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/2800bf2cad2fe6c4eb9edb29e6637b4b937edf89db2a3f95594c93a74ae48144dd1a826712a02a5f3b4e3648a29092f4e573e4828ae88a33b25f87531c329430 + checksum: 10c0/c26cc36504ad9a720e42f009bcc185b16e35ff64644bbec710a998c071d3366face29bb6fa68744adc7312356803666922459e416f3a7ae5c804841957832c3d languageName: node linkType: hard -"@smithy/middleware-endpoint@npm:^4.4.25": - version: 4.4.25 - resolution: "@smithy/middleware-endpoint@npm:4.4.25" +"@smithy/middleware-endpoint@npm:^4.4.32": + version: 4.4.32 + resolution: "@smithy/middleware-endpoint@npm:4.4.32" dependencies: - "@smithy/core": "npm:^3.23.11" - "@smithy/middleware-serde": "npm:^4.2.14" - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/shared-ini-file-loader": "npm:^4.4.7" - "@smithy/types": "npm:^4.13.1" - "@smithy/url-parser": "npm:^4.2.12" - "@smithy/util-middleware": "npm:^4.2.12" + "@smithy/core": "npm:^3.23.17" + "@smithy/middleware-serde": "npm:^4.2.20" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" + "@smithy/url-parser": "npm:^4.2.14" + "@smithy/util-middleware": "npm:^4.2.14" tslib: "npm:^2.6.2" - checksum: 10c0/bbe55e18a7cc6929646183b8e1ca3a2bdc869ba118af483511c4e8cb5f72f214993fbb63e17ecf4f048389f002c196e609084a1c561b5a25afb871c7d1f70dd7 + checksum: 10c0/a77ba3f56956b872a533754c71f75d2a9d6df9af8d58a059d07caedd1af3ffdcae5b667a0b96b6d067555a777510f6fc5847f699a2dd705e9b5837d21510daa4 languageName: node linkType: hard -"@smithy/middleware-retry@npm:^4.4.42": - version: 4.4.42 - resolution: "@smithy/middleware-retry@npm:4.4.42" +"@smithy/middleware-retry@npm:^4.5.6": + version: 4.5.7 + resolution: "@smithy/middleware-retry@npm:4.5.7" dependencies: - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/service-error-classification": "npm:^4.2.12" - "@smithy/smithy-client": "npm:^4.12.5" - "@smithy/types": "npm:^4.13.1" - "@smithy/util-middleware": "npm:^4.2.12" - "@smithy/util-retry": "npm:^4.2.12" + "@smithy/core": "npm:^3.23.17" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/service-error-classification": "npm:^4.3.1" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-middleware": "npm:^4.2.14" + "@smithy/util-retry": "npm:^4.3.6" "@smithy/uuid": "npm:^1.1.2" tslib: "npm:^2.6.2" - checksum: 10c0/13b61e03585286dbe6f0386b0e65adb0140a308cc3b077e73f711b0993ae219bbfe89ada36bfbec40d1605ba92ae1591d59311a415757589689ab12c97146caa + checksum: 10c0/44ba622d961f83935aa13210fc92edfbf017f655ad4f4fb2024f7e880a70867d547b358be2089966689ed92c5deedcdf4723177c015babaf332ba3b55a40fe9b languageName: node linkType: hard -"@smithy/middleware-serde@npm:^4.2.14": - version: 4.2.14 - resolution: "@smithy/middleware-serde@npm:4.2.14" +"@smithy/middleware-serde@npm:^4.2.20": + version: 4.2.20 + resolution: "@smithy/middleware-serde@npm:4.2.20" dependencies: - "@smithy/core": "npm:^3.23.11" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/core": "npm:^3.23.17" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/fd28180e9f0eb4e040a76896283b1d662257cdfb1f9298836425e434f41569df57f327452d8feeb4bab37aa93891ede10b80c5ea10119a724ca1585ee9264672 + checksum: 10c0/2db736d58c0d628a33febad86259eedd6d025135fc403458e4469a077a6adbb909587408acb62c982fa1b2d8b1376507da90661a4315a544c7d73a62179a3453 languageName: node linkType: hard -"@smithy/middleware-stack@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/middleware-stack@npm:4.2.12" +"@smithy/middleware-stack@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/middleware-stack@npm:4.2.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/d06ec807249bb7f13cf4c6ce078a871dbeddb5b0c07536da62942245d2723ec380df4e631ab3c5b3ba7dc9626a609d11fcd48d53c8b0f9b6c9f1239b83d49f40 + checksum: 10c0/5a0323c50b399c4a2ff0d91b219c7c285b006f905f66b291b6013a4e94f14c036ff5a74c565989afc3c6f0fafc6c266bca6746b79e242403713b7d18dc266a92 languageName: node linkType: hard -"@smithy/node-config-provider@npm:^4.3.12": - version: 4.3.12 - resolution: "@smithy/node-config-provider@npm:4.3.12" +"@smithy/node-config-provider@npm:^4.3.14": + version: 4.3.14 + resolution: "@smithy/node-config-provider@npm:4.3.14" dependencies: - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/shared-ini-file-loader": "npm:^4.4.7" - "@smithy/types": "npm:^4.13.1" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/shared-ini-file-loader": "npm:^4.4.9" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/97087669ae1c834bc00ab10ade383a746c411a04788b7104d9f4e921ce7d24c5d77257f9ac8b8c842f886a2d658acd948e133eb95f1ee768cfbe49456441e91c + checksum: 10c0/59033a2fde5327d9ae1a0304a0eb2c3145141024cb4dcb58c5cd3c48371cd3a55d7228a3d2f33a5785b2d6a0a35475cbd0d1b2435d964c824683ac89a664e926 languageName: node linkType: hard -"@smithy/node-http-handler@npm:^4.4.16": - version: 4.4.16 - resolution: "@smithy/node-http-handler@npm:4.4.16" +"@smithy/node-http-handler@npm:^4.6.1": + version: 4.6.1 + resolution: "@smithy/node-http-handler@npm:4.6.1" dependencies: - "@smithy/abort-controller": "npm:^4.2.12" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/querystring-builder": "npm:^4.2.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/querystring-builder": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/f5b0f66a7376d9e878604149f8d00b9267d9b7b329ac9e3e6eeb4bde758814e72a0b43641e6e360a68cca408f0f656e9f6f2798dd26a8ba4de4d8c9c051c1ca8 + checksum: 10c0/6c07fbfd8326cd5369283bd19c1af923ee49b7dcf42fdd199bb7132e4c25eaa6db8573e3b669fb60be0d8f9757a3f2482cd0cf6d6631494255f08239d3036ed2 languageName: node linkType: hard -"@smithy/property-provider@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/property-provider@npm:4.2.12" +"@smithy/property-provider@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/property-provider@npm:4.2.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/d4dc0d6c61e3b1f947b1e66074dc527f1a8499fef00627c6e97f01822d357c80db8853a4283d8206075b7fba6b9c59d648dc94ab4b08902acf2a2cb97533dc39 + checksum: 10c0/8d3da90e228b53885d1e82f28b33c095b72f3b1cb5dcd7f7edcd96292d90848e9cdfed85cef00040e198343e850acef61540e4de24bd10b07fbc8e1e8f4a1fdd languageName: node linkType: hard -"@smithy/protocol-http@npm:^5.3.12": - version: 5.3.12 - resolution: "@smithy/protocol-http@npm:5.3.12" +"@smithy/protocol-http@npm:^5.3.14": + version: 5.3.14 + resolution: "@smithy/protocol-http@npm:5.3.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/f71f8e54d42637acbef9f01e3974a8ad46187ae020366de4dc84dac7ba8413a8a6fb21369c83b660afa110fc5a56d185c7e48de7d2cf45351ebb1b29aa77962b + checksum: 10c0/ed7306e7e4b919fdacf60d1928f003ac4c4679cffd7472b078547fbed7b6cb9ba524f105b1871c2cd79222871169d3183257fd0a1a81c172fa7cf0a9abaf35f9 languageName: node linkType: hard -"@smithy/querystring-builder@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/querystring-builder@npm:4.2.12" +"@smithy/querystring-builder@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/querystring-builder@npm:4.2.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" "@smithy/util-uri-escape": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/171c0d4da2fd024466741e6ee1c05cac5664e0da82c4ac5afd3218278925c25ed00bc3518e02481f4daf3f366034f273fb1cb579f146f10d0edee14dc5676c21 + checksum: 10c0/ae2ceec55b4f32b73fe2eca710563b9dbe65c81157ed58c12c282bad6445998f1fe99d723591f9bac4ae6106d84213b57e960176865fb2dec78fc3bdf024b14b languageName: node linkType: hard -"@smithy/querystring-parser@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/querystring-parser@npm:4.2.12" +"@smithy/querystring-parser@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/querystring-parser@npm:4.2.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/be23cd6e68cd14cb2aaa82a06ae92c1202344a91a74f1d0098adaca0cf9e02bc08a112322a56e34873c7a0877445e49b2795ca3e181292239f42b9a2598af068 + checksum: 10c0/245923618197bbae5eb38b72807368f397fafccee8e98cd98ee7d8961a34acb57b5176fa5fe8083b796229043f135ea775060f17e14aa12080a5d7cdfbf56333 languageName: node linkType: hard -"@smithy/service-error-classification@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/service-error-classification@npm:4.2.12" +"@smithy/service-error-classification@npm:^4.3.1": + version: 4.3.1 + resolution: "@smithy/service-error-classification@npm:4.3.1" dependencies: - "@smithy/types": "npm:^4.13.1" - checksum: 10c0/a37ec7bded03f7578473b002bf99771853f9e59ecc53e85fb0501a794b5ff121259225af981f55788ad7adc57ef85ab536de1d2a1c2f5556117426e5485f7da9 + "@smithy/types": "npm:^4.14.1" + checksum: 10c0/1bc927f53693035165f4b15232c644be579cdd432cf2d3e026faa746d26693e6b1e0f35bcd5d812dd89445695fd1eb7188e415e2523d6d8991e8db900cecdf36 languageName: node linkType: hard -"@smithy/shared-ini-file-loader@npm:^4.4.7": - version: 4.4.7 - resolution: "@smithy/shared-ini-file-loader@npm:4.4.7" +"@smithy/shared-ini-file-loader@npm:^4.4.9": + version: 4.4.9 + resolution: "@smithy/shared-ini-file-loader@npm:4.4.9" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/817a1d1b19f7f681ae5972db44416ba215f422da964eda04eae9ed1a31c05ae8ce3bed69c1429c9c42b9d1ec3493933731d2c3ef4b3858431cfdb51aa40b1b93 + checksum: 10c0/9bf96ea80cedff027373c5547ffa53b35d272292b7d142a86211726343061953a34610f3dbd02153bb285c7c0f3802c5a25b4e27870e8068d777abdcaa9c1748 languageName: node linkType: hard -"@smithy/signature-v4@npm:^5.3.12": - version: 5.3.12 - resolution: "@smithy/signature-v4@npm:5.3.12" +"@smithy/signature-v4@npm:^5.3.14": + version: 5.3.14 + resolution: "@smithy/signature-v4@npm:5.3.14" dependencies: "@smithy/is-array-buffer": "npm:^4.2.2" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" "@smithy/util-hex-encoding": "npm:^4.2.2" - "@smithy/util-middleware": "npm:^4.2.12" + "@smithy/util-middleware": "npm:^4.2.14" "@smithy/util-uri-escape": "npm:^4.2.2" "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/7163c533c6ffebd93c2f7266b22c0d82488746846e50e795afcb15becd8431cfe993006a99b09828e5905ca56a7ffa6080a3537e092f3a57d661f64c5f0f11a7 + checksum: 10c0/ccc788992e281a681984e079b7194a219af40cf4f7087988eba69c4947264b9a83ac00a806164b9074c7e2f0697e492fa2b377b56b783316d247be02aaccbdb3 languageName: node linkType: hard -"@smithy/smithy-client@npm:^4.12.5": - version: 4.12.5 - resolution: "@smithy/smithy-client@npm:4.12.5" +"@smithy/smithy-client@npm:^4.12.13": + version: 4.12.13 + resolution: "@smithy/smithy-client@npm:4.12.13" dependencies: - "@smithy/core": "npm:^3.23.11" - "@smithy/middleware-endpoint": "npm:^4.4.25" - "@smithy/middleware-stack": "npm:^4.2.12" - "@smithy/protocol-http": "npm:^5.3.12" - "@smithy/types": "npm:^4.13.1" - "@smithy/util-stream": "npm:^4.5.19" + "@smithy/core": "npm:^3.23.17" + "@smithy/middleware-endpoint": "npm:^4.4.32" + "@smithy/middleware-stack": "npm:^4.2.14" + "@smithy/protocol-http": "npm:^5.3.14" + "@smithy/types": "npm:^4.14.1" + "@smithy/util-stream": "npm:^4.5.25" tslib: "npm:^2.6.2" - checksum: 10c0/8c5de6c80d4c7b3fb509a605fd5ee1c2ea83eb784264d08b64eb5c893a81ae032c0964afc345956f6b6180f2eaf857dff97730463a19b2a45a781ae89d4fab71 + checksum: 10c0/0680d4d0720d110a637fabb8bdc85175e1471191925f5c55f08636f5327de1bc29c8db75318aac1396491fa83c8a319dd4cd26c5e9fff619207c9a96b9dbd9fe languageName: node linkType: hard -"@smithy/types@npm:^4.13.1": - version: 4.13.1 - resolution: "@smithy/types@npm:4.13.1" +"@smithy/types@npm:^4.14.1": + version: 4.14.1 + resolution: "@smithy/types@npm:4.14.1" dependencies: tslib: "npm:^2.6.2" - checksum: 10c0/775ed9748d9290b8816d933bfb9726eb9301ef2fe9fba1bfbc1966372b9f0d4dd1d3b611aca3c000094bed2ca9d821e10fe2795a75df5bc305bc8845a1e413f7 + checksum: 10c0/9e6209770a25582a11ca67d4750865de0b7732bf3f353ba9260f49e9c4b2adf3274d64057a2bda93da7025e33a54e51bd78c529307efc2b75b429bc45a6ad64c languageName: node linkType: hard -"@smithy/url-parser@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/url-parser@npm:4.2.12" +"@smithy/url-parser@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/url-parser@npm:4.2.14" dependencies: - "@smithy/querystring-parser": "npm:^4.2.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/querystring-parser": "npm:^4.2.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/ff6b127f0bb8ddd6934018277a2ae73ecb036259ec9e0ea4e136da47b39d089ee29ff92fcdbc79613b3c8224f180bcf914289bd71709e9ccc4a444c5f0423086 + checksum: 10c0/360463fe1fb51b8a1c5b80f4a16df993ee4e87221e60ce51c428b0737d6f1325434ec572bb7afca7d65bb9a09c750f307822a23e42be675706ee71fedd7c6c06 languageName: node linkType: hard @@ -1276,41 +1246,41 @@ __metadata: languageName: node linkType: hard -"@smithy/util-defaults-mode-browser@npm:^4.3.41": - version: 4.3.41 - resolution: "@smithy/util-defaults-mode-browser@npm:4.3.41" +"@smithy/util-defaults-mode-browser@npm:^4.3.49": + version: 4.3.49 + resolution: "@smithy/util-defaults-mode-browser@npm:4.3.49" dependencies: - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/smithy-client": "npm:^4.12.5" - "@smithy/types": "npm:^4.13.1" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/b2b028765b49ab5180a7defdd99cf28722917316c9ea6818d2f9d4cfa7a3ac160f75190f3012e91b20d27bd6b60e5751eac829696f496ac5f9612e86cc097539 + checksum: 10c0/0979b09f1108aa41f5bf623e32fa138e129fbffe3adc23c46308afa310b925635428f9d7e36e3e2a312cafe9af325cb5f01667791ee672418d4f302c1961623b languageName: node linkType: hard -"@smithy/util-defaults-mode-node@npm:^4.2.44": - version: 4.2.44 - resolution: "@smithy/util-defaults-mode-node@npm:4.2.44" +"@smithy/util-defaults-mode-node@npm:^4.2.54": + version: 4.2.54 + resolution: "@smithy/util-defaults-mode-node@npm:4.2.54" dependencies: - "@smithy/config-resolver": "npm:^4.4.11" - "@smithy/credential-provider-imds": "npm:^4.2.12" - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/property-provider": "npm:^4.2.12" - "@smithy/smithy-client": "npm:^4.12.5" - "@smithy/types": "npm:^4.13.1" + "@smithy/config-resolver": "npm:^4.4.17" + "@smithy/credential-provider-imds": "npm:^4.2.14" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/property-provider": "npm:^4.2.14" + "@smithy/smithy-client": "npm:^4.12.13" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/00f634beea54b49cde78d42782a395987f5a35841deeec73c06b12cd088dcf1d8cb6f1c42f4df513e9cc68f01e9c8c918152c16fc400563a7fd7bd73a7daaf39 + checksum: 10c0/7146087fa1d7758b37da456719d589f63300843ff5610891de02a0434c5abbd49fd257712c2d9e0bede904f4ec62c9d3c9eddcd6ec0372134f998e4f9c0bce09 languageName: node linkType: hard -"@smithy/util-endpoints@npm:^3.3.3": - version: 3.3.3 - resolution: "@smithy/util-endpoints@npm:3.3.3" +"@smithy/util-endpoints@npm:^3.4.2": + version: 3.4.2 + resolution: "@smithy/util-endpoints@npm:3.4.2" dependencies: - "@smithy/node-config-provider": "npm:^4.3.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/node-config-provider": "npm:^4.3.14" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/ba80337fa6216e8912d5f78bc192c625807ba212071a8504b40b0bcf2b28d293fbd9b180da1ebcd1d15faf60291a6ff534e288266a29dc9cd600bf5eb1d51579 + checksum: 10c0/602e05c8dc43d8902604bac74b717d09da5b4f1994dcdd5025bffeced6002eaa0612ae1ccbe7a0e636a3d92a60747715e22cbacf8feeb672394d15b6ae211b13 languageName: node linkType: hard @@ -1323,40 +1293,40 @@ __metadata: languageName: node linkType: hard -"@smithy/util-middleware@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/util-middleware@npm:4.2.12" +"@smithy/util-middleware@npm:^4.2.14": + version: 4.2.14 + resolution: "@smithy/util-middleware@npm:4.2.14" dependencies: - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/0fd7e7e8b5b02023928e7ad27f1c44a312524c393c39aa064c3c371e521035028116a5aa16d8011068b288179eb862bef917d798419b9f2a2843bf4ea3897e2b + checksum: 10c0/6ba5026b70ad7b58fac89d7428c0b1679be2a97a8fb47811a39e0183901a3c6ca8732ee225ddfda28e7b86223d49983ff709421905da571bc00b82c660e4fc27 languageName: node linkType: hard -"@smithy/util-retry@npm:^4.2.12": - version: 4.2.12 - resolution: "@smithy/util-retry@npm:4.2.12" +"@smithy/util-retry@npm:^4.3.5, @smithy/util-retry@npm:^4.3.6": + version: 4.3.6 + resolution: "@smithy/util-retry@npm:4.3.6" dependencies: - "@smithy/service-error-classification": "npm:^4.2.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/service-error-classification": "npm:^4.3.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/1a8bff8da85d6637310286a3a52f557622cc9bb9dc75d9770640701a9565a3a995aeb34ed68acf333f60bb871dc49e9db196c5a35913b33944e02811f3cfcca2 + checksum: 10c0/a2d3b34ca48edd4f58885c91de28f7ccbe48d0e4a8f005eb569197cf37640b5c04043fd556b0e1ecf6d8623b6c384b52b1c34c898acf77260e517f7d00641811 languageName: node linkType: hard -"@smithy/util-stream@npm:^4.5.19": - version: 4.5.19 - resolution: "@smithy/util-stream@npm:4.5.19" +"@smithy/util-stream@npm:^4.5.25": + version: 4.5.25 + resolution: "@smithy/util-stream@npm:4.5.25" dependencies: - "@smithy/fetch-http-handler": "npm:^5.3.15" - "@smithy/node-http-handler": "npm:^4.4.16" - "@smithy/types": "npm:^4.13.1" + "@smithy/fetch-http-handler": "npm:^5.3.17" + "@smithy/node-http-handler": "npm:^4.6.1" + "@smithy/types": "npm:^4.14.1" "@smithy/util-base64": "npm:^4.3.2" "@smithy/util-buffer-from": "npm:^4.2.2" "@smithy/util-hex-encoding": "npm:^4.2.2" "@smithy/util-utf8": "npm:^4.2.2" tslib: "npm:^2.6.2" - checksum: 10c0/bfc4794f629c7da0a87f05beabf3904efcf5cf8e4c21180f776bb2cdc9d75d593558017b060c3573c656ed1dfbbe068e2951c700cf2e76e4efacad8373087a38 + checksum: 10c0/4b222c975eca2018831f1ab4067f122f4ef5e68f3abb71776003309f1a27f67d509a2d86f5f1f81a91656236db0e29c38314c005b17dbf63f053de4d97be7b59 languageName: node linkType: hard @@ -1389,14 +1359,13 @@ __metadata: languageName: node linkType: hard -"@smithy/util-waiter@npm:^4.2.13": - version: 4.2.13 - resolution: "@smithy/util-waiter@npm:4.2.13" +"@smithy/util-waiter@npm:^4.3.0": + version: 4.3.0 + resolution: "@smithy/util-waiter@npm:4.3.0" dependencies: - "@smithy/abort-controller": "npm:^4.2.12" - "@smithy/types": "npm:^4.13.1" + "@smithy/types": "npm:^4.14.1" tslib: "npm:^2.6.2" - checksum: 10c0/02e29879d64214f01e0acf7f9e1157e5aa671371f9e2fb46fc75595e330f785e237c60eba44eb039c8598bfc0fdf3bcb6556742f6631605f71e856f9267524e9 + checksum: 10c0/44e18c946c731740801787714a513e6c119f9997f29191d3bababe9781e0935d08a927caa475d70f7dcb02d70b532639be94317ac3c6305496f1ebe90fcfcf9b languageName: node linkType: hard @@ -1461,25 +1430,18 @@ __metadata: linkType: hard "@types/chai-subset@npm:^1.3.5": - version: 1.3.5 - resolution: "@types/chai-subset@npm:1.3.5" - dependencies: - "@types/chai": "npm:*" - checksum: 10c0/d5cfb483917b0fdf245c8c51d1fa35a2c302295dfc5383ee4faa545db49a28ea169650bb1b75de2cd31f6f8e486a856d241acf9e0456fc93cb74ac18dfdfd19d - languageName: node - linkType: hard - -"@types/chai@npm:*": - version: 5.0.0 - resolution: "@types/chai@npm:5.0.0" - checksum: 10c0/fcce55f2bbb8485fc860a1dcbac17c1a685b598cfc91a55d37b65b1642b921cf736caa8cce9dcc530830d900f78ab95cf43db4e118db34a5176f252cacd9e1e8 + version: 1.3.6 + resolution: "@types/chai-subset@npm:1.3.6" + peerDependencies: + "@types/chai": <5.2.0 + checksum: 10c0/d8e2362ff7e96b742e00326e656a91fffd6dc1260fd081c30bd607f908eb4d6777cec1e535bae905a8741b8287acded8955c0fe84ee3b6874d38502c029d35c4 languageName: node linkType: hard "@types/chai@npm:^4.1.7": - version: 4.3.11 - resolution: "@types/chai@npm:4.3.11" - checksum: 10c0/0c216ac4a19bfbf8318bb104d32e50704ee2ffc4b538b976c4326e6638fee121462402caa570662227a2a218810388aadb14bdbd3d3d474ec300b00695db448a + version: 4.3.20 + resolution: "@types/chai@npm:4.3.20" + checksum: 10c0/4601189d611752e65018f1ecadac82e94eed29f348e1d5430e5681a60b01e1ecf855d9bcc74ae43b07394751f184f6970fac2b5561fc57a1f36e93a0f5ffb6e8 languageName: node linkType: hard @@ -1493,9 +1455,9 @@ __metadata: linkType: hard "@types/http-cache-semantics@npm:*": - version: 4.0.4 - resolution: "@types/http-cache-semantics@npm:4.0.4" - checksum: 10c0/51b72568b4b2863e0fe8d6ce8aad72a784b7510d72dc866215642da51d84945a9459fa89f49ec48f1e9a1752e6a78e85a4cda0ded06b1c73e727610c925f9ce6 + version: 4.2.0 + resolution: "@types/http-cache-semantics@npm:4.2.0" + checksum: 10c0/82dd33cbe7d4843f1e884a251c6a12d385b62274353b9db167462e7fbffdbb3a83606f9952203017c5b8cabbd7b9eef0cf240a3a9dedd20f69875c9701939415 languageName: node linkType: hard @@ -1516,11 +1478,11 @@ __metadata: linkType: hard "@types/node@npm:*": - version: 20.10.5 - resolution: "@types/node@npm:20.10.5" + version: 25.6.0 + resolution: "@types/node@npm:25.6.0" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10c0/be30609aae0bfe492097815f166ccc07f465220cb604647fa4e5ec05a1d16c012a41b82b5f11ecfe2485cbb479d4d20384b95b809ca0bcff6d94d5bbafa645bb + undici-types: "npm:~7.19.0" + checksum: 10c0/d2d2015630ff098a201407f55f5077a20270ae4f465c739b40865cd9933b91b9c5d2b85568eadaf3db0801b91e267333ca7eb39f007428b173d1cdab4b339ac5 languageName: node linkType: hard @@ -1534,11 +1496,11 @@ __metadata: linkType: hard "@types/node@npm:^20.9.0": - version: 20.14.11 - resolution: "@types/node@npm:20.14.11" + version: 20.19.39 + resolution: "@types/node@npm:20.19.39" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10c0/5306becc0ff41d81b1e31524bd376e958d0741d1ce892dffd586b9ae0cb6553c62b0d62abd16da8bea6b9a2c17572d360450535d7c073794b0cef9cb4e39691e + undici-types: "npm:~6.21.0" + checksum: 10c0/1d16da7b5f47a7415b827fcf3b94d279febf4c14671afec74a03e47856b5270023d9beb1b9aeab4d3b622fd97d61a60206cfc2cca588663181331bc592468289 languageName: node linkType: hard @@ -1583,13 +1545,6 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": - version: 7.1.4 - resolution: "agent-base@npm:7.1.4" - checksum: 10c0/c2c9ab7599692d594b6a161559ada307b7a624fa4c7b03e3afdb5a5e31cd0e53269115b620fcab024c5ac6a6f37fa5eb2e004f076ad30f5f7e6b8b671f7b35fe - languageName: node - linkType: hard - "ansi-colors@npm:4.1.1": version: 4.1.1 resolution: "ansi-colors@npm:4.1.1" @@ -1748,8 +1703,8 @@ __metadata: linkType: hard "bare-fs@npm:^4.5.5": - version: 4.5.5 - resolution: "bare-fs@npm:4.5.5" + version: 4.7.1 + resolution: "bare-fs@npm:4.7.1" dependencies: bare-events: "npm:^2.5.4" bare-path: "npm:^3.0.0" @@ -1761,14 +1716,14 @@ __metadata: peerDependenciesMeta: bare-buffer: optional: true - checksum: 10c0/1f8b31b73848639fff4ab46fb9d8c0477dc571813fd6790ec75edc192abc467310f1082ecb81170aeffca91b4d08f0e9a002d6f9fa6968a07d11ea22be1597ff + checksum: 10c0/4dc67f6dd0264b817941c2b8cbfc42b6abc3980984cdfd129c4d1f22517cb29f6b99a69fc1e3e87f3a9c997e8c94114604bb67fff10574b2adf0966510cf0222 languageName: node linkType: hard "bare-os@npm:^3.0.1": - version: 3.8.0 - resolution: "bare-os@npm:3.8.0" - checksum: 10c0/2211c5f9734c7d3c387a6ba2ff7fd6df805736a1d9b865a1b9e2ba0904782340ae9b9a58eb359e7e4b50d457a1b656290cc3c8d1628d5df5d95d327eeb3dab63 + version: 3.9.0 + resolution: "bare-os@npm:3.9.0" + checksum: 10c0/fc17ca44a7ae59b9c62531365d6b68fb436f0c77c4c35bb2bbdb9382f2f229f73365164c33350fe1833857e0f4224e3079dc5175cd438067c078d99ccb7932e8 languageName: node linkType: hard @@ -1782,29 +1737,32 @@ __metadata: linkType: hard "bare-stream@npm:^2.6.4": - version: 2.8.1 - resolution: "bare-stream@npm:2.8.1" + version: 2.13.1 + resolution: "bare-stream@npm:2.13.1" dependencies: - streamx: "npm:^2.21.0" + streamx: "npm:^2.25.0" teex: "npm:^1.0.1" peerDependencies: + bare-abort-controller: "*" bare-buffer: "*" bare-events: "*" peerDependenciesMeta: + bare-abort-controller: + optional: true bare-buffer: optional: true bare-events: optional: true - checksum: 10c0/7040f22ee412cfbc0fb13accb6578e77d4e5f2c039de54fd63efda4a6748560c5022827ccdbaf27d493229aec9b1112121b431fa21b79c52cdf9f55ad929c362 + checksum: 10c0/2c35e0b4e56667265e9023e9f51b77652ce043fd6611497575871ce62e833760dd3e5919ccc0cebe1af40959c4350035162b47541a1277d6488709f61f199754 languageName: node linkType: hard "bare-url@npm:^2.2.2": - version: 2.3.2 - resolution: "bare-url@npm:2.3.2" + version: 2.4.2 + resolution: "bare-url@npm:2.4.2" dependencies: bare-path: "npm:^3.0.0" - checksum: 10c0/4fd0046314390a54404519d9db20e130ab3a341ef638d040f9603ae3fa0a1d84f6970357d21c8fc64e6163d1f61fd212cb1cfa4cb537dfead99fb06e3c030b15 + checksum: 10c0/50d225755a0f312e4951ca71538e9401276f73e9a1fbc4419118ff0d1b9ec93fe8c03e1ddbc95672afdc2c9d0ced224a5fb2b15b241759b2649dca572a295c51 languageName: node linkType: hard @@ -1836,30 +1794,21 @@ __metadata: languageName: node linkType: hard -"brace-expansion@npm:^2.0.1": - version: 2.0.1 - resolution: "brace-expansion@npm:2.0.1" - dependencies: - balanced-match: "npm:^1.0.0" - checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f - languageName: node - linkType: hard - -"brace-expansion@npm:^2.0.2": - version: 2.0.2 - resolution: "brace-expansion@npm:2.0.2" +"brace-expansion@npm:^2.0.1, brace-expansion@npm:^2.0.2": + version: 2.1.0 + resolution: "brace-expansion@npm:2.1.0" dependencies: balanced-match: "npm:^1.0.0" - checksum: 10c0/6d117a4c793488af86b83172deb6af143e94c17bc53b0b3cec259733923b4ca84679d506ac261f4ba3c7ed37c46018e2ff442f9ce453af8643ecd64f4a54e6cf + checksum: 10c0/439cedf3e23d7993b37919f1d6fdc653ec21a42437ec3e7460bea9ca8b17edf7a24a633273c31d61aa4335877cf29a443f1871814131c87997a1e6223e1f1502 languageName: node linkType: hard -"brace-expansion@npm:^5.0.2": - version: 5.0.4 - resolution: "brace-expansion@npm:5.0.4" +"brace-expansion@npm:^5.0.5": + version: 5.0.5 + resolution: "brace-expansion@npm:5.0.5" dependencies: balanced-match: "npm:^4.0.2" - checksum: 10c0/359cbcfa80b2eb914ca1f3440e92313fbfe7919ee6b274c35db55bec555aded69dac5ee78f102cec90c35f98c20fa43d10936d0cd9978158823c249257e1643a + checksum: 10c0/4d238e14ed4f5cc9c07285550a41cef23121ca08ba99fa9eb5b55b580dcb6bf868b8210aa10526bdc9f8dc97f33ca2a7259039c4cc131a93042beddb424c48e3 languageName: node linkType: hard @@ -1920,25 +1869,6 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^20.0.1": - version: 20.0.3 - resolution: "cacache@npm:20.0.3" - dependencies: - "@npmcli/fs": "npm:^5.0.0" - fs-minipass: "npm:^3.0.0" - glob: "npm:^13.0.0" - lru-cache: "npm:^11.1.0" - minipass: "npm:^7.0.3" - minipass-collect: "npm:^2.0.1" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^7.0.2" - ssri: "npm:^13.0.0" - unique-filename: "npm:^5.0.0" - checksum: 10c0/c7da1ca694d20e8f8aedabd21dc11518f809a7d2b59aa76a1fc655db5a9e62379e465c157ddd2afe34b19230808882288effa6911b2de26a088a6d5645123462 - languageName: node - linkType: hard - "cacheable-lookup@npm:^5.0.3": version: 5.0.4 resolution: "cacheable-lookup@npm:5.0.4" @@ -1976,8 +1906,8 @@ __metadata: linkType: hard "chai@npm:^4.2.0": - version: 4.3.10 - resolution: "chai@npm:4.3.10" + version: 4.5.0 + resolution: "chai@npm:4.5.0" dependencies: assertion-error: "npm:^1.1.0" check-error: "npm:^1.0.3" @@ -1985,8 +1915,8 @@ __metadata: get-func-name: "npm:^2.0.2" loupe: "npm:^2.3.6" pathval: "npm:^1.1.1" - type-detect: "npm:^4.0.8" - checksum: 10c0/c887d24f67be6fb554c7ebbde3bb0568697a8833d475e4768296916891ba143f25fc079f6eb34146f3dd5a3279d34c1f387c32c9a6ab288e579f948d9ccf53fe + type-detect: "npm:^4.1.0" + checksum: 10c0/b8cb596bd1aece1aec659e41a6e479290c7d9bee5b3ad63d2898ad230064e5b47889a3bc367b20100a0853b62e026e2dc514acf25a3c9385f936aa3614d4ab4d languageName: node linkType: hard @@ -2162,19 +2092,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.3.4, debug@npm:^4.3.5": - version: 4.4.3 - resolution: "debug@npm:4.4.3" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 - languageName: node - linkType: hard - -"debug@npm:4.3.4, debug@npm:^4.1.0, debug@npm:^4.1.1": +"debug@npm:4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -2195,6 +2113,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.5": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + "decamelize@npm:^4.0.0": version: 4.0.0 resolution: "decamelize@npm:4.0.0" @@ -2212,11 +2142,11 @@ __metadata: linkType: hard "deep-eql@npm:^4.1.3": - version: 4.1.3 - resolution: "deep-eql@npm:4.1.3" + version: 4.1.4 + resolution: "deep-eql@npm:4.1.4" dependencies: type-detect: "npm:^4.0.0" - checksum: 10c0/ff34e8605d8253e1bf9fe48056e02c6f347b81d9b5df1c6650a1b0f6f847b4a86453b16dc226b34f853ef14b626e85d04e081b022e20b00cd7d54f079ce9bbdd + checksum: 10c0/264e0613493b43552fc908f4ff87b8b445c0e6e075656649600e1b8a17a57ee03e960156fce7177646e4d2ddaf8e5ee616d76bd79929ff593e5c79e4e5e6c517 languageName: node linkType: hard @@ -2228,17 +2158,17 @@ __metadata: linkType: hard "define-data-property@npm:^1.0.1": - version: 1.1.1 - resolution: "define-data-property@npm:1.1.1" + version: 1.1.4 + resolution: "define-data-property@npm:1.1.4" dependencies: - get-intrinsic: "npm:^1.2.1" + es-define-property: "npm:^1.0.0" + es-errors: "npm:^1.3.0" gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.0" - checksum: 10c0/77ef6e0bceb515e05b5913ab635a84d537cee84f8a7c37c77fdcb31fc5b80f6dbe81b33375e4b67d96aa04e6a0d8d4ea099e431d83f089af8d93adfb584bcb94 + checksum: 10c0/dea0606d1483eb9db8d930d4eac62ca0fa16738b0b3e07046cddfacf7d8c868bbe13fa0cb263eb91c7d0d527960dc3f2f2471a69ed7816210307f6744fe62e37 languageName: node linkType: hard -"define-properties@npm:^1.1.3": +"define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: @@ -2264,9 +2194,9 @@ __metadata: linkType: hard "diff@npm:^3.1.0": - version: 3.5.0 - resolution: "diff@npm:3.5.0" - checksum: 10c0/fc62d5ba9f6d1b8b5833380969037007913d4886997838c247c54ec6934f09ae5a07e17ae28b1f016018149d81df8ad89306f52eac1afa899e0bed49015a64d1 + version: 3.5.1 + resolution: "diff@npm:3.5.1" + checksum: 10c0/28913ba64929561bd91e9aad8b6d5a2766d3c75451b8a2f7b2c0aa19f7edd32842da92ae23ae418a57362cc7a8673a26a9441d6ad896b0c64faf0a54c7ccffe6 languageName: node linkType: hard @@ -2343,11 +2273,11 @@ __metadata: linkType: hard "end-of-stream@npm:^1.1.0": - version: 1.4.4 - resolution: "end-of-stream@npm:1.4.4" + version: 1.4.5 + resolution: "end-of-stream@npm:1.4.5" dependencies: once: "npm:^1.4.0" - checksum: 10c0/870b423afb2d54bb8d243c63e07c170409d41e20b47eeef0727547aea5740bd6717aca45597a9f2745525667a6b804c1e7bede41f856818faee5806dd9ff3975 + checksum: 10c0/b0701c92a10b89afb1cb45bf54a5292c6f008d744eb4382fa559d54775ff31617d1d7bc3ef617575f552e24fad2c7c1a1835948c66b3f3a4be0a6c1f35c883d8 languageName: node linkType: hard @@ -2365,6 +2295,20 @@ __metadata: languageName: node linkType: hard +"es-define-property@npm:^1.0.0": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 + languageName: node + linkType: hard + "es6-error@npm:^4.1.1": version: 4.1.1 resolution: "es6-error@npm:4.1.1" @@ -2373,9 +2317,9 @@ __metadata: linkType: hard "escalade@npm:^3.1.1": - version: 3.1.1 - resolution: "escalade@npm:3.1.1" - checksum: 10c0/afd02e6ca91ffa813e1108b5e7756566173d6bc0d1eb951cb44d6b21702ec17c1cf116cfe75d4a2b02e05acb0b808a7a9387d0d1ca5cf9c04ad03a8445c3e46d + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65 languageName: node linkType: hard @@ -2440,24 +2384,26 @@ __metadata: languageName: node linkType: hard -"fast-xml-builder@npm:^1.0.0": - version: 1.1.3 - resolution: "fast-xml-builder@npm:1.1.3" +"fast-xml-builder@npm:^1.1.5": + version: 1.1.5 + resolution: "fast-xml-builder@npm:1.1.5" dependencies: path-expression-matcher: "npm:^1.1.3" - checksum: 10c0/353a2b3695f66f2b0717fcb1a59d2c4108a3b29e7e4f125a51accaccade509dc6fb7e2777da62fb3b339487753ece01eaa270542b10a68dfd43f48d607a6b10e + checksum: 10c0/b814ba5559cb3140de46d2846045607ab4d4c0bfc312a49d22c91efb9f7cd7004971314841e5823eeb467a5bf403e3ade8371b7912200e111df027d42ae51715 languageName: node linkType: hard -"fast-xml-parser@npm:5.4.1": - version: 5.4.1 - resolution: "fast-xml-parser@npm:5.4.1" +"fast-xml-parser@npm:5.7.2": + version: 5.7.2 + resolution: "fast-xml-parser@npm:5.7.2" dependencies: - fast-xml-builder: "npm:^1.0.0" - strnum: "npm:^2.1.2" + "@nodable/entities": "npm:^2.1.0" + fast-xml-builder: "npm:^1.1.5" + path-expression-matcher: "npm:^1.5.0" + strnum: "npm:^2.2.3" bin: fxparser: src/cli/cli.js - checksum: 10c0/8c696438a0c64135faf93ea6a93879208d649b7c9a3293d30d6eb750dc7f766fd083c0df5a82786b60809c3ead64fad155f28dbed25efea91017aaf9f64c91e5 + checksum: 10c0/d48439ce0700add82f5e7c6ccc5a1f06483beb7cd8e88caa83c6406843e52f14988e60d05cbb3a86ffe07e073807674c807e0764d94a280e1c96d7e2011dae8e languageName: node linkType: hard @@ -2531,15 +2477,6 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^3.0.0": - version: 3.0.3 - resolution: "fs-minipass@npm:3.0.3" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 - languageName: node - linkType: hard - "fs.realpath@npm:^1.0.0": version: 1.0.0 resolution: "fs.realpath@npm:1.0.0" @@ -2566,13 +2503,6 @@ __metadata: languageName: node linkType: hard -"function-bind@npm:^1.1.2": - version: 1.1.2 - resolution: "function-bind@npm:1.1.2" - checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 - languageName: node - linkType: hard - "get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" @@ -2594,18 +2524,6 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2": - version: 1.2.2 - resolution: "get-intrinsic@npm:1.2.2" - dependencies: - function-bind: "npm:^1.1.2" - has-proto: "npm:^1.0.1" - has-symbols: "npm:^1.0.3" - hasown: "npm:^2.0.0" - checksum: 10c0/4e7fb8adc6172bae7c4fe579569b4d5238b3667c07931cd46b4eee74bbe6ff6b91329bec311a638d8e60f5b51f44fe5445693c6be89ae88d4b5c49f7ff12db0b - languageName: node - linkType: hard - "get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -2653,17 +2571,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^13.0.0": - version: 13.0.6 - resolution: "glob@npm:13.0.6" - dependencies: - minimatch: "npm:^10.2.2" - minipass: "npm:^7.1.3" - path-scurry: "npm:^2.0.2" - checksum: 10c0/269c236f11a9b50357fe7a8c6aadac667e01deb5242b19c84975628f05f4438d8ee1354bb62c5d6c10f37fd59911b54d7799730633a2786660d8c69f1d18120a - languageName: node - linkType: hard - "global-agent@npm:^3.0.0": version: 3.0.0 resolution: "global-agent@npm:3.0.0" @@ -2679,20 +2586,19 @@ __metadata: linkType: hard "globalthis@npm:^1.0.1": - version: 1.0.3 - resolution: "globalthis@npm:1.0.3" + version: 1.0.4 + resolution: "globalthis@npm:1.0.4" dependencies: - define-properties: "npm:^1.1.3" - checksum: 10c0/0db6e9af102a5254630351557ac15e6909bc7459d3e3f6b001e59fe784c96d31108818f032d9095739355a88467459e6488ff16584ee6250cd8c27dec05af4b0 + define-properties: "npm:^1.2.1" + gopd: "npm:^1.0.1" + checksum: 10c0/9d156f313af79d80b1566b93e19285f481c591ad6d0d319b4be5e03750d004dde40a39a0f26f7e635f9007a3600802f53ecd85a759b86f109e80a5f705e01846 languageName: node linkType: hard "gopd@npm:^1.0.1": - version: 1.0.1 - resolution: "gopd@npm:1.0.1" - dependencies: - get-intrinsic: "npm:^1.1.3" - checksum: 10c0/505c05487f7944c552cee72087bf1567debb470d4355b1335f2c262d218ebbff805cd3715448fe29b4b380bae6912561d0467233e4165830efd28da241418c63 + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead languageName: node linkType: hard @@ -2730,34 +2636,11 @@ __metadata: linkType: hard "has-property-descriptors@npm:^1.0.0": - version: 1.0.1 - resolution: "has-property-descriptors@npm:1.0.1" - dependencies: - get-intrinsic: "npm:^1.2.2" - checksum: 10c0/d62ba94b40150b00d621bc64a6aedb5bf0ee495308b4b7ed6bac856043db3cdfb1db553ae81cec91c9d2bd82057ff0e94145e7fa25d5aa5985ed32e0921927f6 - languageName: node - linkType: hard - -"has-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "has-proto@npm:1.0.1" - checksum: 10c0/c8a8fe411f810b23a564bd5546a8f3f0fff6f1b692740eb7a2fdc9df716ef870040806891e2f23ff4653f1083e3895bf12088703dd1a0eac3d9202d3a4768cd0 - languageName: node - linkType: hard - -"has-symbols@npm:^1.0.3": - version: 1.0.3 - resolution: "has-symbols@npm:1.0.3" - checksum: 10c0/e6922b4345a3f37069cdfe8600febbca791c94988c01af3394d86ca3360b4b93928bbf395859158f88099cb10b19d98e3bbab7c9ff2c1bd09cf665ee90afa2c3 - languageName: node - linkType: hard - -"hasown@npm:^2.0.0": - version: 2.0.0 - resolution: "hasown@npm:2.0.0" + version: 1.0.2 + resolution: "has-property-descriptors@npm:1.0.2" dependencies: - function-bind: "npm:^1.1.2" - checksum: 10c0/5d415b114f410661208c95e7ab4879f1cc2765b8daceff4dc8718317d1cb7b9ffa7c5d1eafd9a4389c9aab7445d6ea88e05f3096cb1e529618b55304956b87fc + es-define-property: "npm:^1.0.0" + checksum: 10c0/253c1f59e80bb476cf0dde8ff5284505d90c3bdb762983c3514d36414290475fe3fd6f574929d84de2a8eec00d35cf07cb6776205ff32efd7c50719125f00236 languageName: node linkType: hard @@ -2771,29 +2654,12 @@ __metadata: linkType: hard "http-cache-semantics@npm:^4.0.0": - version: 4.1.1 - resolution: "http-cache-semantics@npm:4.1.1" - checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc - languageName: node - linkType: hard - -"http-cache-semantics@npm:^4.1.1": version: 4.2.0 resolution: "http-cache-semantics@npm:4.2.0" checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": - version: 7.0.2 - resolution: "http-proxy-agent@npm:7.0.2" - dependencies: - agent-base: "npm:^7.1.0" - debug: "npm:^4.3.4" - checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 - languageName: node - linkType: hard - "http2-wrapper@npm:^1.0.0-beta.5.2": version: 1.0.3 resolution: "http2-wrapper@npm:1.0.3" @@ -2804,25 +2670,6 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1": - version: 7.0.6 - resolution: "https-proxy-agent@npm:7.0.6" - dependencies: - agent-base: "npm:^7.1.2" - debug: "npm:4" - checksum: 10c0/f729219bc735edb621fa30e6e84e60ee5d00802b8247aac0d7b79b0bd6d4b3294737a337b93b86a0bd9e68099d031858a39260c976dc14cdbba238ba1f8779ac - languageName: node - linkType: hard - -"iconv-lite@npm:^0.7.2": - version: 0.7.2 - resolution: "iconv-lite@npm:0.7.2" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10c0/3c228920f3bd307f56bf8363706a776f4a060eb042f131cd23855ceca962951b264d0997ab38a1ad340e1c5df8499ed26e1f4f0db6b2a2ad9befaff22f14b722 - languageName: node - linkType: hard - "ieee754@npm:^1.1.4, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" @@ -2830,13 +2677,6 @@ __metadata: languageName: node linkType: hard -"imurmurhash@npm:^0.1.4": - version: 0.1.4 - resolution: "imurmurhash@npm:0.1.4" - checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 - languageName: node - linkType: hard - "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -2854,13 +2694,6 @@ __metadata: languageName: node linkType: hard -"ip-address@npm:^10.0.1": - version: 10.1.0 - resolution: "ip-address@npm:10.1.0" - checksum: 10c0/0103516cfa93f6433b3bd7333fa876eb21263912329bfa47010af5e16934eeeff86f3d2ae700a3744a137839ddfad62b900c7a445607884a49b5d1e32a3d7566 - languageName: node - linkType: hard - "is-binary-path@npm:~2.1.0": version: 2.1.0 resolution: "is-binary-path@npm:2.1.0" @@ -2957,9 +2790,9 @@ __metadata: linkType: hard "isexe@npm:^3.1.1": - version: 3.1.1 - resolution: "isexe@npm:3.1.1" - checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + version: 3.1.5 + resolution: "isexe@npm:3.1.5" + checksum: 10c0/8be2973a09f2f804ea1f34bfccfd5ea219ef48083bdb12107fe5bcf96b3e36b85084409e1b09ddaf2fae8927fdd9f6d70d90baadb78caa1ca7c530935706c8a4 languageName: node linkType: hard @@ -3068,9 +2901,9 @@ __metadata: linkType: hard "lodash@npm:^4.17.15": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c + version: 4.18.1 + resolution: "lodash@npm:4.18.1" + checksum: 10c0/757228fc68805c59789e82185135cf85f05d0b2d3d54631d680ca79ec21944ec8314d4533639a14b8bcfbd97a517e78960933041a5af17ecb693ec6eecb99a27 languageName: node linkType: hard @@ -3107,22 +2940,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": - version: 11.2.7 - resolution: "lru-cache@npm:11.2.7" - checksum: 10c0/549cdb59488baa617135fc12159cafb1a97f91079f35093bb3bcad72e849fc64ace636d244212c181dfdf1a99bbfa90757ff303f98561958ee4d0f885d9bd5f7 - languageName: node - linkType: hard - -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10c0/cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 - languageName: node - linkType: hard - "lunr@npm:^2.3.9": version: 2.3.9 resolution: "lunr@npm:2.3.9" @@ -3137,26 +2954,7 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^15.0.0": - version: 15.0.4 - resolution: "make-fetch-happen@npm:15.0.4" - dependencies: - "@gar/promise-retry": "npm:^1.0.0" - "@npmcli/agent": "npm:^4.0.0" - cacache: "npm:^20.0.1" - http-cache-semantics: "npm:^4.1.1" - minipass: "npm:^7.0.2" - minipass-fetch: "npm:^5.0.0" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^1.0.0" - proc-log: "npm:^6.0.0" - ssri: "npm:^13.0.0" - checksum: 10c0/b874bf6879fc0b8ef3a3cafdddadea4d956acf94790f8dede1a9d3c74c7886b6cd3eb992616b8e5935e6fd550016a465f10ba51bf6723a0c6f4d98883ae2926b - languageName: node - linkType: hard - -"markdown-it@npm:^14.1.0": +"markdown-it@npm:^14.1.1": version: 14.1.1 resolution: "markdown-it@npm:14.1.1" dependencies: @@ -3222,12 +3020,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.2.2": - version: 10.2.4 - resolution: "minimatch@npm:10.2.4" +"minimatch@npm:^10.2.5": + version: 10.2.5 + resolution: "minimatch@npm:10.2.5" dependencies: - brace-expansion: "npm:^5.0.2" - checksum: 10c0/35f3dfb7b99b51efd46afd378486889f590e7efb10e0f6a10ba6800428cf65c9a8dedb74427d0570b318d749b543dc4e85f06d46d2858bc8cac7e1eb49a95945 + brace-expansion: "npm:^5.0.5" + checksum: 10c0/6bb058bd6324104b9ec2f763476a35386d05079c1f5fe4fbf1f324a25237cd4534d6813ecd71f48208f4e635c1221899bef94c3c89f7df55698fe373aaae20fd languageName: node linkType: hard @@ -3256,74 +3054,14 @@ __metadata: languageName: node linkType: hard -"minipass-collect@npm:^2.0.1": - version: 2.0.1 - resolution: "minipass-collect@npm:2.0.1" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e - languageName: node - linkType: hard - -"minipass-fetch@npm:^5.0.0": - version: 5.0.2 - resolution: "minipass-fetch@npm:5.0.2" - dependencies: - iconv-lite: "npm:^0.7.2" - minipass: "npm:^7.0.3" - minipass-sized: "npm:^2.0.0" - minizlib: "npm:^3.0.1" - dependenciesMeta: - iconv-lite: - optional: true - checksum: 10c0/ce4ab9f21cfabaead2097d95dd33f485af8072fbc6b19611bce694965393453a1639d641c2bcf1c48f2ea7d41ea7fab8278373f1d0bee4e63b0a5b2cdd0ef649 - languageName: node - linkType: hard - -"minipass-flush@npm:^1.0.5": - version: 1.0.5 - resolution: "minipass-flush@npm:1.0.5" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd - languageName: node - linkType: hard - -"minipass-pipeline@npm:^1.2.4": - version: 1.2.4 - resolution: "minipass-pipeline@npm:1.2.4" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 - languageName: node - linkType: hard - -"minipass-sized@npm:^2.0.0": - version: 2.0.0 - resolution: "minipass-sized@npm:2.0.0" - dependencies: - minipass: "npm:^7.1.2" - checksum: 10c0/f9201696a6f6d68610d04c9c83e3d2e5cb9c026aae1c8cbf7e17f386105cb79c1bb088dbc21bf0b1eb4f3fb5df384fd1e7aa3bf1f33868c416ae8c8a92679db8 - languageName: node - linkType: hard - -"minipass@npm:^3.0.0": - version: 3.3.6 - resolution: "minipass@npm:3.3.6" - dependencies: - yallist: "npm:^4.0.0" - checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c - languageName: node - linkType: hard - -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2, minipass@npm:^7.1.3": +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.4, minipass@npm:^7.1.2": version: 7.1.3 resolution: "minipass@npm:7.1.3" checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb languageName: node linkType: hard -"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": +"minizlib@npm:^3.1.0": version: 3.1.0 resolution: "minizlib@npm:3.1.0" dependencies: @@ -3442,13 +3180,6 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^1.0.0": - version: 1.0.0 - resolution: "negotiator@npm:1.0.0" - checksum: 10c0/4c559dd52669ea48e1914f9d634227c561221dd54734070791f999c52ed0ff36e437b2e07d5c1f6e32909fc625fe46491c16e4a8f0572567d4dd15c3a4fda04b - languageName: node - linkType: hard - "node-addon-api@npm:^7.1.1": version: 7.1.1 resolution: "node-addon-api@npm:7.1.1" @@ -3459,22 +3190,22 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 12.2.0 - resolution: "node-gyp@npm:12.2.0" + version: 12.3.0 + resolution: "node-gyp@npm:12.3.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^15.0.0" nopt: "npm:^9.0.0" proc-log: "npm:^6.0.0" semver: "npm:^7.3.5" tar: "npm:^7.5.4" tinyglobby: "npm:^0.2.12" + undici: "npm:^6.25.0" which: "npm:^6.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 10c0/3ed046746a5a7d90950cd8b0547332b06598443f31fe213ef4332a7174c7b7d259e1704835feda79b87d3f02e59d7791842aac60642ede4396ab25fdf0f8f759 + checksum: 10c0/9d9032b405cbe42f72a105259d9eb679376470c102df4a2dbaa51e07d59bf741dcffb85897087ea9d8318b9cabb824a8978af51508ae142f0239ae1e6a3c2329 languageName: node linkType: hard @@ -3544,13 +3275,6 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^7.0.2": - version: 7.0.4 - resolution: "p-map@npm:7.0.4" - checksum: 10c0/a5030935d3cb2919d7e89454d1ce82141e6f9955413658b8c9403cfe379283770ed3048146b44cde168aa9e8c716505f196d5689db0ae3ce9a71521a2fef3abd - languageName: node - linkType: hard - "package-json-from-dist@npm:^1.0.0": version: 1.0.1 resolution: "package-json-from-dist@npm:1.0.1" @@ -3565,10 +3289,10 @@ __metadata: languageName: node linkType: hard -"path-expression-matcher@npm:^1.1.3": - version: 1.1.3 - resolution: "path-expression-matcher@npm:1.1.3" - checksum: 10c0/45c01471bc62c5f38d069418aec831763e6f45bb85f9520b08de441e6cd14f84b3098ecb66255e819c2af21102abcd2b45550dc1285996717ce9292802df2bc5 +"path-expression-matcher@npm:^1.1.3, path-expression-matcher@npm:^1.5.0": + version: 1.5.0 + resolution: "path-expression-matcher@npm:1.5.0" + checksum: 10c0/646cb5bc66cd7d809a52288336f3ac1e6223f156fd8e912936e490e590f7f93e8056d4fd25fcbcc7da61bb698fa520112cb050372a3f65e7b79bd4afa0f77610 languageName: node linkType: hard @@ -3589,16 +3313,6 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^2.0.2": - version: 2.0.2 - resolution: "path-scurry@npm:2.0.2" - dependencies: - lru-cache: "npm:^11.0.0" - minipass: "npm:^7.1.2" - checksum: 10c0/b35ad37cf6557a87fd057121ce2be7695380c9138d93e87ae928609da259ea0a170fac6f3ef1eb3ece8a068e8b7f2f3adf5bb2374cf4d4a57fe484954fcc9482 - languageName: node - linkType: hard - "pathval@npm:^1.1.1": version: 1.1.1 resolution: "pathval@npm:1.1.1" @@ -3621,16 +3335,16 @@ __metadata: linkType: hard "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1": - version: 2.3.1 - resolution: "picomatch@npm:2.3.1" - checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be + version: 2.3.2 + resolution: "picomatch@npm:2.3.2" + checksum: 10c0/a554d1709e59be97d1acb9eaedbbc700a5c03dbd4579807baed95100b00420bc729335440ef15004ae2378984e2487a7c1cebd743cfdb72b6fa9ab69223c0d61 languageName: node linkType: hard -"picomatch@npm:^4.0.3": - version: 4.0.3 - resolution: "picomatch@npm:4.0.3" - checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2 +"picomatch@npm:^4.0.4": + version: 4.0.4 + resolution: "picomatch@npm:4.0.4" + checksum: 10c0/e2c6023372cc7b5764719a5ffb9da0f8e781212fa7ca4bd0562db929df8e117460f00dff3cb7509dacfc06b86de924b247f504d0ce1806a37fac4633081466b0 languageName: node linkType: hard @@ -3663,12 +3377,12 @@ __metadata: linkType: hard "pump@npm:^3.0.0": - version: 3.0.0 - resolution: "pump@npm:3.0.0" + version: 3.0.4 + resolution: "pump@npm:3.0.4" dependencies: end-of-stream: "npm:^1.1.0" once: "npm:^1.3.1" - checksum: 10c0/bbdeda4f747cdf47db97428f3a135728669e56a0ae5f354a9ac5b74556556f5446a46f720a8f14ca2ece5be9b4d5d23c346db02b555f46739934cc6c093a5478 + checksum: 10c0/2780e66b5471c19e3e3e1063b84f3f6a3a08367f24c5ed552f98cd5901e6ada27c7ad6495d4244f553fd03b01884a4561933064f053f47c8994d84fd352768ea languageName: node linkType: hard @@ -3782,13 +3496,6 @@ __metadata: languageName: node linkType: hard -"retry@npm:^0.13.1": - version: 0.13.1 - resolution: "retry@npm:0.13.1" - checksum: 10c0/9ae822ee19db2163497e074ea919780b1efa00431d197c7afdb950e42bf109196774b92a49fc9821f0b8b328a98eea6017410bfc5e8a0fc19c85c6d11adb3772 - languageName: node - linkType: hard - "roarr@npm:^2.15.3": version: 2.15.4 resolution: "roarr@npm:2.15.4" @@ -3817,13 +3524,6 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3.0.0": - version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" - checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 - languageName: node - linkType: hard - "semver-compare@npm:^1.0.0": version: 1.0.0 resolution: "semver-compare@npm:1.0.0" @@ -3840,18 +3540,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.2": - version: 7.5.4 - resolution: "semver@npm:7.5.4" - dependencies: - lru-cache: "npm:^6.0.0" - bin: - semver: bin/semver.js - checksum: 10c0/5160b06975a38b11c1ab55950cb5b8a23db78df88275d3d8a42ccf1f29e55112ac995b3a26a522c36e3b5f76b0445f1eef70d696b8c7862a2b4303d7b0e7609e - languageName: node - linkType: hard - -"semver@npm:^7.3.5": +"semver@npm:^7.3.2, semver@npm:^7.3.5": version: 7.7.4 resolution: "semver@npm:7.7.4" bin: @@ -3910,34 +3599,6 @@ __metadata: languageName: node linkType: hard -"smart-buffer@npm:^4.2.0": - version: 4.2.0 - resolution: "smart-buffer@npm:4.2.0" - checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 - languageName: node - linkType: hard - -"socks-proxy-agent@npm:^8.0.3": - version: 8.0.5 - resolution: "socks-proxy-agent@npm:8.0.5" - dependencies: - agent-base: "npm:^7.1.2" - debug: "npm:^4.3.4" - socks: "npm:^2.8.3" - checksum: 10c0/5d2c6cecba6821389aabf18728325730504bf9bb1d9e342e7987a5d13badd7a98838cc9a55b8ed3cb866ad37cc23e1086f09c4d72d93105ce9dfe76330e9d2a6 - languageName: node - linkType: hard - -"socks@npm:^2.8.3": - version: 2.8.7 - resolution: "socks@npm:2.8.7" - dependencies: - ip-address: "npm:^10.0.1" - smart-buffer: "npm:^4.2.0" - checksum: 10c0/2805a43a1c4bcf9ebf6e018268d87b32b32b06fbbc1f9282573583acc155860dc361500f89c73bfbb157caa1b4ac78059eac0ef15d1811eb0ca75e0bdadbc9d2 - languageName: node - linkType: hard - "source-map-support@npm:^0.5.6": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" @@ -3962,15 +3623,6 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^13.0.0": - version: 13.0.1 - resolution: "ssri@npm:13.0.1" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10c0/cf6408a18676c57ff2ed06b8a20dc64bb3e748e5c7e095332e6aecaa2b8422b1e94a739a8453bf65156a8a47afe23757ba4ab52d3ea3b62322dc40875763e17a - languageName: node - linkType: hard - "stream-browserify@npm:3.0.0": version: 3.0.0 resolution: "stream-browserify@npm:3.0.0" @@ -3981,14 +3633,14 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.12.5, streamx@npm:^2.15.0, streamx@npm:^2.21.0": - version: 2.23.0 - resolution: "streamx@npm:2.23.0" +"streamx@npm:^2.12.5, streamx@npm:^2.15.0, streamx@npm:^2.25.0": + version: 2.25.0 + resolution: "streamx@npm:2.25.0" dependencies: events-universal: "npm:^1.0.0" fast-fifo: "npm:^1.3.2" text-decoder: "npm:^1.1.0" - checksum: 10c0/15708ce37818d588632fe1104e8febde573e33e8c0868bf583fce0703f3faf8d2a063c278e30df2270206811b69997f64eb78792099933a1fe757e786fbcbd44 + checksum: 10c0/1ecc4b722050e9088b99cde59d035e846ac97cedc3ef14a00b196d9c0b6f47d9fd18df454a19f56f0f586ab4f23fb7229069b9e8eaf22072a21bd9c909d4e0ea languageName: node linkType: hard @@ -4066,10 +3718,10 @@ __metadata: languageName: node linkType: hard -"strnum@npm:^2.1.2": - version: 2.2.0 - resolution: "strnum@npm:2.2.0" - checksum: 10c0/9a656f5048047abff8d10d0bb57761a01916e368a71e95d4f5a962b57f64b738e20672e68ba10b7de3dc78e861c77bc0566bdeed7017abdda1caf0303c929a3f +"strnum@npm:^2.2.3": + version: 2.2.3 + resolution: "strnum@npm:2.2.3" + checksum: 10c0/1ee78101f1cd73a5b32f63cfd0be501bd246801a002f5987efef903a49e9297d1b63574e302ab3c06ee5e715c524d6cbdfef010e372ec1ea848e0179836cc208 languageName: node linkType: hard @@ -4113,15 +3765,15 @@ __metadata: linkType: hard "tar@npm:^7.5.4": - version: 7.5.11 - resolution: "tar@npm:7.5.11" + version: 7.5.13 + resolution: "tar@npm:7.5.13" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/b6bb420550ef50ef23356018155e956cd83282c97b6128d8d5cfe5740c57582d806a244b2ef0bf686a74ce526babe8b8b9061527623e935e850008d86d838929 + checksum: 10c0/5c65b8084799bde7a791593a1c1a45d3d6ee98182e3700b24c247b7b8f8654df4191642abbdb07ff25043d45dcff35620827c3997b88ae6c12040f64bed5076b languageName: node linkType: hard @@ -4144,12 +3796,12 @@ __metadata: linkType: hard "tinyglobby@npm:^0.2.12": - version: 0.2.15 - resolution: "tinyglobby@npm:0.2.15" + version: 0.2.16 + resolution: "tinyglobby@npm:0.2.16" dependencies: fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.3" - checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844 + picomatch: "npm:^4.0.4" + checksum: 10c0/f2e09fd93dd95c41e522113b686ff6f7c13020962f8698a864a257f3d7737599afc47722b7ab726e12f8a813f779906187911ff8ee6701ede65072671a7e934b languageName: node linkType: hard @@ -4187,10 +3839,10 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:^4.0.0, type-detect@npm:^4.0.8": - version: 4.0.8 - resolution: "type-detect@npm:4.0.8" - checksum: 10c0/8fb9a51d3f365a7de84ab7f73b653534b61b622aa6800aecdb0f1095a4a646d3f5eb295322127b6573db7982afcd40ab492d038cf825a42093a58b1e1353e0bd +"type-detect@npm:^4.0.0, type-detect@npm:^4.1.0": + version: 4.1.0 + resolution: "type-detect@npm:4.1.0" + checksum: 10c0/df8157ca3f5d311edc22885abc134e18ff8ffbc93d6a9848af5b682730ca6a5a44499259750197250479c5331a8a75b5537529df5ec410622041650a7f293e2a languageName: node linkType: hard @@ -4202,28 +3854,28 @@ __metadata: linkType: hard "typedoc-plugin-markdown@npm:^4.0.0": - version: 4.10.0 - resolution: "typedoc-plugin-markdown@npm:4.10.0" + version: 4.11.0 + resolution: "typedoc-plugin-markdown@npm:4.11.0" peerDependencies: typedoc: 0.28.x - checksum: 10c0/20c7bc8ef68bd90053649ce223d02d4aceefed675c09efb1740c7791fbc37c10a1e25d14647605484a198f0695312eb21119015616d91c73fe1d63df5e4fb061 + checksum: 10c0/03374acfd0b5bd5af13198c043ffc31324b097647f5ffd92959647a9a277f0ece665331ff6a650ddaefdf9ff33928e138c5483e7cddbac830eb77e69b6067803 languageName: node linkType: hard "typedoc@npm:^0.28.0": - version: 0.28.17 - resolution: "typedoc@npm:0.28.17" + version: 0.28.19 + resolution: "typedoc@npm:0.28.19" dependencies: - "@gerrit0/mini-shiki": "npm:^3.17.0" + "@gerrit0/mini-shiki": "npm:^3.23.0" lunr: "npm:^2.3.9" - markdown-it: "npm:^14.1.0" - minimatch: "npm:^9.0.5" - yaml: "npm:^2.8.1" + markdown-it: "npm:^14.1.1" + minimatch: "npm:^10.2.5" + yaml: "npm:^2.8.3" peerDependencies: - typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x bin: typedoc: bin/typedoc - checksum: 10c0/25c3f6c08748debd2549e8af2c96dcdea255297924e8e0ecc78c86aea35d69c149eb5ad0a0d333a3a69d4e41a887ce55fef0aa97236789f0e658f3ad051429e8 + checksum: 10c0/a46ea8ec4e661320dacf27f1d68595bdf9d671383f20060b590f212e6ebbf9a65d4962e698e8a43804a14add1f04a3528381c338801350dc03ddc6baf33fb898 languageName: node linkType: hard @@ -4261,21 +3913,24 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^5.0.0": - version: 5.0.0 - resolution: "unique-filename@npm:5.0.0" - dependencies: - unique-slug: "npm:^6.0.0" - checksum: 10c0/afb897e9cf4c2fb622ea716f7c2bb462001928fc5f437972213afdf1cc32101a230c0f1e9d96fc91ee5185eca0f2feb34127145874975f347be52eb91d6ccc2c +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04 languageName: node linkType: hard -"unique-slug@npm:^6.0.0": - version: 6.0.0 - resolution: "unique-slug@npm:6.0.0" - dependencies: - imurmurhash: "npm:^0.1.4" - checksum: 10c0/da7ade4cb04eb33ad0499861f82fe95ce9c7c878b7139dc54d140ecfb6a6541c18a5c8dac16188b8b379fe62c0c1f1b710814baac910cde5f4fec06212126c6a +"undici-types@npm:~7.19.0": + version: 7.19.2 + resolution: "undici-types@npm:7.19.2" + checksum: 10c0/7159f10546f9f6c47d36776bb1bbf8671e87c1e587a6fee84ae1f111ae8de4f914efa8ca0dfcd224f4f4a9dfc3f6028f627ccb5ddaccf82d7fd54671b89fac3e + languageName: node + linkType: hard + +"undici@npm:^6.25.0": + version: 6.25.0 + resolution: "undici@npm:6.25.0" + checksum: 10c0/2597cc6689bdb02c210c557b1f85febbfda65becae6e6fc1061508e2f33734d25207f81cd8af56ada9956329eb3a7bd7431e87dcfeceba20ee87059b57dcf985 languageName: node linkType: hard @@ -4399,13 +4054,6 @@ __metadata: languageName: node linkType: hard -"yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a - languageName: node - linkType: hard - "yallist@npm:^5.0.0": version: 5.0.0 resolution: "yallist@npm:5.0.0" @@ -4413,12 +4061,12 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.8.1": - version: 2.8.2 - resolution: "yaml@npm:2.8.2" +"yaml@npm:^2.8.3": + version: 2.8.3 + resolution: "yaml@npm:2.8.3" bin: yaml: bin.mjs - checksum: 10c0/703e4dc1e34b324aa66876d63618dcacb9ed49f7e7fe9b70f1e703645be8d640f68ab84f12b86df8ac960bac37acf5513e115de7c970940617ce0343c8c9cd96 + checksum: 10c0/ddff0e11c1b467728d7eb4633db61c5f5de3d8e9373cf84d08fb0cdee03e1f58f02b9f1c51a4a8a865751695addbd465a77f73f1079be91fe5493b29c305fd77 languageName: node linkType: hard