Skip to content

feat: playlist mode with lifecycle hardening, PipeWire safety, and freezing fixes#253

Open
rhafaelcm wants to merge 22 commits into
jeffshee:masterfrom
rhafaelcm:master
Open

feat: playlist mode with lifecycle hardening, PipeWire safety, and freezing fixes#253
rhafaelcm wants to merge 22 commits into
jeffshee:masterfrom
rhafaelcm:master

Conversation

@rhafaelcm

@rhafaelcm rhafaelcm commented Mar 20, 2026

Copy link
Copy Markdown

Summary

  • Playlist mode: Added playlist with configurable repeat count for all monitors
  • Thumbnails & duration: Show video thumbnails and duration in both Local Video and Playlist tabs
  • Add All to Playlist: Button to add all local videos to the playlist at once
  • Clear playlist: Button to clear all playlist items
  • Unsaved changes warning: Label shown when playlist has pending changes
  • Error handling: Graceful handling of thumbnail generation failures
  • VA-API: Enable hardware acceleration in Flatpak build
  • Open GUI on active tab: GUI now opens on the tab matching the current playback mode
  • Deadlock fix: Found that VLC's internal decoder stop() (triggered by set_media() on an active player) blocks waiting for decoder threads, which in turn need the GTK main thread for X11 video output — causing a deadlock. To solve this:
    • Removed fade transition effect between playlist videos (simplifies transition logic)
    • Replaced in-place set_media() with full VLCWidget recreation on each playlist video change — the old widget is cleaned up synchronously, completely avoiding the GTK main thread deadlock
  • GPU memory leak fix: Synchronized VLC widget cleanup to release GPU decoder buffers before allocating a new instance, preventing unbounded GPU memory growth during playlist playback
  • Centercrop timing fix: video_set_crop_geometry() was being called before VLC created its video output, causing intermittent failure to fill the screen (especially on 2K monitors). Now uses the MediaPlayerVout event with a timed retry fallback to apply crop geometry only after the video output is ready. Also fixed _on_size_changed() calling non-existent methods on Gdk.Monitor.
  • Playlist player lifecycle stabilization: Hardened the playlist player with explicit transition state, watchdog for stuck playback, structured recovery path, deterministic event detach/attach around media swaps, and per-monitor cleanup on shutdown. Eliminates several stale-state classes of bugs that surfaced after long playlist sessions.
  • AV1 detection and flagging (config v6): Probe each playlist item with ffprobe and persist per-item media info (codec, dimensions, duration, problematic flag). AV1 videos without HW decode are marked as problematic, forced to software decode, and shown with an orange tooltip in the playlist UI; the player logs codec info on playback, tracks recovery attempts per item, and advances past videos that keep stalling instead of looping forever.
  • PipeWire safety in playlist mode: Repeatedly opening/closing libVLC's audio output in playlist mode was causing PipeWire to lock up after a while; switching to media reuse caused the opposite — Hidamari itself froze. Since playlist mode never needs audio, the fix is to never load the audio backend at all and harden the lifecycle for long sessions:
    • Build libVLC with --no-audio --aout=none whenever the player starts in MODE_PLAYLIST (VLCWidget.disable_audio propagated from VideoPlayer through PlayerWindow); recovery preserves the flag automatically.
    • Always add :no-audio per media in _set_playlist_media (defense in depth, not just on non-primary monitors).
    • Soft-restart proactively recreates the VLCWidget every PLAYLIST_SOFT_RESTART_EVERY = 25 transitions to keep the decoder healthy on long sessions.
    • Sync 250 ms time.sleep between Instance release and recreation inside recover_vlc_widget gives libVLC time to free the decoder before a fresh Instance starts on the same widget.
    • Circuit breaker: PLAYLIST_RECOVERY_MAX_IN_WINDOW recoveries within PLAYLIST_RECOVERY_WINDOW_SEC mark the current video as problematic and advance synchronously instead of looping.
    • Deterministic sanitization before recreate: bump preload generation and cancel fade / fade_opacity on every window so in-flight workers never touch a widget about to be destroyed.
    • window.stop() inline before set_media releases the previous stream synchronously on every transition.
    • Watchdog less aggressive: trigger recovery on 2 consecutive stalls (3 for AV1 + software decode), raise interval from 30 s to 45 s, and make recover/advance calls synchronous (no GLib.idle_add hop).
  • Audio gating in playlist mode (UI + DBus): Visible italic note under the playlist tab heading explaining audio is disabled in playlist mode; mute toggle and volume slider greyed out with tooltip; the underlying Gio.SimpleAction mute is also set_enabled(False) so the hamburger popover, keyboard accelerators and app.activate_action are all blocked — not just clicks on the widget. Server-side is_mute and volume DBus setters short-circuit when MODE_PLAYLIST is active, covering the systray menu and any other DBus client.

Commits

  • 6903541 feature: add playlist mode with repeat count for all monitors
  • 4c82608 fix: resolve TypeError in config load and improve playlist UX
  • c7aa0e1 feat: enable VA-API hardware acceleration in Flatpak
  • 692ca64 feat: smooth playlist transitions, thumbnails/duration in UI, add-all button
  • 5b46ad3 fix: resolve freezing during playlist transitions
  • 60740b2 fix: handle thumbnail generation errors gracefully
  • 6b8f46b feat: add clear playlist button and unsaved changes warning
  • 186e572 fix: resolve playlist freezing caused by Fade timer race condition and resource leaks
  • 7398aba fix: resolve playlist freeze caused by blocking ffprobe and config race
  • 7962ffb fix: use VLC input-repeat to prevent VA-API decoder leak on playlist repeats
  • 9d33b14 fix: stop players before loading new media and add stuck-video watchdog
  • 8462add fix: remove explicit stop() that caused GTK main thread deadlock
  • 134880a refactor: remove fade transition effect from playlist video switching
  • 2eff7a0 fix: recreate VLCWidget per playlist transition to prevent deadlock
  • 43b56f8 feat: open GUI on the tab matching the active playback mode
  • 350e46e fix: prevent GPU memory leak during playlist transitions
  • 10ef8ff fix: apply centercrop after VLC vout is ready to prevent intermittent scaling on 2K monitors
  • a9a6ae4 Stabilize VLC playlist player lifecycle
  • f8d704c feat: detect and flag AV1 videos without hardware decode support in playlist
  • 9ecf902 fix: prevent PipeWire crashes and Hidamari freezes in playlist mode
  • cb72e87 feat: surface audio-disabled notice on the playlist tab
  • 3bd3761 fix: actually block mute/volume changes while in playlist mode

Test plan

  • Test playlist creation, reordering, removal, and clearing
  • Verify instant transitions between playlist videos (no freeze)
  • Confirm thumbnails and duration display in Local Video and Playlist tabs
  • Verify "Add All to Playlist" works correctly
  • Check unsaved changes label appears/disappears correctly
  • Confirm no freezing during extended playlist playback across multiple monitors
  • Verify GUI opens on the correct tab for each playback mode
  • Verify GPU memory stays stable during extended playlist playback (no unbounded growth)
  • Verify video fills entire screen on both 2K and Full HD monitors during playlist transitions
  • Confirm AV1 videos without HW decode show the orange warning tooltip in the playlist tab and force software decode at playback time
  • Confirm a persistently stalling playlist item gets marked problematic and the playlist auto-advances to the next item instead of looping recovery
  • In playlist mode, run wpctl status (or pactl list source-outputs) and confirm there is no vlc client connected to PipeWire
  • Leave a playlist with short videos (10-30 s) running for at least 1 hour and check logs:
    • [Playlist Health] lines (60 s interval) with stable RSS
    • [Playlist] Soft-restart log line every 25 transitions
    • no consecutive Recovery #N loops
  • In playlist mode, confirm the audio-disabled note is visible on the playlist tab and the volume / mute controls are greyed out everywhere they appear
  • In playlist mode, try to toggle mute via the hamburger popover, the systray "Toggle Mute Audio" item and a direct DBus call (gdbus call ... org.freedesktop.DBus.Properties.Set ... is_mute) — all attempts should be ignored and ~/.config/hidamari/config.json should keep the original is_mute value
  • Switch from playlist back to single video / stream mode and confirm audio works normally (fresh libVLC Instance without --no-audio) and the volume / mute controls are sensitive again

Built and packaged: https://github.com/rhafaelcm/hidamari/releases/tag/fork-v0.1.0-playlist

Allows users to create a video playlist that rotates across all monitors.
Each video plays to completion N times (configurable repeat count) before
advancing to the next video in a circular loop. Uses VLC's
MediaPlayerEndReached event for precise end-of-video detection.

Made-with: Cursor
- Fix _checkDefaultSource crashing with None path for non-video modes
- Add "Add to Playlist" option to right-click context menu in Local Video tab
- Remove separate Add button from Playlist tab, update description
- Add PKG_CONFIG_PATH and runtime env vars for lib64 in Flatpak manifest

Made-with: Cursor
Add LIBVA_DRIVERS_PATH with Intel, Mesa, and Mesa default driver
paths so libva can auto-detect the correct VA-API driver for any GPU.

Made-with: Cursor
… button

- Add fade-out/fade-in opacity transition between playlist videos
- Show thumbnails and video duration in the Playlist tab (TreeView)
- Display video duration below each thumbnail in Local Video tab
- Add "Add All to Playlist" button in Local Video tab
- Add get_video_duration() and get_thumbnail_pixbuf() utility functions
- Fix thumbnail loading bug when generate_thumbnail returns True but path is None

Made-with: Cursor
- Static wallpaper only set once (first video) instead of every transition,
  and now runs in a background thread to avoid blocking the GTK main loop
- Add _is_transitioning flag to prevent race conditions from overlapping
  fade animations when MediaPlayerEndReached fires multiple times
- Move get_video_duration calls to background threads in playlist store
  to avoid blocking UI with synchronous ffprobe calls
- Use GLib.idle_add for thread-safe ListStore updates in IconView

Made-with: Cursor
Wrap generate_thumbnail and get_thumbnail in try/except to catch
GLib.GError when the external thumbnailer (totem-video-thumbnailer)
fails for unsupported or corrupted videos, preventing thread crashes.

Made-with: Cursor
- Add "Clear playlist" button with edit-clear-all-symbolic icon
- Show "Unsaved changes" label when playlist is edited (add, remove,
  move, clear) that disappears after clicking "Apply Playlist"

Made-with: Cursor
…d resource leaks

- Fix critical race condition in Fade class using cycle_id to prevent
  orphaned Timer thread accumulation across playlist transitions
- Release old VLC Media objects in set_media() to prevent memory leak
- Protect _is_transitioning with try/finally to prevent permanent deadlock
- Cache ffprobe video dimensions in _setup_playlist to avoid blocking
  GTK main thread on every transition
- Fix Image.open() file descriptor leak in set_static_wallpaper
- Add diagnostic logs: health check every 60s, transition tracking,
  media end counting, thread count monitoring

Made-with: Cursor
- Add logging.basicConfig() in player process so diagnostic logs appear
- Replace synchronous ffprobe pre-cache of all 43 videos (which blocked
  GTK main loop for 20-90s) with lazy background probing per video
- Fix config save race: save config inside _setup_player() after setting
  mode and Default, so the player process reads correct values from disk

Made-with: Cursor
…repeats

The stop()/play() cycle for repeating videos was reinitializing the
hardware decoder on every repeat, leaking VA-API contexts and memory
(549MB -> 817MB in 7 media ends). VLC eventually entered a dead state.

Delegate repeat handling to VLC via :input-repeat media option, which
loops internally without destroying the decoder pipeline.

Made-with: Cursor
Stop all VLC players explicitly before set_media() during transitions
to ensure VA-API decoder resources are fully released before creating
new ones. Previously, set_media() on a still-playing player caused
overlapping decoder contexts and memory growth.

Add a watchdog timer (30s) that detects stuck playback by monitoring
player position, forcing advance to next video if VLC enters a dead
state. Also switch health check memory metric from ru_maxrss (peak)
to VmRSS (current RSS) for accurate diagnostics.

Made-with: Cursor
VLC's player.stop() waits synchronously for decoder threads to finish,
but those threads need the GTK main thread for X11 video output rendering,
creating a deadlock. set_media() handles the swap internally without this
issue. The watchdog timer recovers from stuck VLC decoder states.

Made-with: Cursor
Transition between playlist videos is now instant instead of fading
opacity out/in, simplifying the transition logic.

Made-with: Cursor
Replace in-place set_media() with full VLCWidget recreation on each
playlist video change. The old widget is cleaned up in a background
thread, avoiding the GTK main thread deadlock caused by VLC's internal
decoder stop.

Made-with: Cursor
Synchronize VLC widget cleanup to release GPU decoder buffers before
allocating a new instance. Previously, cleanup ran in a daemon thread
that could fail to complete, causing GPU memory to grow unbounded.

- Make replace_vlc_widget() cleanup synchronous (stop, release media,
  release player/instance, gc.collect) before creating the new widget
- Detach MediaPlayerEndReached event from old player before replacement
- Guard GLib timers with _timers_active flag to prevent accumulation
- Release vlc.Media in VLCWidget.cleanup() before releasing the player
- Call gc.collect() after VLC cleanup to force GPU resource deallocation

Made-with: Cursor
… scaling on 2K monitors

video_set_crop_geometry() was called before start_playback(), so VLC
could silently ignore the crop on newly created widget instances.
Now schedule_centercrop() uses the MediaPlayerVout event plus a timed
retry fallback to ensure the geometry is applied once the video output
actually exists. Also fixes _on_size_changed() calling non-existent
methods on Gdk.Monitor instead of the player window.

Made-with: Cursor
…laylist

Introduces playlist media info tracking (config v6) to probe video codec
via ffprobe and persist metadata per playlist item. AV1 videos without
HW decode are marked as problematic, forcing software decode and showing
a visual warning (markup label, orange color, tooltip) in the GUI. The
player logs codec info on playback, tracks recovery attempts per item,
and can advance past persistently stalling AV1 videos automatically.

Made-with: Cursor
The playlist mode was repeatedly opening/closing the libVLC audio output,
which caused PipeWire to lock up after a while. Switching to media reuse
instead caused the opposite: PipeWire stayed alive but Hidamari itself
froze. Since playlist mode never needs audio, we now build the libVLC
instance without any audio backend at all and harden the playlist
lifecycle to keep long sessions stable.

- Build libVLC with --no-audio --aout=none whenever the player starts
  in MODE_PLAYLIST (VLCWidget.disable_audio propagated from VideoPlayer
  via PlayerWindow). Recovery preserves the flag automatically.
- Always add :no-audio per media in _set_playlist_media (defense in
  depth, not just on non-primary monitors).
- Disable ScaleVolume / ToggleMute in the GUI when mode is playlist,
  with an explanatory tooltip; refreshed after every mode-changing
  apply so the user sees it immediately.
- Soft-restart proactively recreates the VLCWidget every 25 transitions
  via PLAYLIST_SOFT_RESTART_EVERY to keep the decoder healthy on long
  sessions.
- Sync 250 ms time.sleep between Instance release and recreation inside
  recover_vlc_widget gives libVLC time to free the decoder before a
  fresh Instance starts on the same widget.
- Circuit breaker: PLAYLIST_RECOVERY_MAX_IN_WINDOW recoveries within
  PLAYLIST_RECOVERY_WINDOW_SEC marks the current video as problematic
  via mark_playlist_media_info_stalled and advances synchronously.
- Deterministic sanitization before recreate: bump preload generation
  and cancel fade / fade_opacity on every window so workers in flight
  never touch a widget that's about to be destroyed.
- window.stop() inline before set_media releases the previous stream
  synchronously on every transition.
- Watchdog less aggressive: trigger recovery on 2 consecutive stalls
  (3 for AV1 + software decode), raise interval from 30 s to 45 s, and
  make recover/advance calls synchronous (no GLib.idle_add hop).

Made-with: Cursor
Add a small italic note under the playlist tab heading clarifying that
audio output is disabled in playlist mode to keep PipeWire stable. The
mute toggle and volume slider are already greyed out via
set_audio_controls_for_mode(), but a tooltip alone is easy to miss; the
inline note makes the rationale visible at a glance.

Made-with: Cursor
Setting set_sensitive(False) on the ToggleMute and ScaleVolume widgets
greyed them out in the panel but left the underlying Gio.SimpleAction
'mute' enabled, so the hamburger popover and the systray could still
flip is_mute. The widgets only ever invoked --no-audio --aout=none
libVLC instances, so audio never came back on, but the persisted config
still drifted and would resurface the next time the user switched to
video mode.

- gui: also disable the 'mute' Gio action via lookup_action(...).
  set_enabled(False), so menu items, accelerators and programmatic
  app.activate_action calls all respect the playlist gate.
- server: add _is_audio_disabled_mode() and short-circuit the
  is_mute and volume DBus setters when in MODE_PLAYLIST. This covers
  the systray menu and any other DBus client that talks straight to
  the server, keeping the persisted config consistent regardless of
  the entry point.

Made-with: Cursor
@rhafaelcm rhafaelcm changed the title feat: playlist UI improvements, smooth transitions, and freezing fixes feat: playlist mode with lifecycle hardening, PipeWire safety, and freezing fixes May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant