using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Threading; using ytLive.Helpers; using ytLive.Models; using ytLive.Services; using ytLive.Services.Audio; using ytLive.Services.Compositor; using ytLive.Services.Encoder; namespace ytLive.ViewModels; public partial class MainViewModel : ViewModelBase { private readonly YouTubeAuthService _youtubeAuth; private readonly YouTubeStreamService _youtubeStream; private readonly YouTubeChatService _youtubeChat; private SceneElement? _selectedElement; private LayoutStore _layoutStore; private string? _activeLayoutPath; private DispatcherTimer? _saveDebounce; private bool _isLoading; // Toasts (TASK 24): non-blocking user-facing messages. Parameterless ctor // news up the real service; the VM is built from XAML so no DI seam exists. private readonly INotificationService _notifications = new NotificationService(); // Live frame pipeline (TASK 4 ship step 5): composites the active scene at the // tier's FPS and paces frames into the encoder. The RTMP URL seam stays null // until the live-stream create flow (TASK 5) supplies the reusable stream URL. private readonly Func _rtmpUrlProvider; // Scene graph (TASK 31): owns the scene collection + element inventory and the // baked-crust compositor cache. The ViewModel exposes Scenes/StagedScene/LiveScene // on top of it; the scene collection itself lives here. // Field-initialized, NOT ctor-assigned: the ctor touches Scenes (CollectionChanged // wiring) before its later statements run — first-ctor-line NRE if the graph isn't // born yet (startup crash regression from TASK 31, 2026-09-01; headless RealApp // tests never ran it, so it waited for the first native launch). private SceneGraph _sceneGraph = new(); private readonly TransitionService _transition = new(); private WebView2Manager? _webView2Manager; // Branding flash (monetization): a full-frame "made with LlamaCasty!" shown // for ~1s every 300s on the live output, ~25% opacity. Paid unlock sets // BrandFlashEnabled = false. See ai.md "Monetization". // The free-tier branding credit. Composed into the OUTPUT by FramePump, not just // previewed — see BrandFlashPresenter. Gated on IsPremium (Polar), never a toggle. private readonly BrandFlashPresenter _brandFlash; public SceneElement? SelectedElement { get => _selectedElement; set { if (_selectedElement != null) _selectedElement.PropertyChanged -= OnSelectedElementPropertyChanged; if (SetProperty(ref _selectedElement, value)) { OnPropertyChanged(nameof(IsWebcamSelected)); OnPropertyChanged(nameof(IsElementsPanelEmpty)); if (_selectedElement != null) _selectedElement.PropertyChanged += OnSelectedElementPropertyChanged; } } } private void OnSelectedElementPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName == nameof(SceneElement.UseDefaultBackground)) { if (StagedScene != null) _sceneGraph.InvalidateBake(StagedScene); UpdateBackgroundImage(); RefreshSnapshotsAsync(); ScheduleSave(); } } private static readonly SolidColorBrush BarOnBrush = CreateBrush("#2ecc71"); private static readonly SolidColorBrush BarOffBrush = CreateBrush("#e94560"); private static SolidColorBrush CreateBrush(string hex) => (SolidColorBrush)new BrushConverter().ConvertFromString(hex)!; /// True when the Elements panel should show the "No configurable elements" hint. public bool IsElementsPanelEmpty => SelectedElement is { } el && !el.IsDraggable && !el.IsWebcam && el is not Source { Type: SourceType.Countdown or SourceType.WebSource or SourceType.ChatBox }; public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null && BackgroundImage == null; // Paid unlock flips this off (see ai.md "Monetization"). When disabled the // presenter stops emitting, so any active credit dies on the next tick — and a // credit already in flight is cut, not run out. Derived, never assignable: the // whole point is that nobody can turn the advertising on a paying stream. public bool BrandFlashEnabled => !IsPremium; // Window geometry pass-through (code-behind owns the WPF specifics). public (double Left, double Top, double Width, double Height, string State)? RestoreWindowState() => _layoutStore.LoadWindowState(); public void PersistWindowState(double left, double top, double width, double height, string state) => _layoutStore.SaveWindowState(left, top, width, height, state); // Commands public ICommand AddSceneCommand { get; } public ICommand AddSourceCommand { get; } public ICommand AddImageCommand { get; } public ICommand RemoveSourceCommand { get; } public ICommand EditElementCommand { get; } public ICommand ToggleElementVisibilityCommand { get; } public ICommand BrowseAlertVideoCommand { get; } public ICommand ResetAlertVideoCommand { get; } public ICommand ActivateLicenseCommand { get; } public ICommand OpenLicenseEntryCommand { get; } public ICommand OpenHotkeyConfigCommand { get; } public ICommand ChangeAccountCommand { get; } public ICommand LogoutCommand { get; } public MainViewModel() { AppLog.Write("MainViewModel ctor begin"); _youtubeAuth = new YouTubeAuthService(OAuthCredentials.ClientId, OAuthCredentials.ClientSecret, sessionChanged: ch => TokenStore.Save(ch)); _youtubeStream = new YouTubeStreamService(_youtubeAuth); _youtubeChat = new YouTubeChatService(_youtubeAuth); AppLog.Write("MainViewModel ctor: services created"); _selectedQuality = QualityOptions[0]; ApplyStreamQuality(_selectedQuality); _youtubeChat.MessageReceived += OnChatMessageReceived; _liveTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _liveTimer.Tick += OnLiveTimerTick; _volumeFlashTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) }; _volumeFlashTimer.Tick += (_, _) => EndVolumeFlash(); _gameVolumeFlashTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) }; _gameVolumeFlashTimer.Tick += (_, _) => EndGameVolumeFlash(); _saveDebounce = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1500) }; _saveDebounce.Tick += (_, _) => SaveLayoutNow(); _brandFlash = new BrandFlashPresenter(); _brandFlash.OnFrame = PublishBrandFlashPreview; // Start the cadence ONCE for the life of the app, here. CREATOR RULING // 2026-09-26: the credit must show in every scene and in recordings, so it // cannot be tied to IsLive (that gate meant a recording made without going // live carried no credit). The pump reads BrandFlashFrame() whenever it runs // — go-live and local recording are the same FramePump, so one start covers // output, recording and the preview pane alike. Enabled is seeded from the // current licence state; ValidateLicenseAtStartupAsync re-pushes it later. _brandFlash.Enabled = !IsPremium; _brandFlash.Start(); _healthPollTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; _healthPollTimer.Tick += OnHealthPollTick; Scenes.CollectionChanged += OnScenesChanged; AddSceneCommand = new RelayCommand(name => AddScene(name as string ?? string.Empty)); AddSourceCommand = new RelayCommand(parameter => AddSource(parameter)); AddWebcamCommand = new RelayCommand(_ => _ = AddWebcamToStagedSceneAsync()); AddImageCommand = new RelayCommand(_ => AddImage()); RemoveSourceCommand = new RelayCommand(element => RemoveElement(element as SceneElement)); EditElementCommand = new RelayCommand(element => BeginEditElement(element as SceneElement)); ToggleElementVisibilityCommand = new RelayCommand(element => ToggleElementVisibility(element as SceneElement)); BrowseAlertVideoCommand = new RelayCommand(element => BrowseAlertVideo(element as Source), element => element is Source { Type: SourceType.AlertBox }); ResetAlertVideoCommand = new RelayCommand(element => ResetAlertVideo(element as Source), element => element is Source { Type: SourceType.AlertBox }); ChangeWebcamCommand = new RelayCommand(_ => _ = ChangeWebcamAsync(), _ => CanChangeWebcam); ShowWebcamCommand = new RelayCommand(_ => ShowWebcamInStagedScene(), _ => CanShowWebcamInStagedScene); OpenSocialDialogCommand = new RelayCommand(_ => OpenSocialDialog()); ChangeCaptureCommand = new RelayCommand(_ => _ = ChangeBackgroundCaptureAsync()); RefreshCaptureCommand = new RelayCommand(_ => RefreshBackgroundAutoCapture()); SetBackgroundDisplayCommand = new RelayCommand(display => SetBackgroundCapture(display as DisplayInfo)); _windowEnumerator = WindowEnumeratorOverride ?? new Win32WindowEnumerator(); RefreshWindowsCommand = new RelayCommand(_ => RefreshOpenWindows()); CaptureWindowCommand = new RelayCommand(window => { if (window is not WindowInfo info) return; _ = RedesignateBackgroundAsync(info.CaptureKey); }); RefreshOpenWindows(); ToggleShowDesktopCommand = new RelayCommand(_ => ToggleBackgroundShowDesktop()); BrowseBackgroundCommand = new RelayCommand(_ => BrowseBackground()); ToggleMicMuteCommand = new RelayCommand(_ => ToggleMicMute()); ToggleGameMuteCommand = new RelayCommand(_ => ToggleGameMute()); OpenMicPickerCommand = new RelayCommand(_ => PickMicrophone()); OpenSettingsCommand = new RelayCommand(_ => ShowOverlay(nameof(IsSettingsOpen), "App Settings")); OpenBugCommand = new RelayCommand(_ => ShowOverlay(nameof(IsBugOpen), "Report Bug")); OpenFeatureCommand = new RelayCommand(_ => ShowOverlay(nameof(IsFeatureOpen), "Feature Request")); OpenAboutCommand = new RelayCommand(_ => ShowOverlay(nameof(IsAboutOpen), "About")); CloseOverlayCommand = new RelayCommand(_ => ShowOverlay()); SubmitBugCommand = new RelayCommand(_ => SubmitBug()); SubmitFeatureCommand = new RelayCommand(_ => SubmitFeature()); OpenChannelCommand = new RelayCommand(_ => OpenUrl(ChannelUrl)); OpenMastodonCommand = new RelayCommand(_ => OpenUrl(MastodonUrl)); OpenCoffeeCommand = new RelayCommand(_ => OpenUrl(CoffeeUrl)); OpenPremiumCommand = new RelayCommand(_ => OpenUrl(PremiumUrl), _ => IsPremiumAvailable); OpenLicensesCommand = new RelayCommand(_ => ShowLicenses()); StartStreamCommand = new RelayCommand(_ => StartSession()); StartTestCommand = new RelayCommand(_ => BeginTestStream(), _ => CanStartTest); SignInCommand = new RelayCommand(_ => _ = SignInAsync(), _ => !IsConnected); EndStreamCommand = new RelayCommand(_ => StopStream(), _ => IsLive || IsRecording); ChooseRecordFolderCommand = new RelayCommand(_ => ChooseRecordFolder()); ResetRecordFolderCommand = new RelayCommand(_ => ResetRecordFolder()); RetryWebcamLockCommand = new RelayCommand(_ => _ = ValidateWebcamResourceStartupAsync()); ActivateLicenseCommand = new RelayCommand(_ => _ = ActivateLicenseAsync(), _ => !IsLicenseValidating); OpenLicenseEntryCommand = new RelayCommand(_ => IsLicenseEntryOpen = !IsLicenseEntryOpen); OpenHotkeyConfigCommand = new RelayCommand(_ => OpenHotkeyConfig()); ChangeAccountCommand = new RelayCommand(_ => _ = ChangeAccountAsync()); LogoutCommand = new RelayCommand(_ => _ = SignOutYouTubeAsync()); _layoutStore = new LayoutStore(LayoutPathOverride ?? DefaultLayoutPath); _activeLayoutPath = _layoutStore.ActivePath; _recordFolder = _layoutStore.LoadRecordFolder(); // Load saved hotkey bindings (filled with defaults for any unbound actions). _hotkeyBindings = _layoutStore.LoadHotkeyBindings(); BroadcastForm = new Services.LiveBroadcastFormViewModel( () => _layoutStore, () => _youtubeStream, () => CurrentBroadcastId, () => IsLive); InitYpp(); // Restore the last picked mic so the mixer's first capture attaches to // the same, already-vetted device (green dot) or reports it missing // (yellow) instead of silently falling back to the default. MicSourceName = _layoutStore.LoadMicSourceName(); AudioSyncOffsetMs = _layoutStore.LoadAudioSyncOffsetMs(); _cameraEnumerator = CameraEnumeratorOverride ?? new MediaCaptureCameraEnumerator(); _cameraManager = new CameraManager( _cameraEnumerator, CameraFrameSourceFactoryOverride ?? (id => new MediaCaptureFrameSource(id)), System.Windows.Application.Current?.Dispatcher); _cameraManager.PreviewBitmapChanged += OnCameraPreviewBitmapChanged; _cameraManager.CameraFailed += OnCameraFailed; _microphoneEnumerator = new WinRtMicrophoneEnumerator(); // The mixer consumes these Func seams live each mix tick, so the volume // sliders and mute buttons are stream-honest (they also update the // meters' display). Before this wiring the gains defaulted to unity and // the controls were decorative. _gainProvider = new AudioGainProvider( () => MicMuted, () => MicVolume, () => GameMuted, () => GameAudioVolume); _audioMixer = new AudioMixer( new WasapiMicAudioSource(() => MicSourceName), new WasapiLoopbackAudioSource(), message => AppLog.Write(message), micGain: _gainProvider.MicGain, loopbackGain: _gainProvider.LoopbackGain, syncOffsetMs: () => AudioSyncOffsetMs); _audioMixer.MicLevelChanged += OnMicLevelChanged; _audioMixer.LoopbackLevelChanged += OnLoopbackLevelChanged; _audioMixer.MicConnected += OnMicConnected; _audioMixer.MicFailed += OnMicFailed; _musicPlayer.PlaybackEnded += OnTraxPlaybackEnded; _ = StartMicCaptureAsync(); _fullScreenDetector = new Win32FullScreenDetector(); _fullScreenDetector.FullscreenMonitorChanged += OnFullscreenMonitorChanged; foreach (var display in _fullScreenDetector.GetDisplays()) Displays.Add(display); _screenCaptureFactory = new ScreenCaptureSourceFactory( () => new WindowInteropHelper(System.Windows.Application.Current?.MainWindow).Handle); _screenCaptureManager = new ScreenCaptureManager( _screenCaptureFactory.Resolve, System.Windows.Application.Current?.Dispatcher); _screenCaptureManager.PreviewBitmapChanged += OnScreenPreviewBitmapChanged; _screenCaptureManager.CaptureFailed += OnScreenCaptureFailed; _mediaManager = new MediaVideoSourceManager( path => new MediaVideoSource( path, MediaDecodeWidth, MediaDecodeHeight, new FfmpegLocator(), () => new FfmpegDecodeProcess(), frameRateProbe: new FfmpegFrameRateProbe( new FfmpegLocator(), () => new FfmpegDecodeProcess())), System.Windows.Application.Current?.Dispatcher); _mediaManager.PreviewBitmapChanged += OnMediaPreviewBitmapChanged; _mediaManager.MediaFailed += OnMediaFailed; _chatLayer = new ChatOverlayLayer(new ChatBoxRenderer()); _alertLayer = new AlertOverlayLayer( new AlertRenderer(), clipDecoderFactory: AlertClipDecoderFor, clipPathResolver: ResolveAlertClipPath, audioSink: _audioMixer.EnqueueAlertAudio, tickerPreviewSink: PublishAlertTickerPreview); _rtmpUrlProvider = () => _reusableStreamUrl; // TASK 5: the reusable stream's ingest URL _framePump = new FramePump( sceneProvider: () => StagedScene, frameResolver: ResolveOutputFrame, compositorOptions: BuildCompositorOptions, encoderOptions: BuildEncoderOptions, encoderFactory: () => new FfmpegEncoder(new FfmpegLocator()), log: message => AppLog.Write(message), socialBar: () => (_socialBarFrame, _socials?.BarPosition ?? SocialBarPosition.Bottom), alertTicker: () => _alertLayer.AlertTickerFrame, brandFlash: BrandFlashFrame, transition: _transition, sceneGraph: _sceneGraph); _framePump.Failed += OnFramePumpFailed; _framePump.HealthUpdated += OnFramePumpHealthUpdated; LoadLayout(); StampDefaultAlertVideo(); // Resource validation runs as soon as the layout is up: enumerate the // OS once, and if exactly one webcam exists, lock it for the session // (the saved identity's scene configs may already hold it — refcounted). // Failures surface as a persistent red alert in the Layers panel. WebcamStartupValidationTask = ValidateWebcamResourceStartupAsync(); // The cached reusable stream gives the pump its RTMP URL the moment go-live // starts (the pump reads the URL once at startup — no waiting on YouTube). _reusableStream = _layoutStore.LoadReusableStream(); _reusableStreamUrl = _reusableStream?.RtmpUrl; _ = LoadSavedSessionAsync(); _ = ValidateLicenseAtStartupAsync(); AppLog.Write("MainViewModel ctor end"); } // Per-instance in DEBUG so two dev instances cannot clobber each other's layout // (see InstanceProfile). Release always resolves the one real profile. private static string DefaultLayoutPath => Path.Combine( InstanceProfile.DataRoot, "ytLlive.db"); // Test seam: a real MainWindow test must never read or write the user's // real layout DB (it would persist test sources over real ones). Tests set // this to a temp path before constructing MainWindow and reset it after. internal static string? LayoutPathOverride { get; set; } // Test seams (same pattern): startup resource validation must never touch a // real camera while under test. Tests inject a fake enumerator / frame-source // factory before constructing MainWindow and reset them after. internal static ICameraEnumerator? CameraEnumeratorOverride { get; set; } internal static Func? CameraFrameSourceFactoryOverride { get; set; } private void LoadLayout() { _isLoading = true; try { AppLog.Write("LoadLayout begin"); var scenes = _layoutStore.Load(); Scenes.Clear(); foreach (var scene in scenes) Scenes.Add(scene); if (Scenes.Count == 0) { foreach (var name in SceneCatalog.All) AddScene(name); } HealBackgrounds(); RefreshHotkeyLabels(); foreach (var scene in Scenes) foreach (var config in scene.Elements.OfType()) { ClampWebcamToBounds(config, scene.Name); HealLegacySquareRect(config); } // One-time heal: ChatBox layers created before the "YouTube Chat" // default still carry the old "Chat" name — creator renames untouched. foreach (var scene in Scenes) foreach (var source in scene.Elements.OfType()) if (source.Type == SourceType.ChatBox && string.Equals(source.Name, "Chat", StringComparison.OrdinalIgnoreCase)) source.Name = "YouTube Chat"; AppLog.Write($"LoadLayout: {Scenes.Count} scenes loaded"); } finally { _isLoading = false; } StagedScene = Scenes.FirstOrDefault(); UpdateActiveBackground(); UpdateActiveChatBoxPreviews(); UpdateActiveAlertBoxPreviews(); StartMockChatPreview(); UpdateBackgroundImage(); ReacquireWebcam(); ReacquireScreenCaptures(); RegisterLoadedWebSources(); _socials = _layoutStore.Socials; RenderSocialBarFrame(); HealFediverseSoftwareInBackground(); OnPropertyChanged(nameof(HasSocials)); OnPropertyChanged(nameof(SocialBarVisible)); OnPropertyChanged(nameof(SocialBarDotBrush)); _music = _layoutStore.Music; LoadTrax(play: false); var (transType, transDur) = _layoutStore.LoadTransitionSettings(); if (Enum.TryParse(transType, true, out var parsed)) SelectedTransitionType = parsed; TransitionDurationMs = transDur; BroadcastForm.Load(); ScheduleSave(); AppLog.Write("LoadLayout end"); } public void Shutdown() { _saveDebounce?.Stop(); SaveLayoutNow(); _fullScreenDetector.StopWatching(); _audioMixer.Dispose(); _framePump.Dispose(); _cameraManager.Dispose(); _screenCaptureManager.Dispose(); _mediaManager.Dispose(); _webView2Manager?.Dispose(); _layoutStore.Dispose(); _musicPlayer.Dispose(); _youtubeChat.Dispose(); _alertLayer.Dispose(); } public void SaveLayoutNow() { _saveDebounce?.Stop(); try { _layoutStore.Save(Scenes, _webcam, _socials, _music); _layoutStore.SaveTransitionSettings( SelectedTransitionType.ToString(), TransitionDurationMs); _layoutStore.SaveAudioSyncOffsetMs(AudioSyncOffsetMs); } catch (Exception ex) { Debug.WriteLine($"Layout save failed: {ex.Message}"); } } private void ScheduleSave() { if (_isLoading || _saveDebounce == null) return; _saveDebounce.Stop(); _saveDebounce.Start(); } private void BeginEditElement(SceneElement? element) { if (element == null) return; element.IsEditing = true; } private void ToggleElementVisibility(SceneElement? element) { if (element == null) return; element.IsVisible = !element.IsVisible; if (element is Source { IsBackground: true }) OnPropertyChanged(nameof(BackgroundVisible)); } // Scene-element → latest frame, for the live compositor. The map mirrors the // preview: webcam by DeviceId, live captures (the background) by CaptureKey, // images/background by AssetId. A null frame leaves the element transparent. private VideoFrame? ResolveOutputFrame(SceneElement element) { var frame = ResolveOutputFrameCore(element); // Output-side eyes (2026-09-01): preview showed the webcam but the recording // didn't — a null resolver frame silently DROPS the layer. Log once per 5s // per kind so the repro run tells acquire-mismatch from compose-skip. if (frame == null && element is WebcamSceneConfig webcam) { var now = DateTime.UtcNow; if (now - _lastWebcamNullLog >= TimeSpan.FromSeconds(5)) { _lastWebcamNullLog = now; AppLog.Write($"Output resolver: webcam '{webcam.WebcamId}' frame is null — layer skipped in composite"); } } // Alerts (TASK 47, 2026-09-26): a healthy decode logged frames=240 while the // box stayed blank, so name the two possibilities per tick — did the resolver // ever get asked for the alert box, and did it have a frame to hand back? if (element is Source { Type: SourceType.AlertBox } alert) { var now = DateTime.UtcNow; if (now - _lastAlertResolveLog >= TimeSpan.FromSeconds(1)) { _lastAlertResolveLog = now; AppLog.Write(frame == null ? $"Output resolver: alert box '{alert.Id}' frame is null (visible={alert.IsVisible} playing={_alertLayer.IsPlaying}) — skipped in composite" : $"Output resolver: alert box '{alert.Id}' frame {frame!.Width}x{frame!.Height} (visible={alert.IsVisible} playing={_alertLayer.IsPlaying})"); } } return frame; } private DateTime _lastAlertResolveLog = DateTime.MinValue; private DateTime _lastWebcamNullLog = DateTime.MinValue; /// Maps a config's identity GUID to the CameraManager session key (the /// DEVICE id). internal = test seam. Take-2 fix: looking up the identity GUID /// against a device-keyed dictionary returned null forever. internal string DeviceKeyForWebcam(string webcamId) => _webcam is { } w && w.Id == webcamId && !string.IsNullOrWhiteSpace(w.DeviceId) ? w.DeviceId : webcamId; private VideoFrame? ResolveOutputFrameCore(SceneElement element) { return element switch { // CameraManager sessions are keyed by DEVICE id (AcquireAsync(_webcam.DeviceId)); // configs carry the identity GUID (WebcamSceneConfig.WebcamId = _webcam.Id). // Looking up by the GUID returned null forever — the webcam was structurally // invisible on the output while the preview (bitmap path) looked fine // (caught by the resolver logging, 2026-09-01 take two). WebcamSceneConfig webcam => _cameraManager.GetLatestFrame(DeviceKeyForWebcam(webcam.WebcamId)), Source { IsLiveCapture: true, CaptureKey: not null } live => _screenCaptureManager.GetLatestFrame(live.CaptureKey), Source { Type: SourceType.ChatBox } chatBox => RenderChatBox(chatBox), Source { Type: SourceType.AlertBox } alertBox => RenderAlertBox(alertBox), Source { Type: SourceType.WebSource } web => _webView2Manager?.GetLatestFrame(web.Id), Source { IsMediaSource: true, MediaPath: not null } media => _mediaManager.GetLatestFrame(media.MediaPath), Source { AssetId: not null } image => StaticPixelCache.Get(image.AssetId), _ => null, }; } // The tier's crop rect over the 1920x1080 master, integer-aligned (the VM's // OutputRect* are doubles — the vertical 607.5 half-pixel crop rounds to 608). private CompositorOptions BuildCompositorOptions() { var quality = SelectedQuality; return new CompositorOptions { SourceRectX = (int)Math.Round(OutputRectX), SourceRectY = (int)Math.Round(OutputRectY), SourceRectWidth = (int)Math.Round(OutputRectWidth), SourceRectHeight = (int)Math.Round(OutputRectHeight), OutputWidth = quality.Width, OutputHeight = quality.Height, }; } // The credit's cadence, timing, placement and licence gate all live in // BrandFlashPresenter; MainViewModel only routes it to the frame pump and the // preview (see MainViewModel.BrandFlash.cs). }