Fix 22 bugs: SMTC crashes, mixer leaks/races, seekbar freeze, settings leak, UIA stalls, OSD, startup - #1109
Fix 22 bugs: SMTC crashes, mixer leaks/races, seekbar freeze, settings leak, UIA stalls, OSD, startup#1109frans15-2006 wants to merge 99 commits into
Conversation
- Subscribe to AudioEndpointVolume.OnVolumeNotification in VolumeMixerViewModel.AttachDevice (unsubscribe on reattach/dispose) so external volume-key changes push MasterVolume/IsMasterMuted immediately instead of relying on stale polling - Add _suppressDevicePush guard to stop Sync->setter->device write-back loop that could overwrite OS changes with stale reads - Make SyncMasterFromDevice dispatcher-aware and exception-safe (fixes cross-thread PropertyChanged from low-level hook) - Marshal VolumeMixerWindow.ShowFlyout to UI dispatcher in MainWindow.HookCallback (hook fires off-UI-thread, before OS applies step) Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…resume Visualizer: - Stop WASAPI cleanly on Suspend and clear cached device ID; on Resume wait 2.5s for audio stack then restart with backoff - Extend restart retries 5x500ms -> 10x800ms plus one delayed 15s follow-up so slow resume without further system events still recovers - Re-arm single-shot silence watchdog while silence is short (was firing once then going blind) and null-guard DataAvailable timer access - Restart on abnormal RecordingStopped (device invalidated underneath us) Volume mixer (same S3 root cause: stale MMDevice, no DefaultDeviceChanged on desktop): - Subscribe to PowerModeChanged.Resume + SessionUnlock/Logon and re-resolve default render device after 2s settle, fixing frozen volume flyout and single-session mixer after sleep Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Manifest: - Add desktop/com to IgnorableNamespaces (strict parsers reject unknown unmarked extension namespaces -> 'error in parsing the app package') - Fix Dependencies versions (bogus MaxVersionTested 10.0.0.0, stale 14393 min) to match wapproj 19041/26100 Installer: - Check for .NET 10 Desktop Runtime (x64) in FluentFlyout_Installer.bat; GitHub builds are framework-dependent so a missing runtime installs fine but the app silently never starts (no window, no Task Manager entry) - offer the download instead of failing silently - Document the prerequisite in README.txt and README.md msixbundle section - Drop stale dev-machine AppInstallerUri from wapproj Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…ng change UpdateUi set the new single-copy text but left the previous song's forever-loop TranslateTransform running until the async position pass rebuilt marquees. Frames in between rendered the new text at the old offset (empty widget), then snapped back (blink) - worst with loop mode, which doubles the text. - ResetMarquee helper stops the animation, zeroes X and restores single-copy text synchronously on song change and on hide - Refresh cached StringWidth measurements + UpdateMarquees immediately for text changes only (play/pause updates skip it so scrolling never restarts); extracted shared RefreshCachedTextWidths for CalculateSize Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Alt+Tab/keys dying over time (regression vs v2.10, worse at boot): HookCallback ran ShowMediaFlyout synchronously on the low-level keyboard hook - blocking Dispatcher.Invoke, media COM fetches and animation setup on every key press. Slow hooks stall all keystrokes system-wide and Windows silently drops them. Debounce + queue via Dispatcher.BeginInvoke so the hook returns immediately (volume path already deferred). Dismiss jump-up-then-down: CloseAnimation derived From from GetWindowPlacement (zeros for transient handles) scaled by the selected monitor's DPI (wrong when the flyout lives on another monitor). Now From comes from the window's actual rendered position (PointToScreen + host monitor DPI, placement fallback), To is always recomputed as From +/-20dip (pinned to From when animations are off so stale storyboard values can't teleport the window). Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
… silent guards) - OpenAnimation DIP scaling can race the window's DPI flip on mixed-DPI multi-monitor setups, stranding the flyout off-screen; queue a raw-pixel resting-position correction after the flight (no-op when already exact). Shared by media + volume flyouts. - Normalize out-of-range Position setting to bottom-center instead of flying with a stale storyboard target. - Log ShowMediaFlyout suppressions (null session/disabled/fullscreen) and every show with monitor + raw coords - these paths were silent, leaving empty logs for flyout-never-appears reports. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…D hunt) - HideVolumeOsd enumerated XAML islands with NULL child-after, which always returns the FIRST match: infinite 100%-CPU spin + hiding the wrong island whenever it missed criteria. Chain properly through islands and their content bridges. - Both flyout auto-hide loops (async void) caught only TaskCanceledException: any other throw was an abnormal exit with no trace. Catch Exception, log, emergency-hide. Per-session poll guarded so one dead session can't break the mixer refresh. - AudioSessionModel: suppress device write-back while syncing from device (same echo guard as master volume). - Breadcrumb logs on volume flyout show/hide for the next crash report. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Explorer recreates the OSD XAML island (new HWND), so the stored hidden handle is dead and ShowFlyout's Zero-gate never re-hides. On TaskbarCreated recovery (post-stabilization), reset the handle and re-hide with retries, gated on VolumeControlEnabled. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…it audio OnDataAvailable assumed 16-bit int or 32-bit float and treated interleaved channels as mono samples: 24-bit streams (typical of spatial/Atmos pipelines) decoded as permanent silence, and 6/8-channel streams fed the FFT at multiples of the real rate. Decode per WaveFormat (8/16/24/32-int, 32-float via Encoding) and downmix each frame to mono, with a non-finite guard. Stereo behavior is equivalent (L/R average vs alternating). Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Process.MainModule throws Access Denied for admin-level processes seen from a non-elevated caller, so sessions fell back to 'Unknown' with no icon. Resolve exe paths via OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION) + QueryFullProcessImageName (granted cross-integrity), then read FileVersionInfo/icons from the file itself: new MediaPlayerData.TryGetProcessPath shared by the mixer icon lookup and the session display-name fallback chain (FileDescription -> exe name -> window title/ProcessName). Process handles now disposed. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…tion change - Handle WM_DISPLAYCHANGE (was ignored): immediately drop cached tray/XAML handles, poisoned automation lookups and monitor id, then re-resolve the taskbar/position instead of running up to 1.5s stale. WindowProc is now an instance method to reach instance state. - Invalid DPI no longer keeps the stale hit-test region: reset to empty (invisible + click-through) instead of leaving a ghost that eats clicks. - Sanitize region rects (NaN/infinity/overflow/non-positive) to Empty so a garbage rect from transient geometry can never swallow taskbar input. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…exclusive mode) pauseOtherMediaSessionsIfNeeded ran on every media-property change, so thumbnail refreshes/progress updates re-paused the other app even when playback focus never moved. Gate the metadata path on genuine new-track/status boundaries (state transitions still pause via CurrentSession_OnPlaybackStateChanged). Harden PauseOtherSessions fan-out (null session guard + per-session catch; it is fire-and-forget). Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Any duplicate launch signals the running instance to open its settings window, so duplicate autostart entries/boot storms surface a window on every login. Gate the signal: when the first instance started <30s ago, the duplicate exits silently. Interactive re-launches keep opening settings. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
SongImage ImageBrush used Stretch=UniformToFill inside the fixed 78x78 frame, center-cropping rectangular covers. Use Uniform so the full artwork is always visible (letterboxed). Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Low-level keyboard callbacks run on the installing thread: the hook was installed in the MainWindow ctor, so every keystroke waited behind UI-thread stalls (thumbnail decode, dominant colors, COM) while the mouse stayed smooth and no CPU spiked. Install the hook on a dedicated thread with its own message pump (GetMessage loop + WM_QUIT shutdown, 3s bounded join); marshal the LockWindow presentation step to the Dispatcher since window creation/resource reads are UI-thread affine. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…ntflyout # Conflicts: # FluentFlyoutWPF/MainWindow.xaml.cs # FluentFlyoutWPF/ViewModels/VolumeMixerViewModel.cs Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Commit 08b24cb appended lines to MainWindow.xaml.cs while the file ended without a trailing newline, leaving an orphaned 'ShowMediaFlyout(toggleMode: true);' statement outside the class - a syntax error that broke the dotnet-format check (parse failure + whitespace fix at EOF). Restore the clean file tail (matching master, no final newline per .editorconfig insert_final_newline = false). Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- The nullable warning at MainWindow.xaml.cs(1174,44) was the last dotnet-format failure: the FindResource(...).ToString() branch of the conditional expression is nullable-annotated, so assigning it to the non-nullable 'label' tripped CS8600 and the style fixer rewrote the file. Compute the label via a nullable resource with a fallback to the resource key (behavior preserved: "Insert" still skips the lookup, missing resources no longer throw into the outer catch). - Revert the temporary debug instrumentation in dotnet-format.yml. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…ity, missing P/Invoke declarations
Fix 15 upstream issues: flyouts, mixer, widget, hooks, startup
Fix 22 bugs: SMTC crashes, mixer leaks/races, seekbar freeze, settings leak, UIA stalls, OSD, startup
Back_Click fired TrySkipPreviousAsync without awaiting or guarding it. The command runs on ControlSession, which TrySkipPreviousAsync does not null-check and which throws COMException when the SMTC session dies between the click and the call - so the click either failed silently (the discarded Task kept the exception as an unobserved task exception, no log at all) or, once awaited anywhere, escaped the async void handler and exited the process (App only logs before crashing). Same treatment as the TryGetPlaybackInfo/TryGetTimelineProperties guards: null-guard ControlSession with the established pattern first, then await the command inside try/catch, log via Logger, and leave the UI state as-is. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
PlayPause_Click fired TryTogglePlayPauseAsync without awaiting or guarding it. The command runs on ControlSession, which TryTogglePlayPauseAsync does not null-check and which throws COMException when the SMTC session dies between the click and the call - so the click either failed silently (the discarded Task kept the exception as an unobserved task exception, no log at all) or, once awaited anywhere, escaped the async void handler and exited the process (App only logs before crashing). Same treatment as the TryGetPlaybackInfo/TryGetTimelineProperties guards: null-guard ControlSession with the established pattern first, then await the command inside try/catch, log via Logger, and leave the UI state as-is. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Forward_Click fired TrySkipNextAsync without awaiting or guarding it. The command runs on ControlSession, which TrySkipNextAsync does not null-check and which throws COMException when the SMTC session dies between the click and the call - so the click either failed silently (the discarded Task kept the exception as an unobserved task exception, no log at all) or, once awaited anywhere, escaped the async void handler and exited the process (App only logs before crashing). Same treatment as the TryGetPlaybackInfo/TryGetTimelineProperties guards: null-guard ControlSession with the established pattern first, then await the command inside try/catch, log via Logger, and leave the UI state as-is. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Repeat_Click awaits TryChangeAutoRepeatModeAsync on ControlSession inside an async void handler with no try/catch. A session that dies between the click and the call rethrows COMException on the await continuation, which propagates to the dispatcher as an unhandled exception and exits the process (App only logs before crashing). This is the same failure class already fixed for the reads via the TryGetPlaybackInfo/TryGetTimelineProperties guards; the write path was missed. Wrap the whole handler body in try/catch (GetActiveMediaSession's focused-session lookup can also throw), log via Logger, and leave the UI as-is: the optimistic SymbolRepeat flip above re-syncs from AutoRepeatMode on the next playback-info update. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Shuffle_Click awaits TryChangeShuffleActiveAsync on ControlSession inside an async void handler with no try/catch. A session that dies between the click and the call rethrows COMException on the await continuation, which propagates to the dispatcher as an unhandled exception and exits the process (App only logs before crashing). This is the same failure class already fixed for the reads via the TryGetPlaybackInfo/TryGetTimelineProperties guards; the write path was missed. Wrap the whole handler body in try/catch (GetActiveMediaSession's focused-session lookup can also throw), log via Logger, and leave the UI as-is: the optimistic SymbolShuffle flip above re-syncs from IsShuffleActive on the next playback-info update. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
## Summary Fixes an unguarded crash path in the media flyout's control buttons — a session dying between the click and the WinRT call could throw unhandled inside an `async void` handler and kill the process. Same failure class already fixed for the SMTC read paths in unchihugo#1109 (`TryGetPlaybackInfo`/`TryGetTimelineProperties`), but these five click handlers were missed. Directly addresses part of open issue unchihugo#807 ("clicking widget buttons crashes the app" / dead-session control calls). ## What changed - `Back_Click` / `PlayPause_Click` / `Forward_Click` — added the file's existing `GetActiveMediaSession() is not { ControlSession: not null }` guard, wrapped the `Try*Async()` call in try/catch, logged failures instead of crashing. - `Repeat_Click` / `Shuffle_Click` — same treatment; these were the actual process-killers since `App` has no `DispatcherUnhandledException` handler and a `COMException` here rethrows onto the dispatcher. UI state (repeat/shuffle icon) isn't manually reverted on failure — it re-syncs from `AutoRepeatMode`/`IsShuffleActive` on the next playback-info update in `UpdateUI`, so no extra state-tracking was needed. ## Scope Only `FluentFlyoutWPF/MainWindow.xaml.cs`, only these five handlers. One commit per handler (no shared helper — Repeat/Shuffle's failure surface differs from Back/PlayPause/Forward). ## Known related gaps (not touched, follow-up candidates) - `TaskbarWidgetControl.xaml.cs` (~761–779): `Previous_Click`/`PlayPause_Click`/`Next_Click` call the same unguarded `Try*Async()` pattern in `async void` — this is likely the actual crash site in unchihugo#807 (widget buttons, not flyout buttons). - `Seekbar_OnPreviewMouseLeftButtonUp` (~1882): unguarded `await ... TryChangePlaybackPositionAsync(...)` in `async void`. ## Verification No .NET SDK available in the authoring environment. Review-based: brace/paren/bracket balance verified across the diff, each handler confirmed defined exactly once with the guarded call, no use-after-catch on pattern variables, no race window between the guard and the awaited call.
…ugo#807) The taskbar widget buttons call TrySkipPreviousAsync / TryTogglePlayPauseAsync / TrySkipNextAsync. Those helpers only null-checked the MediaSession wrapper, not ControlSession, and did not catch COMException from a session that dies between the check and the WinRT call. Because the widget handlers are async void, that exception exited the process — the remaining crash site of unchihugo#807 after the flyout-side click handlers were already guarded. Null-check ControlSession, wrap each call in try/catch, log and return false. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
TaskbarWidgetControl.Previous_Click / PlayPause_Click / Next_Click are async void and called the shared Try* helpers without a local catch. Even with the helpers now guarded, a null MainWindow race (taskbar window recreated mid-click) or any unexpected throw still exits the process via the dispatcher. Wrap each handler (and the open-player context menu) in try/catch and log failures. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Seekbar_OnPreviewMouseLeftButtonUp is async void and awaited TryChangePlaybackPositionAsync on session.ControlSession without null-checking ControlSession or catching COMException. A session dying mid-drag (or a null ControlSession) threw unhandled onto the dispatcher and killed the process; a failed seek could also leave _isDragging stuck true so the position stopped updating. Null-check ControlSession, wrap the seek in try/catch, and clear _isDragging in finally so the drag always ends. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
, unchihugo#923) The taskbar visualizer captures WASAPI loopback, so it reacts to every render stream — system sounds, blocked apps, and any non-music audio. TaskbarVisualizerHasContent was set true whenever the FFT had energy, ignoring the app filter the media widget already uses. That left the bars visible for blocked apps (unchihugo#923) and for any audio that is not an SMTC music session (unchihugo#359). Before marking content present, require GetActiveMediaSession() (which already honours the blocked/allowed list) to report a Playing session. Otherwise force HasContent false so the visualizer stays collapsed. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Some players (ytmdesktop2, certain UWP SMTC bridges) publish a valid session with an empty Title, leaving the media flyout and taskbar widget blank. Add ResolveDisplayTitle that returns the SMTC title when present and otherwise the resolved process/app display name (then the session id). Apply it on every path that paints title text (UpdateTaskbar, playback-state updates, media-property updates, UpdateUI). Narrow the blank-interim suppress from unchihugo#961 so it only kicks in once a non-blank title is already cached for the session — first-time empty titles still fall through to the process-name fallback. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
PauseOtherSessions called ControlSession.GetPlaybackInfo() raw inside a fire-and-forget fan-out. A session dying between enumeration and the call threw COMException (caught, but noisy) and skipped the safer TryGetPlaybackInfo path already used everywhere else for the same failure class. Route through TryGetPlaybackInfo and bind ControlSession once so TryPauseAsync can't NRE on a null session. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…unchihugo#516) Use Album Art as Accent Color only set ControlPlayPause.Background. The AccentedButton style template repainted hover/pressed with the system accent blue, so the button snapped off-theme on hover (unchihugo#910). The seekbar never received the accent at all (unchihugo#516). Switch the play/pause button to TransparentButton (so Background is respected), paint it via ApplyAccentBrush, and dim a derived hover brush on MouseEnter/ Leave. Set Seekbar.Foreground so the filled track uses the same accent. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Fix remaining crash paths and visualizer/title/accent bugs (unchihugo#807, unchihugo#359, unchihugo#923, unchihugo#608, unchihugo#910, unchihugo#516)
|
Vibe coded? :< |
|
I have read and agree to the FluentFlyout CLA 1 out of 2 committers have signed the CLA. |
|
I have read and agree to the FluentFlyout CLA |
|
recheck |
Summary
Implements 22 previously unfiled bugs found via code-level review (not yet reported as GitHub issues). One commit per fix (24 commits — bug #22 ships as four scoped commits). No open issues are referenced; every change is scoped to a specific root cause found in the code.
Media flyout (SMTC)
UpdateUIguard is nowmediaSession?.ControlSession == null, so the "No media playing" reset runs instead of NRE-ing when app filtering removes the last session.TryGetPlaybackInfo/TryGetTimelinePropertieshelpers and converted every rawGetPlaybackInfo()/GetTimelineProperties()call site; a session dying mid-event now logs instead of killing the process.LostMouseCapture, so releasing outside the slider can no longer leave_isDraggingstuck true.IsPreviousEnabled(was reusingIsNextEnabled), matching the taskbar widget.Closedevent when the window is closed early, instead of animating a dead window insideasync void.shell:AppsFolder\<AUMID>when the cached process id is -1, instead of throwing insideProcess.GetProcessById.Volume mixer / audio
MMDevice.COMExceptionout of a WPF binding setter.Taskbar widget
StringBuilder.Equals(string)reference-comparison bug with a properOrdinalIgnoreCasestring compare..Wait()calls (up to 1.5s) on the UI dispatcher during taskbar element lookup; converted to async with timeouts.Visualizer
BarCountchanges, avoiding an out-of-bounds window.Settings / API / OSD / startup
SettingsManager.PropertyChangedhandler is now unsubscribed on close.Verification
No .NET SDK available in the authoring environment; verification was review-based (brace balance, no unguarded raw SMTC calls, no blocking UI-thread waits, re-read surrounding logic for each fix). CI (dotnet-format/build) passed on this branch. Manual smoke test before merge still recommended.