From 170866760232e94a4343c39db6bfbee296f2e982 Mon Sep 17 00:00:00 2001 From: gramps Date: Thu, 12 Nov 2026 08:01:01 -0800 Subject: [PATCH] =?UTF-8?q?feat(quality):=20retarget=20the=20ladder=20?= =?UTF-8?q?=E2=80=94=201080p60@12,=201080p30@10,=20720p@6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Models/OutputProfile.cs | 2 +- ViewModels/MainViewModel.Streaming.cs | 16 ++++++++++++---- docs/ai/ui-surface.md | 2 +- ytLive.Tests/OutputProfileTests.cs | 12 ++++++------ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/Models/OutputProfile.cs b/Models/OutputProfile.cs index 459de35..97970ca 100644 --- a/Models/OutputProfile.cs +++ b/Models/OutputProfile.cs @@ -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); } } diff --git a/ViewModels/MainViewModel.Streaming.cs b/ViewModels/MainViewModel.Streaming.cs index 0fb9c2d..38ab1b8 100644 --- a/ViewModels/MainViewModel.Streaming.cs +++ b/ViewModels/MainViewModel.Streaming.cs @@ -446,14 +446,22 @@ public partial class MainViewModel : ViewModelBase private const double MasterFrameWidth = 1920; private const double MasterFrameHeight = 1080; + /// + /// The shipped ladder. The rate on each tier is the creator-facing target the encoder is asked to + /// hold (CBR — see FfmpegArgs.AddRateControl), 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. + /// 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; } = diff --git a/docs/ai/ui-surface.md b/docs/ai/ui-surface.md index bbd3f14..f5bc984 100644 --- a/docs/ai/ui-surface.md +++ b/docs/ai/ui-surface.md @@ -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` diff --git a/ytLive.Tests/OutputProfileTests.cs b/ytLive.Tests/OutputProfileTests.cs index 966cdfa..0253c6e 100644 --- a/ytLive.Tests/OutputProfileTests.cs +++ b/ytLive.Tests/OutputProfileTests.cs @@ -21,12 +21,12 @@ public class OutputProfileTests /// step deliberately: this is the table the budget has to cover. 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); } ///