Files
ytLlive/ViewModels/index.md
T
gramps a0a8331569 TASK 24 post-pause polish batch (creator's 8 review issues, 2026-08-15): (1) desktop/game audio volume slider + (2) mute were no-ops — the AudioMixer's gain Func seams defaulted to unity because the VM never passed them; new AudioGainProvider (Services/Audio/) wires MicMuted/MicVolume/GameMuted/GameAudioVolume into the mixer at construction, read live each mix tick, so sliders and mutes are stream-honest; the game bar also scales TRAX locally via new MusicPlayer.LocalGain (UpdateTraxLocalGain on every game volume/mute change + on load) so the creator HEARS the controls work on the music in the headphones. (3) TRAX played once then stopped — OnPlaybackStopped only looped on Position>=Length (unreliable for MediaFoundationReader); now any clean stop rewinds + replays, errors/explicit stops surface via PlaybackEnded; unused using dropped. (4+6) TRAX moved right of Socials in the footer's left cluster (centered under scenes/sources); the mic cluster is now MIC + meter + mute + volume. (5) mic source persists — LayoutStore gains a Settings key/value table (SaveMicSourceName/LoadMicSourceName); the VM saves on pick and restores before the mixer's first Start, so a restart reconnects the same vetted device (green) or reports it missing (yellow). (7) Backdrop's missing trashcan shifted the edit/eye icons right — new HiddenBoolToVisibilityConverter keeps the trash column reserved (Hidden, not Collapsed) so every source row's icons stay in fixed columns. (8) a 1px hairline with top/bottom padding separates scenes from sources in the left panel. Docs in the same commit: ai.md (gains honest + drive TRAX locally, TRAX loops, mic persistence, footer layout), TASKS.md TASK 24, HANDOFF.md shipped state, ViewModels/index.md. ONE integration test: AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback (real mixer + provider gains through the pipe harness: scaled loopback audible, silence after mute). Build 0 warnings, 197 tests passing
2026-08-15 10:23:49 -07:00

10 KiB
Raw Blame History

ViewModels — index

MVVM layer. See schema.md for the memory-map conventions.

File Purpose
MainViewModel.cs The app brain: scenes/elements collections + commands, stream state (IsLive/IsOffline/IsConnected), chat feed, overlays, layout save/open, resolution dropdown (QualityOptions, SelectedQuality, ResolutionHelp, ApplyStreamQuality) that computes the output rect over the 1920×1080 master (OutputRectX/Y/W/H, DimRects, IsOutputCropped, ResolutionBadgeText). Auth: loads the saved DPAPI session at startup (LoadSavedSessionAsync), SignInAsync feeds the GoLive dialog; go-live (ship step 7): BeginGoLive accepts the dialog → sets StreamVisibility (always "Private") → fires CreateBroadcastAsync (stores _currentBroadcastId for TASK 5's bind/transition; failure → StreamStatus.Error) + _framePump.StartAsync(); End Livestream signs out (StopStream clears session + token + _currentBroadcastId — crash-safe). Connected account: top-bar avatar/name (AccountAvatarUrl/AccountDisplayName via SyncConnectedAccount, cleared on End). Element rows: scenes are pure selection rows (no per-row icons); source rows carry edit/visibility/trash (EditElementCommand sets element.IsEditing, ToggleElementVisibilityCommand flips IsVisible, RemoveSourceCommand; the Backdrop's trash slot stays reserved via HiddenBoolToVisibilityConverter so the edit/eye icons keep their fixed columns — TASK 24); duplicate resource names get a no-space incrementing suffix via shared NextSourceName (Image, Image2, Image3… — next free number derived from actual names, so deletions never collide; used by AddSource + AddReusedImage). Audio (KISS — the mic is the creator's only audio control; capture SHIPPED, runs for the app's lifetime so the meters preview live): the sound meter is a READ-ONLY realtime level display: fill = Math.Min(1, AudioLevelMeter.ToDisplay((float)AudioLevel) * MicVolume) (MeterFillWidth/MeterBrush, green→yellow→red, zone markers at 60%/80% — ToDisplay maps the raw linear RMS onto a 60..0 dBFS scale with +10 dB amplification so real speech/game levels occupy the bar and peaks hit red at maxed volume), where AudioLevel (live input, 0 with no input) is fed by the audio mixer once capture lands and MicVolume acts as a gain on ambient noise; while the slider is dragged the bar previews the slider position (SetVolumeAdjusting), returning to the live level on release (0 with no input — clicking the meter does nothing); MicVolume (default 0.8) + read-only MicMuted/MicMuteText/ToggleMicMuteCommandMicVolume drives MicMuted (muted ⇔ volume 0): sliding to 0 flips the speaker to muted, sliding up from 0 clears it; muting stores the prior volume, unmuting restores it (default 0.8 if unknown) and flashes the meter to the restored position ~300ms (BeginVolumeFlash/EndVolumeFlash); MicSourceName = picked voice source, shown left-justified inside the meter bar (the fill runs at 75% opacity so the text + ruler markings show through); OpenMicPickerCommand/PickMicrophone() open the MicPickerDialog (a picked device takes effect immediately — PickMicrophone swaps the live source via _audioMixer.RestartMic(), loopback keeps running); the MIC label is a button (OpenMicPickerCommand) with a status dot (MicStatus, Models/MicStatus: green = MicConnected via the source's Started event, yellow = MicFailed — in use/unplugged, red = no mic device at startup; MicStatusBrush/MicStatusToolTip); capture starts once at startup (StartMicCaptureAsync) — NOT go-live (BeginGoLive/StopStream no longer touch the mixer) — zero devices = red dot + the mixer never starts. Game audio bar (desktop/game, overlaid at the bottom of the preview window — bottom-center chip, a mirror of the mic bar): IsGameAudioBarVisible (shown only while a full-screen game is producing sound — the VM polls IGameAudioDetector via the default GameAudioDetector every 250ms (_gameAudioTimer); the pure GameAudioHysteresis SHOWs after ~500ms of fullscreen+sound, HIDEs ~1s after leaving fullscreen, and silence never hides an active bar), GameAudioLevel (loopback meter via LoopbackLevelChanged, scaled by volume), GameMuted/GameMuteText/ToggleGameMuteCommand, GameAudioVolume (0..1 volume slider), Begin/End/CancelGameVolumeFlash (mirrors the mic bar's volume flash); the game speaker + slider share the mic bar's PreviewMouseLeftButtonDown/Up + LostMouseCapture code-behind pattern. Webcam: owns CameraManager (MediaCapture), AddWebcamToActiveSceneAsync (picker → default 480×270 bottom-right placement → acquire; the Windows camera picker ALWAYS opens so the creator chooses — never silently reuses the previous camera; picking a different camera swaps the app-wide identity via SwapWebcamIdentityAsync, same path as ChangeWebcamAsync; propagates the running shared bitmap via CameraManager.GetPreviewBitmap first, so a webcam added mid-session never renders a transparent container; greys out when the active scene already has a config via CanAddWebcamToActiveScene), ChangeWebcamAsync (device swap — ReleaseAllAsync old + re-acquire), ShowWebcamInActiveScene (reveal hidden config / empty-canvas right-click), ReacquireWebcam after layout load (re-propagates the shared bitmap to every config), OnCameraPreviewBitmapChanged forwards the shared bitmap into every WebcamSceneConfig.VideoImageSource; SelectedElement drives the preview overlay + internal ClampWebcamToBounds(config, sceneName) (per-scene size cap seam: 50%-per-dimension everywhere, half-screen-AREA in Chat via MaxWebcamWidthFor/MaxWebcamHeightFor). Scenes (five-scene catalog): empty DB seeds Starting/Live/BRB/Chat/Ending (SceneCatalog.All); AddScene(name) accepts only canonical, missing names; MissingScenes/ShowAddScene drive the "+" button (hidden once all five exist; its menu lists only the missing scenes via AddSceneCommand). Screen backdrop (Live-only by policy): owns ScreenCaptureManager + ScreenCaptureSourceFactory (WinRT GraphicsCapture), internal static EnsureBackdrop heals one per backdrop-enabled scene (gated on Scene.HasBackdrop), ReacquireScreenCaptures/RefreshBackdropAutoCapture/NoteBackgroundWindow key by full-screen game monitor or the primary display via Win32FullScreenDetector (GetDisplays()/PrimaryMonitorIndex()), ChangeBackdropCaptureAsync (OS picker) + SetBackdropCapture(DisplayInfo) (in-app "Capture Display"), RedesignateBackdropAsync re-targets all backdrops and releases orphaned sessions, internal static EnforceBackdropPolicy normalizes Scene.HasBackdrop by name on every load + strips lingering non-Live backdrops, CanChangeBackdrop gates the capture menu to Live (the only scene with a backdrop), BackdropImage hides the preview watermark (ShowPreviewPlaceholder). REC sign (2026-08-13): top-center indicator always visible — RecDotBrush (offline #555555, live #e94560, live-private #8f1f1f), RecTextBrush (dim offline, white live), RecDotOpacity (0.55 offline, pulsing 1.0/0.35 live via _recDotPulse flipped on the live tick), IsLivePrivate (IsLive && StreamVisibility == "Private") — notified from the StreamStatus + StreamVisibility setters; LiveIndicatorVisible/LivePulseOpacity removed. Mic mute icon: a second 16px clickable glyph (mic, red + slash when muted) between the meter and the speaker on the mic bar — same ToggleMicMuteCommand. TASK 24 (2026-08-15): the gain seams (AudioGainProvidermicGain/loopbackGain) reach the mixer at construction so volume sliders + mutes are stream-honest; the game bar also scales TRAX locally (MusicPlayer.LocalGain); MicSourceName persists via LayoutStore's Settings table + restores before first Start; TRAX moved next to Socials in the footer's left cluster. Health stats (TASK 4 ship step 6, 2026-08-13): OnFramePumpHealthUpdated marshals FramePump.HealthUpdated (encoder's parsed bitrate/FPS/dropped/duration — raised on the stderr thread) onto the UI thread into CurrentHealth (bottom bar bindings); ResetHealth(status) zeroes dropped/duration on go-live/End so stats never linger
GoLiveViewModel.cs Start Stream dialog: account row (saved channel shown with Change Account, or Sign in to YouTube; Start gated on IsSignedIn/IsBusy) + title/description + locked Visibility (always "Private" — ship step 7, no dropdown), start/cancel requests
About hub (TASK 21, in MainViewModel) the About overlay is the in-app creator hub: ChannelUrl/MastodonUrl/CoffeeUrl (public consts, live) + PremiumUrl (tabled billing seam — TASK 23 → the greyed "Unlock Premium" button lights up when filled); OpenLicensesCommandShowLicenses() loads the shipped THIRD-PARTY-NOTICES.txt into LicensesText and sets IsLicensesOpen (in-app scrolling panel, never the OS viewer); BackToAbout_Click in MainWindow.xaml.cs flips it off.
ReuseImageViewModel.cs Add Image dialog: candidate list (ReuseImageCandidate), reuse/new/cancel
CameraPickerViewModel.cs Add Webcam dialog: async camera list (loading / has / none states), UseRequested(CameraDeviceInfo)/CancelRequested
MicPickerViewModel.cs Select Microphone dialog (voice source): async mic list (loading / has / none states), UseRequested(MicrophoneDeviceInfo)/CancelRequested
SocialsDialogViewModel.cs Social Media Site Promotion dialog (six fixed slots, SocialSlotViewModel): row 0 = YouTube sign-in gate (sign-in provider fills the channel, delete = sign-out); rows 1-5 validated entries / empty / freemium locks 2-5 (isPremium seam); DetectServiceISocialValidator on confirm (input snaps to text + real logo on success, red do-not + stays editable on failure); CanSave blocked while any slot edits/validates/errors; BarEnabled switch; commit via CommittedEntries + BuildSocialsConfig() (YouTube first) + SaveRequested/CancelRequested. WPF-free — the window owns the dialogs (delete confirm, sign-in gate) and calls StartEdit/ConfirmEdit/DeleteSlotAsync.

Related: Models/index.md for the data; Services/index.md for what the ViewModels drive; base class in Helpers/ViewModelBase.cs.