Every 1080 tier carried 8 Mbps, inherited from TASK 4's "mainstream ceiling,
upload headroom stays comfortable". It was never derived from anything: the same
number served 30 fps and 60 fps, landscape and vertical, which no per-tier
derivation would produce. With CBR now actually holding the rate (700c7a8) that
number becomes the picture quality, so it has to be chosen rather than inherited.
New defaults sit between YouTube's H.264 recommendation (17 at 1080p60, 14 at
1080p30, 8 at 720p — support.google.com/youtube/answer/2853702) and the rate
practitioner OBS guides converge on for YouTube (9–12 Mbps at 1080p60). Vertical
mirrors its landscape pair: 1080x1920 is the same pixel count as 1920x1080.
The free-profile fallback in OutputProfile.Resolve moves with it (8.0 -> 10.0) so
a build with no shipped tiers still emits the 1080p30 rate rather than a stale
one.
668 tests (1 audio-pipe flake under full-suite load, passes alone), 0 warnings.
70 lines
3.5 KiB
C#
70 lines
3.5 KiB
C#
namespace ytLive.Models;
|
|
|
|
/// <summary>
|
|
/// The one entitlement rule, as pure policy: what a build is allowed to emit.
|
|
///
|
|
/// <para><b>Frame rate is the paid boundary.</b> Every supported resolution at 30 fps is free;
|
|
/// 60 fps requires paid ownership, regardless of frame dimensions.</para>
|
|
///
|
|
/// <para><b>Fail closed.</b> Callers pass <c>pro: false</c> when entitlement is unknown,
|
|
/// unreadable or unresolved; the free profile is the default state, never the fallback
|
|
/// after an error.</para>
|
|
///
|
|
/// <para><b>This is not a feature gate.</b> Every feature, scene, overlay and the recording
|
|
/// itself stay available; only the emitted frame gets smaller. Never call it for anything
|
|
/// else — a cracked build buys almost nothing, which is the entire anti-piracy posture.</para>
|
|
/// </summary>
|
|
public static class OutputProfile
|
|
{
|
|
/// <summary>Free ceiling: 30 frames per second.</summary>
|
|
public const int FreeMaxFps = 30;
|
|
|
|
/// <summary>
|
|
/// Pre-release switch. <c>false</c> means the frame-rate boundary is not enforced yet, so every
|
|
/// shipped tier is selectable by any build — the app is still being tested at 60 fps in
|
|
/// <b>Release</b>, where <c>DevBuild.IsDevelopment</c> is false and the developer override cannot
|
|
/// help. Flip to <c>true</c> when the paid edition ships; the rule itself already exists and its
|
|
/// tests do not change.
|
|
///
|
|
/// <para><b>Why <c>static readonly</c>:</b> a <c>const</c> folds the consuming <c>||</c> at compile
|
|
/// time and leaves an unreachable operand, which is a warning. The JIT folds a readonly field
|
|
/// just as well, and the build stays at zero warnings.</para>
|
|
/// </summary>
|
|
public static readonly bool EntitlementEnforced = false;
|
|
|
|
/// <summary>The default free tier and fallback when no shipped tier is available.</summary>
|
|
public const string FreeFallbackName = "1080p30";
|
|
|
|
/// <summary>Is this tier within the free budget? Pro may use anything shipped.</summary>
|
|
public static bool IsAllowed(QualityOption tier, bool pro)
|
|
=> pro
|
|
|| tier.Fps <= FreeMaxFps;
|
|
|
|
/// <summary>
|
|
/// The tier a session will actually encode. A permitted selection passes through
|
|
/// untouched (so Pro and the free tier's own 720p30 keep their exact bitrate); a
|
|
/// disallowed one resolves to the best tier the budget allows — most pixels first, then
|
|
/// most frames, then the cheapest bitrate — because silently encoding something the user
|
|
/// did not pick is only acceptable if it is the closest legal thing to it.
|
|
/// </summary>
|
|
/// <param name="requested">What the dropdown says.</param>
|
|
/// <param name="shipped">Every tier the build offers.</param>
|
|
/// <param name="pro">Entitlement, already resolved. Unknown must be passed as false.</param>
|
|
public static QualityOption Resolve(
|
|
QualityOption requested, IEnumerable<QualityOption> shipped, bool pro)
|
|
{
|
|
if (IsAllowed(requested, pro)) return requested;
|
|
|
|
var candidate = shipped
|
|
.Where(tier => IsAllowed(tier, pro))
|
|
.OrderByDescending(tier => tier.Width * tier.Height)
|
|
.ThenByDescending(tier => tier.Fps)
|
|
.ThenBy(tier => tier.Bitrate)
|
|
.FirstOrDefault();
|
|
|
|
// No shipped tier fits the budget: build the floor explicitly rather than trusting
|
|
// the table. A build whose tiers were all redefined must still emit a legal frame.
|
|
return candidate ?? new QualityOption(FreeFallbackName, FreeMaxFps, 10.0, 1920, 1080);
|
|
}
|
|
}
|