Resolution dropdown drives the output frame: vertical 9:16 tier with semi-crop preview over the fixed 1920x1080 master

This commit is contained in:
2026-08-06 09:20:32 -07:00
parent 4bf4a13299
commit 8f0ecd3796
7 changed files with 92 additions and 9 deletions
+52 -4
View File
@@ -295,12 +295,20 @@ public class MainViewModel : ViewModelBase
// dropdown is disabled while live because YouTube stream resolution is
// immutable after stream creation (see TASKS.md compliance notes).
// The composition master frame. Every tier is an output rect + target
// resolution over this frame (see ai.md "Resolution tiers"): 16:9 tiers
// use the full frame; the vertical tier uses a centered 9:16 window and
// the preview dims the cropped strips (semi-crop).
private const double MasterFrameWidth = 1920;
private const double MasterFrameHeight = 1080;
public QualityOption[] QualityOptions { get; } =
{
new("1080p60", 60, 8.0),
new("1080p30", 30, 8.0),
new("720p60", 60, 6.0),
new("480p30", 30, 2.5),
new("1080p60", 60, 8.0, 1920, 1080),
new("1080p30", 30, 8.0, 1920, 1080),
new("720p60", 60, 6.0, 1280, 720),
new("720p30", 30, 6.0, 1280, 720),
new("Vertical 1080p60", 60, 8.0, 1080, 1920),
};
public string ResolutionHelp { get; } =
@@ -319,12 +327,51 @@ public class MainViewModel : ViewModelBase
}
}
// Output frame of the selected tier over the 1920x1080 master. The strips
// outside it are dimmed in the preview so the cropped area stays visible.
public double OutputRectX { get; private set; }
public double OutputRectY { get; private set; }
public double OutputRectWidth { get; private set; }
public double OutputRectHeight { get; private set; }
public bool IsOutputCropped { get; private set; }
public string ResolutionBadgeText { get; private set; } = string.Empty;
public ObservableCollection<Rect> DimRects { get; } = new();
private void ApplyStreamQuality(QualityOption quality)
{
var health = CurrentHealth;
health.CurrentBitrate = quality.Bitrate;
health.FPS = quality.Fps;
OnPropertyChanged(nameof(CurrentHealth));
var scale = Math.Min(MasterFrameWidth / quality.Width, MasterFrameHeight / quality.Height);
var rectW = quality.Width * scale;
var rectH = quality.Height * scale;
var rectX = (MasterFrameWidth - rectW) / 2;
var rectY = (MasterFrameHeight - rectH) / 2;
OutputRectX = rectX;
OutputRectY = rectY;
OutputRectWidth = rectW;
OutputRectHeight = rectH;
DimRects.Clear();
if (rectX > 0) DimRects.Add(new Rect(0, 0, rectX, MasterFrameHeight));
if (rectX + rectW < MasterFrameWidth)
DimRects.Add(new Rect(rectX + rectW, 0, MasterFrameWidth - rectX - rectW, MasterFrameHeight));
if (rectY > 0) DimRects.Add(new Rect(0, 0, MasterFrameWidth, rectY));
if (rectY + rectH < MasterFrameHeight)
DimRects.Add(new Rect(0, rectY + rectH, MasterFrameWidth, MasterFrameHeight - rectY - rectH));
IsOutputCropped = DimRects.Count > 0;
ResolutionBadgeText = $"{quality.Label} · {quality.Width}×{quality.Height}";
OnPropertyChanged(nameof(OutputRectX));
OnPropertyChanged(nameof(OutputRectY));
OnPropertyChanged(nameof(OutputRectWidth));
OnPropertyChanged(nameof(OutputRectHeight));
OnPropertyChanged(nameof(IsOutputCropped));
OnPropertyChanged(nameof(ResolutionBadgeText));
}
public string BugReportText
@@ -383,6 +430,7 @@ public class MainViewModel : ViewModelBase
AppLog.Write("MainViewModel ctor: services created");
_selectedQuality = QualityOptions[0];
ApplyStreamQuality(_selectedQuality);
_youtubeChat.MessageReceived += OnChatMessageReceived;