feat(profile): the free budget is pixels and fps, enforced once — with a dev-only release hatch
The paid delta is output quality, so the rule needs to be measurable rather than a word:
921,600 pixels and 30 fps, fail-closed. Models/OutputProfile.cs is that rule as pure
policy — a permitted tier passes through untouched (Pro, or the free tier's own 720p30),
a disallowed one resolves to the best tier the budget allows and carries ITS bitrate, so
a capped user is never offered an 8 Mbps budget for a 720p frame.
Applied once, in BuildEncoderOptions. That is genuinely one place: the stream and the
local recording are two output blocks of ONE ffmpeg process fed by one EncoderOptions,
so there is no second encoder to forget. (commercial.md claimed "both encoders — never
two checks"; the code is stronger than the doc, and the doc is corrected.)
Developer override, as asked: a "Developer mode — all resolutions" checkbox in App
Settings, bound to MainViewModel.DeveloperMode, which feeds HasProOutput. It is not
hidden in a release build — Controls/DeveloperPanel's Page and Compile items are removed
when Configuration != Debug, and OverlayHost adds it at runtime inside #if DEBUG, so a
shipped exe has no BAML for it at all. The setter also refuses the value when
DevBuild.IsDevelopment is false, which is now the single source for that question (the
scoreboard partial had its own duplicate const).
Two traps worth their lines: a `const bool` dev flag makes `if (!Flag) return;` an
unreachable-code warning in one configuration or the other (CS0162), so DevBuild uses
static readonly; and a Condition on an ItemGroup is invisible to a test reading
item.Attribute("Condition") — read the parent.
Tests: 14 pass across OutputProfileTests, AssemblyIdentityTests and StorePackagingTests.
The invariant test is the one that matters: iterate every shipped tier under a free
entitlement and assert none emits more than the budget, so a future 4K tier fails here
instead of shipping to everyone. App builds Debug and Release at 0 warnings.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
using ytLive.Helpers;
|
||||
|
||||
namespace ytLive.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// The entitlement surface: whether this session may emit output above the free budget.
|
||||
///
|
||||
/// <para>Deliberately a separate partial from <c>License.cs</c>: that file owns *how* a
|
||||
/// licence is validated (Polar today), this one owns *what the answer means* for output.
|
||||
/// Steam's DLC check and Polar's lease both collapse into <see cref="IsPremium"/>; nothing
|
||||
/// downstream of <see cref="HasProOutput"/> cares which one proved it.</para>
|
||||
/// </summary>
|
||||
public partial class MainViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The single input to <see cref="Models.OutputProfile.Resolve"/>.
|
||||
///
|
||||
/// <para><b>Fail closed:</b> an unresolved or unreadable licence is simply false, so a
|
||||
/// build with no entitlement emits the free profile. There is no "unknown" branch to get
|
||||
/// wrong.</para>
|
||||
///
|
||||
/// <para><b>Developer mode never ships:</b> <see cref="DeveloperMode"/> is only ever
|
||||
/// settable inside a development build (see <see cref="DevBuild"/>), so this expression
|
||||
/// reduces to <c>IsPremium</c> in a Release binary.</para>
|
||||
/// </summary>
|
||||
public bool HasProOutput => IsPremium || DeveloperMode;
|
||||
|
||||
private bool _developerMode;
|
||||
|
||||
/// <summary>
|
||||
/// Development-only override: ignore the output budget and allow every shipped tier.
|
||||
///
|
||||
/// <para>There is no UI to reach this in a Release build — the panel that hosts the
|
||||
/// checkbox is excluded from the Release compile — and the setter refuses the value
|
||||
/// unless <see cref="DevBuild.IsDevelopment"/> is true, so even a hand-edited settings
|
||||
/// file cannot unlock it.</para>
|
||||
/// </summary>
|
||||
public bool DeveloperMode
|
||||
{
|
||||
get => _developerMode;
|
||||
set
|
||||
{
|
||||
if (!DevBuild.IsDevelopment) return;
|
||||
if (!SetProperty(ref _developerMode, value)) return;
|
||||
OnPropertyChanged(nameof(HasProOutput));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,20 +78,15 @@ public partial class MainViewModel
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private const bool DevelopmentBuild = true;
|
||||
#else
|
||||
private const bool DevelopmentBuild = false;
|
||||
#endif
|
||||
|
||||
/// <summary>Can the scoreboard be configured at all? Signed-in creators yes; in a DEBUG build
|
||||
/// always, so the strip can be previewed and judged before there is an account (creator,
|
||||
/// 2026-10-30).</summary>
|
||||
public bool CanUseScoreboard => IsConnected || IsDevelopmentBuild;
|
||||
|
||||
/// <summary>True in a DEBUG build. Used only to expose development-only controls, never to
|
||||
/// change behaviour — the shipped binary differs by their absence, not by a code path.</summary>
|
||||
public bool IsDevelopmentBuild => DevelopmentBuild;
|
||||
/// change behaviour — the shipped binary differs by their absence, not by a code path.
|
||||
/// Single source: <see cref="DevBuild.IsDevelopment"/>.</summary>
|
||||
public bool IsDevelopmentBuild => DevBuild.IsDevelopment;
|
||||
|
||||
private bool _scoreboardSimulate;
|
||||
|
||||
|
||||
@@ -405,10 +405,14 @@ public partial class MainViewModel : ViewModelBase
|
||||
|
||||
// Full encoder options for the current tier, or null when no RTMP URL is
|
||||
// available — the pump then skips the encoder entirely (TASK 5 fills the seam).
|
||||
//
|
||||
// BOTH outputs (RTMP + local MP4) are encoded from this one option set, so the
|
||||
// output-profile rule is applied here, once: a capped build cannot stream 1080p and
|
||||
// record it too. See Models/OutputProfile.cs — pixels, not "720p".
|
||||
private EncoderOptions? BuildEncoderOptions()
|
||||
{
|
||||
var url = _rtmpUrlProvider();
|
||||
var quality = SelectedQuality;
|
||||
var quality = OutputProfile.Resolve(SelectedQuality, QualityOptions, HasProOutput);
|
||||
return new EncoderOptions
|
||||
{
|
||||
RtmpUrl = url ?? string.Empty,
|
||||
|
||||
Reference in New Issue
Block a user