Skip to content

Commit 8f4093f

Browse files
Improve YouTube bandwidth probe stability detection
1 parent 72dd26a commit 8f4093f

6 files changed

Lines changed: 858 additions & 161 deletions

File tree

js/module.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,15 +1033,15 @@ export interface IAutoConfigLegRequest {
10331033
}
10341034
export interface IAutoConfigTwitchActiveProbe {
10351035
probeId: string;
1036-
kind: 'twitch-standard-v1';
1036+
kind: 'twitch-standard';
10371037
legId: string;
10381038
serviceName: 'Twitch';
10391039
server: string;
10401040
streamKey: string;
10411041
}
10421042
export interface IAutoConfigYoutubeActiveProbe {
10431043
probeId: string;
1044-
kind: 'youtube-unbound-v1';
1044+
kind: 'youtube-unbound';
10451045
legId: string;
10461046
serviceName: 'YouTube - RTMPS';
10471047
server: string;
@@ -1073,7 +1073,7 @@ export interface IAutoConfigEvent {
10731073
}
10741074
export interface IAutoConfigProbeMeasurement {
10751075
provider: 'twitch' | 'youtube';
1076-
method: 'twitch-bandwidth-test-v1' | 'youtube-unbound-ramp-v1';
1076+
method: 'twitch-bandwidth-test' | 'youtube-unbound-ramp';
10771077
success: boolean;
10781078
measuredKbps?: number;
10791079
safeKbps?: number;

js/module.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,7 +1961,7 @@ export interface IAudioTrackFactory {
19611961
saveLegacySettings(): void;
19621962
}
19631963

1964-
// ---- Auto Optimizer API v1 (native API version 2) ----
1964+
// ---- Auto Optimizer API ----
19651965

19661966
export interface IAutoConfigCapabilities {
19671967
apiVersion: 2;
@@ -2038,7 +2038,7 @@ export interface IAutoConfigLegRequest {
20382038

20392039
export interface IAutoConfigTwitchActiveProbe {
20402040
probeId: string;
2041-
kind: 'twitch-standard-v1';
2041+
kind: 'twitch-standard';
20422042
legId: string;
20432043
serviceName: 'Twitch';
20442044
server: string;
@@ -2054,7 +2054,7 @@ export interface IAutoConfigYoutubeActiveProbe {
20542054
* before deleting the liveStream through the YouTube API.
20552055
*/
20562056
probeId: string;
2057-
kind: 'youtube-unbound-v1';
2057+
kind: 'youtube-unbound';
20582058
legId: string;
20592059
serviceName: 'YouTube - RTMPS';
20602060
server: string;
@@ -2092,7 +2092,7 @@ export interface IAutoConfigEvent {
20922092

20932093
export interface IAutoConfigProbeMeasurement {
20942094
provider: 'twitch' | 'youtube';
2095-
method: 'twitch-bandwidth-test-v1' | 'youtube-unbound-ramp-v1';
2095+
method: 'twitch-bandwidth-test' | 'youtube-unbound-ramp';
20962096
success: boolean;
20972097
/** Observed aggregate RTMP throughput, including audio. */
20982098
measuredKbps?: number;

obs-studio-client/tests/autoconfig-probe-policy-test.cpp

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,124 @@
33
#include <catch2/catch_test_macros.hpp>
44

55
using autoConfig::probePolicy::YoutubeRampEvidence;
6+
using autoConfig::probePolicy::YoutubeBaselineAssessment;
7+
using autoConfig::probePolicy::YoutubeBaselineDecision;
8+
using autoConfig::probePolicy::YoutubeConfirmationDecision;
9+
using autoConfig::probePolicy::YoutubeProbeSampleClass;
10+
using autoConfig::probePolicy::YoutubeProbeSampleMetrics;
11+
using autoConfig::probePolicy::assessYoutubeBaseline;
12+
using autoConfig::probePolicy::classifyYoutubeProbeSample;
613
using autoConfig::probePolicy::clampEstimateToObservedSafe;
14+
using autoConfig::probePolicy::decideYoutubeConfirmation;
715
using autoConfig::probePolicy::effectiveProbeCeilingKbps;
816
using autoConfig::probePolicy::hasProbeThroughputMetrics;
17+
using autoConfig::probePolicy::makeYoutubeProbeSampleMetrics;
918
using autoConfig::probePolicy::probeSubstepProgress;
1019
using autoConfig::probePolicy::reachedEffectiveProbeCeiling;
20+
using autoConfig::probePolicy::resolveYoutubeBaseline;
1121
using autoConfig::probePolicy::safeVideoKbps;
22+
using autoConfig::probePolicy::youtubeLowControlRecovered;
23+
using autoConfig::probePolicy::youtubeSampleAccepted;
24+
25+
TEST_CASE("YouTube probe metrics use deterministic basis-point ratios")
26+
{
27+
const YoutubeProbeSampleMetrics sample = makeYoutubeProbeSampleMetrics(900, 1000, 3, 150, 10, 2, 100);
28+
29+
CHECK(sample.throughputBasisPoints == 9000);
30+
CHECK(sample.dropBasisPoints == 200);
31+
CHECK(sample.congestionHighBasisPoints == 1000);
32+
CHECK(sample.congestionSevereBasisPoints == 200);
33+
CHECK(classifyYoutubeProbeSample(sample) == YoutubeProbeSampleClass::Clean);
34+
}
35+
36+
TEST_CASE("YouTube probe sample classification honors clean and hard boundaries")
37+
{
38+
CHECK(classifyYoutubeProbeSample({9000, 200, 1000, 200}) == YoutubeProbeSampleClass::Clean);
39+
CHECK(classifyYoutubeProbeSample({8999, 200, 1000, 200}) == YoutubeProbeSampleClass::Marginal);
40+
CHECK(classifyYoutubeProbeSample({7500, 500, 3000, 1000}) == YoutubeProbeSampleClass::Marginal);
41+
CHECK(classifyYoutubeProbeSample({7499, 0, 0, 0}) == YoutubeProbeSampleClass::Hard);
42+
CHECK(classifyYoutubeProbeSample({10000, 501, 0, 0}) == YoutubeProbeSampleClass::Hard);
43+
CHECK(classifyYoutubeProbeSample({10000, 0, 3001, 0}) == YoutubeProbeSampleClass::Hard);
44+
CHECK(classifyYoutubeProbeSample({10000, 0, 0, 1001}) == YoutubeProbeSampleClass::Hard);
45+
}
46+
47+
TEST_CASE("YouTube probe ignores isolated congestion but rejects sustained congestion")
48+
{
49+
const YoutubeProbeSampleMetrics isolatedSpike = makeYoutubeProbeSampleMetrics(950, 1000, 0, 150, 1, 0, 100);
50+
const YoutubeProbeSampleMetrics sustainedCongestion = makeYoutubeProbeSampleMetrics(950, 1000, 0, 150, 31, 0, 100);
51+
52+
CHECK(classifyYoutubeProbeSample(isolatedSpike) == YoutubeProbeSampleClass::Clean);
53+
CHECK(classifyYoutubeProbeSample(sustainedCongestion) == YoutubeProbeSampleClass::Hard);
54+
}
55+
56+
TEST_CASE("Two YouTube baseline samples select every initial decision")
57+
{
58+
const YoutubeProbeSampleMetrics cleanA{9500, 100, 500, 100};
59+
const YoutubeProbeSampleMetrics cleanB{9300, 150, 700, 150};
60+
const YoutubeProbeSampleMetrics marginalA{8500, 300, 1500, 300};
61+
const YoutubeProbeSampleMetrics marginalB{8200, 350, 2000, 400};
62+
const YoutubeProbeSampleMetrics hardA{7000, 300, 1500, 300};
63+
const YoutubeProbeSampleMetrics hardB{8000, 600, 1500, 300};
64+
65+
CHECK(assessYoutubeBaseline(cleanA, cleanB).decision == YoutubeBaselineDecision::Clean);
66+
CHECK(assessYoutubeBaseline(cleanA, {10500, 200, 1000, 200}).decision == YoutubeBaselineDecision::NeedsThird);
67+
CHECK(assessYoutubeBaseline(marginalA, marginalB).decision == YoutubeBaselineDecision::Impaired);
68+
CHECK(assessYoutubeBaseline(cleanA, marginalA).decision == YoutubeBaselineDecision::NeedsThird);
69+
CHECK(assessYoutubeBaseline(hardA, hardB).decision == YoutubeBaselineDecision::Unstable);
70+
}
71+
72+
TEST_CASE("A third YouTube baseline sample resolves from component medians")
73+
{
74+
const YoutubeProbeSampleMetrics clean{9500, 100, 500, 100};
75+
const YoutubeProbeSampleMetrics marginal{8500, 300, 1500, 300};
76+
const YoutubeProbeSampleMetrics hard{6000, 700, 4000, 1500};
77+
78+
const YoutubeBaselineAssessment impaired = resolveYoutubeBaseline(clean, hard, marginal);
79+
CHECK(impaired.decision == YoutubeBaselineDecision::Impaired);
80+
CHECK(impaired.reference.throughputBasisPoints == marginal.throughputBasisPoints);
81+
CHECK(impaired.reference.dropBasisPoints == marginal.dropBasisPoints);
82+
CHECK(impaired.reference.congestionHighBasisPoints == marginal.congestionHighBasisPoints);
83+
CHECK(impaired.reference.congestionSevereBasisPoints == marginal.congestionSevereBasisPoints);
84+
85+
CHECK(resolveYoutubeBaseline(clean, hard, clean).decision == YoutubeBaselineDecision::Clean);
86+
CHECK(resolveYoutubeBaseline(hard, clean, hard).decision == YoutubeBaselineDecision::Unstable);
87+
CHECK(resolveYoutubeBaseline({7000, 100, 500, 100}, clean, {9500, 600, 500, 100}).decision == YoutubeBaselineDecision::Unstable);
88+
}
89+
90+
TEST_CASE("Impaired YouTube baseline accepts only bounded relative degradation")
91+
{
92+
const YoutubeBaselineAssessment baseline{YoutubeBaselineDecision::Impaired, {8500, 300, 1500, 300}};
93+
94+
CHECK(youtubeSampleAccepted({8000, 400, 2500, 800}, baseline));
95+
CHECK_FALSE(youtubeSampleAccepted({7999, 400, 2500, 800}, baseline));
96+
CHECK_FALSE(youtubeSampleAccepted({8000, 401, 2500, 800}, baseline));
97+
CHECK_FALSE(youtubeSampleAccepted({8000, 400, 2501, 800}, baseline));
98+
CHECK_FALSE(youtubeSampleAccepted({8000, 400, 2500, 801}, baseline));
99+
100+
const YoutubeBaselineAssessment cleanBaseline{YoutubeBaselineDecision::Clean, {9500, 100, 500, 100}};
101+
CHECK(youtubeSampleAccepted({9200, 100, 500, 100}, cleanBaseline));
102+
CHECK_FALSE(youtubeSampleAccepted({8500, 100, 500, 100}, cleanBaseline));
103+
}
104+
105+
TEST_CASE("YouTube low control must recover acceptance and the previous rung")
106+
{
107+
const YoutubeBaselineAssessment baseline{YoutubeBaselineDecision::Impaired, {8500, 300, 1500, 300}};
108+
const YoutubeProbeSampleMetrics original{8800, 300, 1000, 200};
109+
110+
CHECK(youtubeLowControlRecovered({8300, 400, 2000, 700}, original, baseline));
111+
CHECK_FALSE(youtubeLowControlRecovered({8299, 400, 2000, 700}, original, baseline));
112+
CHECK_FALSE(youtubeLowControlRecovered({8300, 401, 2000, 700}, original, baseline));
113+
CHECK_FALSE(youtubeLowControlRecovered({8300, 400, 2001, 700}, original, baseline));
114+
CHECK_FALSE(youtubeLowControlRecovered({8300, 400, 2000, 701}, original, baseline));
115+
}
116+
117+
TEST_CASE("YouTube high-low-high confirmation distinguishes all four outcomes")
118+
{
119+
CHECK(decideYoutubeConfirmation(true, false) == YoutubeConfirmationDecision::CapacityKnee);
120+
CHECK(decideYoutubeConfirmation(true, true) == YoutubeConfirmationDecision::TransientRecovered);
121+
CHECK(decideYoutubeConfirmation(false, false) == YoutubeConfirmationDecision::PathUnstable);
122+
CHECK(decideYoutubeConfirmation(false, true) == YoutubeConfirmationDecision::Inconsistent);
123+
}
12124

13125
TEST_CASE("YouTube first-rung failure retains a conservative observed cap")
14126
{

0 commit comments

Comments
 (0)