feat(quality): retarget the ladder — 1080p60@12, 1080p30@10, 720p@6
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.
This commit is contained in:
@@ -64,6 +64,6 @@ public static class OutputProfile
|
||||
|
||||
// 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, 8.0, 1920, 1080);
|
||||
return candidate ?? new QualityOption(FreeFallbackName, FreeMaxFps, 10.0, 1920, 1080);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,14 +446,22 @@ public partial class MainViewModel : ViewModelBase
|
||||
private const double MasterFrameWidth = 1920;
|
||||
private const double MasterFrameHeight = 1080;
|
||||
|
||||
/// <summary>
|
||||
/// The shipped ladder. The rate on each tier is the creator-facing target the encoder is asked to
|
||||
/// hold (CBR — see <c>FfmpegArgs.AddRateControl</c>), between YouTube's H.264 recommended value
|
||||
/// (17 Mbps at 1080p60, 14 at 1080p30, 8 at 720p) and the practitioner sweet spot most guides land
|
||||
/// on for YouTube. The old flat 8 Mbps was inherited from TASK 4 as a "mainstream ceiling" — an
|
||||
/// unmeasured round number that produced a 5.1 Mbps average. Vertical mirrors its landscape pair:
|
||||
/// 1080×1920 is the same pixel count as 1920×1080.
|
||||
/// </summary>
|
||||
public QualityOption[] QualityOptions { get; } =
|
||||
{
|
||||
new("1080p60", 60, 8.0, 1920, 1080),
|
||||
new("1080p30", 30, 8.0, 1920, 1080),
|
||||
new("1080p60", 60, 12.0, 1920, 1080),
|
||||
new("1080p30", 30, 10.0, 1920, 1080),
|
||||
new("720p60", 60, 6.0, 1280, 720),
|
||||
new("720p30", 30, 6.0, 1280, 720),
|
||||
new("Vertical 1080p30", 30, 8.0, 1080, 1920),
|
||||
new("Vertical 1080p60", 60, 8.0, 1080, 1920),
|
||||
new("Vertical 1080p30", 30, 10.0, 1080, 1920),
|
||||
new("Vertical 1080p60", 60, 12.0, 1080, 1920),
|
||||
};
|
||||
|
||||
public string ResolutionHelp { get; } =
|
||||
|
||||
@@ -144,7 +144,7 @@ Placement math itself is covered by `WindowPlacementTests` (pure function, no HW
|
||||
geometry fields are 44px: the measured width of `-1920` in the app's font at 11px (via WPF's
|
||||
`FormattedText` — **measure, don't guess**) plus the field's padding and border. Guard:
|
||||
`ytLive.Tests/GeometryFieldWidthTests.cs`
|
||||
- Resolution tiers: 1080p60@8 (default) → 1080p30@8 → 720p60@6 → 720p30@6 → **Vertical 1080p30@8 and 1080p60@8 (9:16, 1080×1920)**. The composition master frame is **always 1920×1080** — a tier is an output rect + target resolution over that master, so source geometry is never rewritten (no rounding drift). 16:9 tiers use the full frame; vertical tiers use a centered **607×1080** window and the preview dims the cropped side strips at 55% black with an accent outline (semi-crop — the cut area stays visible). A resolution badge in the preview corner shows the active tier. The resolution picker and live readouts (bitrate/FPS/dropped/duration) are in the top-left cell above the layers panel; the bottom bar is an empty height-preserving spacer. A **tooltip** explains finding upload bandwidth — an in-app speed test was deliberately dropped (unreliable). The future encoder crops the master to the rect and scales to the tier's Width×Height
|
||||
- Resolution tiers: 1080p60@12 (default) → 1080p30@10 → 720p60@6 → 720p30@6 → **Vertical 1080p30@10 and 1080p60@12 (9:16, 1080×1920)** — the `@` is the CBR target the encoder is asked to hold, between YouTube's H.264 recommendation and the practitioner sweet spot (its 14/17/8 recommendations are the max, the tier minimums 5/6/3 the step-down floor). Vertical mirrors its landscape pair: same pixel count, same rate. The old flat 8 Mbps was an inherited round number, not a measurement. The composition master frame is **always 1920×1080** — a tier is an output rect + target resolution over that master, so source geometry is never rewritten (no rounding drift). 16:9 tiers use the full frame; vertical tiers use a centered **607×1080** window and the preview dims the cropped side strips at 55% black with an accent outline (semi-crop — the cut area stays visible). A resolution badge in the preview corner shows the active tier. The resolution picker and live readouts (bitrate/FPS/dropped/duration) are in the top-left cell above the layers panel; the bottom bar is an empty height-preserving spacer. A **tooltip** explains finding upload bandwidth — an in-app speed test was deliberately dropped (unreliable). The future encoder crops the master to the rect and scales to the tier's Width×Height
|
||||
- **Locked output tiers stay visible and clickable.** They are dimmed for non-owners. Choosing one keeps the free tier selected and opens About in-app; only the explicit llamacasty.com link opens a browser.
|
||||
- Crash diagnosis: `AppLog` writes startup checkpoints to `%APPDATA%\ytLlive\startup.log`; `App.xaml.cs` logs `DispatcherUnhandledException`/`AppDomain.UnhandledException`. When WPF won't run from WSL, this log is how you find the failure (it caught the `MenuItemRole.Separator` XAML crash and the ComboBox SelectionBoxItem bug). **The log is SIZE-CAPPED (TASK 55, 2026-10-22):** at 5 MB `startup.log` rolls to `startup.1.log` (one rolled copy kept) — the policy lives in `Helpers/LogRotation.cs` as a pure function over a path, so `AppLog`'s 156-call-site surface never changes and the rule is testable on its own. Guard: `ytLive.Tests/AppLogRotationTests.cs`
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ public class OutputProfileTests
|
||||
/// step deliberately: this is the table the budget has to cover.</summary>
|
||||
private static readonly QualityOption[] Shipped =
|
||||
{
|
||||
new("1080p60", 60, 8.0, 1920, 1080),
|
||||
new("1080p30", 30, 8.0, 1920, 1080),
|
||||
new("1080p60", 60, 12.0, 1920, 1080),
|
||||
new("1080p30", 30, 10.0, 1920, 1080),
|
||||
new("720p60", 60, 6.0, 1280, 720),
|
||||
new("720p30", 30, 6.0, 1280, 720),
|
||||
new("Vertical 1080p30", 30, 8.0, 1080, 1920),
|
||||
new("Vertical 1080p60", 60, 8.0, 1080, 1920),
|
||||
new("Vertical 1080p30", 30, 10.0, 1080, 1920),
|
||||
new("Vertical 1080p60", 60, 12.0, 1080, 1920),
|
||||
};
|
||||
|
||||
private static QualityOption Tier(string name) => Shipped.Single(t => t.Display == name);
|
||||
@@ -40,7 +40,7 @@ public class OutputProfileTests
|
||||
Assert.Equal(1920, resolved.Width);
|
||||
Assert.Equal(1080, resolved.Height);
|
||||
Assert.Equal(30, resolved.Fps);
|
||||
Assert.Equal(8.0, resolved.Bitrate);
|
||||
Assert.Equal(10.0, resolved.Bitrate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -77,7 +77,7 @@ public class OutputProfileTests
|
||||
Assert.Equal(1920, resolved.Width);
|
||||
Assert.Equal(1080, resolved.Height);
|
||||
Assert.Equal(30, resolved.Fps);
|
||||
Assert.Equal(8.0, resolved.Bitrate);
|
||||
Assert.Equal(10.0, resolved.Bitrate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user