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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions js/module.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,7 @@ interface IAutoOptimizerOutputResult {
videos: IAutoOptimizerVideoRecommendation[];
encoding?: IAutoOptimizerEncodingRecommendation;
measurement: IAutoOptimizerMeasurement;
warnings?: string[];
}
type AutoOptimizerFatalErrorCode = 'cancelled' | 'hardware_no_usable_encoder' | 'hardware_benchmark_overloaded' | 'hardware_benchmark_timeout' | 'hardware_benchmark_unavailable' | 'auto_optimizer_worker_failed' | 'auto_optimizer_worker_launch_failed';
interface IAutoOptimizerError {
Expand Down
29 changes: 28 additions & 1 deletion js/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1836,6 +1836,13 @@ export interface IStreaming {
network: INetwork,
video: IVideo,
signalHandler: (signal: EOutputSignal) => void,
/**
* Returns encoder options and mode-specific metadata, filtered by the assigned service.
* Works for ordinary and Enhanced Broadcasting outputs without starting a stream or
* requesting a Twitch configuration; this is not the active Enhanced Broadcasting ladder.
* The instance must remain alive. A failed query throws a JavaScript Error without
* changing the output or its encoders.
*/
getAvailableEncoders(): IEncoderOption[],
start(): void, // throws
stop(force?: boolean): void,
Expand Down Expand Up @@ -2146,16 +2153,26 @@ interface IAutoOptimizerLimits {
* settings are managed by Desktop. A provider probe may test above this
* value to verify stability or shared upload capacity, but the
* recommendation never exceeds it.
* When multiple distinct platforms are selected across the request, OSN
* additionally caps each ordinary output at 6000 Kbps. Twitch Enhanced
* Broadcasting retains its platform-managed bitrate ladder.
*/
maxBitrateKbps?: number;
/**
* Maximum resolution OSN may test for this run without changing persistent
* video settings. Supply `maxWidth` and `maxHeight` together. OSN tests only
* the supported 1920x1080, 1280x720, and 960x540 tiers, or their portrait
* the supported 2560x1440, 1920x1080, 1280x720, and 960x540 tiers, or their portrait
* equivalents, up to this limit. It promotes only 16:9 or 9:16 output; a
* custom aspect ratio keeps its current resolution and frame rate. The
* caller remains responsible for applying a recommended Base Canvas resize
* safely.
* Horizontal 1440p testing is available only when every destination in the
* request is Twitch. Other streams are limited to 1080p. Vertical testing
* remains limited to 1080x1920, including Twitch Enhanced Broadcasting's
* paired 1440p horizontal / 1080p vertical workload. Ordinary Twitch 1440p
* requests use an experimental 8000 Kbps limit instead of the service's
* 6000 Kbps default; the caller must apply that tested bitrate without
* reapplying the lower service bitrate limit.
*/
maxWidth?: number;
maxHeight?: number;
Expand Down Expand Up @@ -2192,6 +2209,9 @@ interface IAutoOptimizerOutputRequest {
* Broadcasting upload as `current`. Valid only with `display: 'both'`. OSN
* validates the paired ladder only for one Twitch destination, one Enhanced
* Broadcasting probe, and two distinct registered canvas IDs.
* Without a successful paired probe, an estimated result preserves this
* video's current resolution and frame rate; it does not validate that
* the saved pair can be streamed.
*/
additionalVideo?: IAutoOptimizerAdditionalVideoRequest;
estimateReason?: AutoOptimizerEstimateReason;
Expand Down Expand Up @@ -2393,6 +2413,13 @@ interface IAutoOptimizerOutputResult {
/** Omitted for Twitch Enhanced Broadcasting because Twitch supplies its encoding ladder. */
encoding?: IAutoOptimizerEncodingRecommendation;
measurement: IAutoOptimizerMeasurement;
/**
* Non-fatal warning codes for the recommended settings, independent of
* measurement confidence. `enhanced_broadcasting_configuration_warning`
* means Twitch returned a warning for the selected configuration even
* though its probe passed. Twitch's HTML message is not exposed.
*/
warnings?: string[];
}

type AutoOptimizerFatalErrorCode =
Expand Down
22 changes: 18 additions & 4 deletions obs-studio-client/source/auto-optimizer-client-contract.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "auto-optimizer-client-contract.hpp"

#include "auto-optimizer-quality-policy.hpp"
#include "auto-optimizer-enhanced-broadcasting-policy.hpp"
#include "auto-optimizer-probe-policy.hpp"
#include "nlohmann/json.hpp"

Expand Down Expand Up @@ -661,11 +662,18 @@ bool parseRecommendation(const json &value, const OutputContext &output, const s
if (result.additionalVideo) {
const auto &additional = *result.additionalVideo;
const auto &additionalContext = *output.additionalVideo;
if (additional["width"] != result.height || additional["height"] != result.width ||
(measurementMode == "active" && static_cast<int64_t>(additional["fpsNum"].get<int>()) * result.fpsDen !=
static_cast<int64_t>(result.fpsNum) * additional["fpsDen"].get<int>()) ||
!tupleWithinLimits(additional["width"], additional["height"], additional["fpsNum"], additional["fpsDen"], additionalContext.limits))
if (!tupleWithinLimits(additional["width"], additional["height"], additional["fpsNum"], additional["fpsDen"], additionalContext.limits))
return false;
// Only measured results must match Twitch's tested pair. Estimates retain
// the saved vertical settings, which may differ in resolution or cadence.
if (measurementMode == "active") {
const auto paired = enhancedBroadcastingPolicy::pairedVerticalCandidate(
{(uint32_t)result.width, (uint32_t)result.height, (uint32_t)result.fpsNum, (uint32_t)result.fpsDen});
if (additional["width"] != paired.width || additional["height"] != paired.height ||
static_cast<int64_t>(additional["fpsNum"].get<int>()) * result.fpsDen !=
static_cast<int64_t>(result.fpsNum) * additional["fpsDen"].get<int>())
return false;
}
if (measurementMode == "estimated" &&
(additional["width"] != additionalContext.current.width || additional["height"] != additionalContext.current.height ||
static_cast<int64_t>(additional["fpsNum"].get<int>()) * additionalContext.current.fpsDen !=
Expand Down Expand Up @@ -764,6 +772,10 @@ bool parseOutputResult(const json &value, const OutputContext &expected, ParsedO
return false;

const auto &measurement = value["measurement"];
if (measurement.contains("configurationWarning") &&
(!measurement["configurationWarning"].is_boolean() || expected.outputKind != "twitch-enhanced-broadcasting" ||
(measurement["configurationWarning"].get<bool>() && measurement.value("mode", "") != "active")))
return false;
result.measurementMode = measurement.value("mode", "");
result.confidence = measurement.value("confidence", "");
if ((result.measurementMode != "active" && result.measurementMode != "estimated") ||
Expand Down Expand Up @@ -806,6 +818,8 @@ bool parseOutputResult(const json &value, const OutputContext &expected, ParsedO
sharedUploadEstimate))
return false;
result.projected.update(std::move(recommendationProjection));
if (measurement.value("configurationWarning", false))
result.projected["warnings"] = {"enhanced_broadcasting_configuration_warning"};
result.projected["measurement"] = {{"mode", result.measurementMode}, {"confidence", result.confidence}};
if (result.reason)
result.projected["measurement"]["reason"] = *result.reason;
Expand Down
95 changes: 93 additions & 2 deletions obs-studio-client/tests/auto-optimizer-client-contract-test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -585,10 +585,80 @@ TEST_CASE("Auto Optimizer client requires the exact active Dual Output aggregate
{"divergent preset", [](json &value) { value["legs"][1]["recommendation"]["preset"] = "veryfast"; }}});
}

TEST_CASE("Auto Optimizer client requires exact proof for a 1440p horizontal and 1080p vertical pair")
{
json output = enhancedBroadcastingOutput();
output["limits"] = {{"maxWidth", 2560}, {"maxHeight", 1440}};
const auto prepared = prepare({{"streamSetup", "enhanced-broadcasting"}, {"outputs", json::array({output})}});
json returned = returnedEnhancedBroadcastingOutput();
returned["recommendation"]["width"] = 2560;
returned["recommendation"]["height"] = 1440;
returned["measurement"]["probes"][0]["testedWidth"] = 2560;
returned["measurement"]["probes"][0]["testedHeight"] = 1440;
ResultFixture fixture{prepared, {{"schemaVersion", 1}, {"sessionId", "run"}, {"status", "complete"}, {"legs", json::array({returned})}}};
REQUIRE(contract::projectResult(fixture.result.dump(), "run", prepared.context).valid);
checkInvalidResultMutations(
fixture, {
{"unproven vertical size", [](json &value) { value["legs"][0]["recommendation"]["additionalVideo"]["width"] = 720; }},
{"unproven primary size", [](json &value) { value["legs"][0]["measurement"]["probes"][0]["testedWidth"] = 1920; }},
{"measured pair outside Twitch policy",
[](json &value) {
auto &output = value["legs"][0];
auto &vertical = output["recommendation"]["additionalVideo"];
vertical["width"] = 1440;
vertical["height"] = 2560;
output["measurement"]["probes"][0]["testedAdditionalVideo"] = vertical;
}},
});
}

TEST_CASE("Auto Optimizer client preserves saved Dual Format estimates without measured pairing requirements")
{
const bool failedProbe = GENERATE(false, true);
CAPTURE(failedProbe);
json output = enhancedBroadcastingOutput();
output["current"] = current(2560, 1440, 6000, 1);
output["additionalVideo"]["current"] = current(1440, 2560, 6000, 2);
output["additionalVideo"]["current"]["fpsNum"] = 30;
if (!failedProbe)
output.erase("probes");
const auto prepared = prepare({{"streamSetup", "enhanced-broadcasting"}, {"outputs", json::array({output})}});
json returned = returnedEnhancedBroadcastingOutput();
returned["recommendation"]["width"] = 2560;
returned["recommendation"]["height"] = 1440;
const json vertical = {{"display", "vertical"}, {"width", 1440}, {"height", 2560}, {"fpsNum", 30}, {"fpsDen", 1}};
returned["recommendation"]["additionalVideo"] = vertical;
returned["measurement"]["mode"] = "estimated";
returned["measurement"]["confidence"] = "medium";
if (failedProbe)
returned["measurement"]["probes"][0]["success"] = false;
else
returned["measurement"].erase("probes");
ResultFixture fixture{prepared, {{"schemaVersion", 1}, {"sessionId", "run"}, {"status", "complete"}, {"legs", json::array({returned})}}};
const auto result = contract::projectResult(fixture.result.dump(), "run", prepared.context);
REQUIRE(result.valid);
const auto projected = json::parse(result.json)["outputs"][0];
CHECK(projected["measurement"]["mode"] == "estimated");
CHECK(projected["videos"][1] == vertical);
CHECK_FALSE(projected.contains("encoding"));
checkInvalidResultMutations(
fixture, {{"changed saved vertical size",
[](json &value) {
value["legs"][0]["recommendation"]["additionalVideo"]["width"] = 1080;
value["legs"][0]["recommendation"]["additionalVideo"]["height"] = 1920;
}},
{"changed saved vertical cadence", [](json &value) { value["legs"][0]["recommendation"]["additionalVideo"]["fpsNum"] = 60; }},
{"unmeasured primary promotion", [](json &value) { value["legs"][0]["recommendation"]["fpsNum"] = 120; }}});
auto limited = prepared.context;
limited.outputs[0].additionalVideo->limits.maxWidth = 1080;
limited.outputs[0].additionalVideo->limits.maxHeight = 1920;
CHECK_FALSE(contract::projectResult(fixture.result.dump(), "run", limited).valid);
}

TEST_CASE("Auto Optimizer client preserves measured Enhanced Broadcasting fallback explanations")
{
for (const auto reason :
{"enhanced_broadcasting_transport_fallback", "enhanced_broadcasting_workload_fallback", "enhanced_broadcasting_transport_and_workload_fallback"}) {
for (const auto reason : {"enhanced_broadcasting_transport_fallback", "enhanced_broadcasting_workload_fallback",
"enhanced_broadcasting_transport_and_workload_fallback", "enhanced_broadcasting_configuration_fallback"}) {
CAPTURE(reason);
auto fixture = enhancedBroadcastingFixture();
fixture.result["legs"][0]["measurement"]["reason"] = reason;
Expand All @@ -604,6 +674,27 @@ TEST_CASE("Auto Optimizer client preserves measured Enhanced Broadcasting fallba
}
}

TEST_CASE("Auto Optimizer projects Twitch configuration warnings separately from measurement confidence")
{
auto fixture = enhancedBroadcastingFixture();
auto &measurement = fixture.result["legs"][0]["measurement"];
measurement["configurationWarning"] = true;
measurement["reason"] = "enhanced_broadcasting_configuration_fallback";
const auto result = contract::projectResult(fixture.result.dump(), "run", fixture.prepared.context);
REQUIRE(result.valid);
const auto output = json::parse(result.json)["outputs"][0];
CHECK(output["warnings"] == json::array({"enhanced_broadcasting_configuration_warning"}));
CHECK(output["measurement"]["reason"] == measurement["reason"]);
CHECK(output["measurement"]["confidence"] == "high");
CHECK_FALSE(output["measurement"].contains("configurationWarning"));
measurement["configurationWarning"] = false;
const auto noWarning = contract::projectResult(fixture.result.dump(), "run", fixture.prepared.context);
REQUIRE(noWarning.valid);
CHECK_FALSE(json::parse(noWarning.json)["outputs"][0].contains("warnings"));
measurement["configurationWarning"] = "<b>untrusted platform text</b>";
CHECK_FALSE(contract::projectResult(fixture.result.dump(), "run", fixture.prepared.context).valid);
}

TEST_CASE("Auto Optimizer client requires exact Enhanced Broadcasting combined workload proof")
{
const auto fixture = enhancedBroadcastingFixture();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@

namespace policy = autoOptimizer::enhancedBroadcastingPolicy;

TEST_CASE("Enhanced Broadcasting probes preserve discovered H.264 and HEVC independently of resolution")
{
std::unordered_set<std::string> codecs{"h264", "h265", "av1", "vp9"};
policy::filterProbeCodecs(codecs);
CHECK(codecs == std::unordered_set<std::string>{"h264", "h265"});
}

TEST_CASE("Enhanced Broadcasting probes do not add undiscovered codecs")
{
for (const auto &available : std::vector<std::unordered_set<std::string>>{{"h264"}, {"h265"}, {}}) {
auto filtered = available;
policy::filterProbeCodecs(filtered);
CHECK(filtered == available);
}
}

TEST_CASE("Enhanced Broadcasting probes exclude AV1 even when it is the only discovered codec")
{
std::unordered_set<std::string> codecs{"av1"};
policy::filterProbeCodecs(codecs);
CHECK(codecs.empty());
}

TEST_CASE("Enhanced Broadcasting candidates are ordered and capped at 1080p")
{
const auto result = policy::candidates(1920, 1080, 60, 1);
Expand Down Expand Up @@ -144,7 +167,9 @@ TEST_CASE("Enhanced Broadcasting fallback reports retain earlier upload instabil
policy::CandidateFallbackEvidence evidence;
CHECK(evidence.reason().empty());
evidence.record("enhanced_broadcasting_ladder_below_candidate");
CHECK(evidence.reason().empty());
CHECK(evidence.reason() == "enhanced_broadcasting_configuration_fallback");
CHECK_FALSE(evidence.workloadPressure);
CHECK_FALSE(evidence.transportPressure);
evidence.record("enhanced_broadcasting_transport_pressure");
CHECK(evidence.reason() == "enhanced_broadcasting_transport_fallback");
evidence.record("enhanced_broadcasting_ladder_below_candidate");
Expand All @@ -153,6 +178,19 @@ TEST_CASE("Enhanced Broadcasting fallback reports retain earlier upload instabil
CHECK(policy::CandidateFallbackEvidence{}.reason().empty());
}

TEST_CASE("Enhanced Broadcasting configuration fallback does not imply measured overload")
{
for (const auto error : {"enhanced_broadcasting_ladder_below_candidate", "enhanced_broadcasting_config_request_failed"}) {
policy::CandidateFallbackEvidence evidence;
evidence.record(error);
CHECK(evidence.reason() == "enhanced_broadcasting_configuration_fallback");
CHECK_FALSE(evidence.workloadPressure);
CHECK_FALSE(evidence.transportPressure);
evidence.record("enhanced_broadcasting_render_overload");
CHECK(evidence.reason() == "enhanced_broadcasting_workload_fallback");
}
}

TEST_CASE("Enhanced Broadcasting fallback reports distinguish workload and upload failures")
{
for (const auto error :
Expand All @@ -170,3 +208,15 @@ TEST_CASE("Enhanced Broadcasting fallback reports distinguish workload and uploa
CHECK(reversed.reason() == evidence.reason());
}
}
TEST_CASE("Enhanced Broadcasting 1440p pairs with a 1080p vertical workload")
{
const auto candidates = autoOptimizer::enhancedBroadcastingPolicy::candidates(2560, 1440, 60, 1);
REQUIRE(candidates.size() == 7);
CHECK(candidates.front().width == 2560);
CHECK(candidates.front().height == 1440);
const auto vertical = autoOptimizer::enhancedBroadcastingPolicy::pairedVerticalCandidate(candidates.front());
CHECK(vertical.width == 1080);
CHECK(vertical.height == 1920);
CHECK(vertical.fpsNum == 60);
CHECK(autoOptimizer::enhancedBroadcastingPolicy::candidates(1920, 1080, 60, 1).size() == 5);
}
Loading
Loading