Commit Graph
6663 Commits
Author SHA1 Message Date
vicrodh c927eb9c9c feat(qbzd): harden qconnect ownership lifecycle
cx
2026-08-30 22:22:39 -06:00
vicrodh d3cf7ae53d chore(qconnect): wire LAN runtime dependencies
cx
2026-08-30 22:22:31 -06:00
vicrodh b4404f45ba fix(qconnect): enforce shared lifecycle authority
cx
2026-08-30 22:22:08 -06:00
vicrodh 74a41150e9 feat(qconnect): add transactional LAN delegation
cx
2026-08-30 15:33:23 -06:00
vicrodh f30abd12ac feat(qconnect): add official LAN wire service
cx
2026-08-30 13:50:32 -06:00
vicrodh 52c22006ae feat(qconnect): report real buffer state
cx
2026-08-30 13:34:25 -06:00
vicrodh 9493e96e05 fix(qconnect): align auth and playback wire
cx
2026-08-30 13:08:58 -06:00
vicrodh 41b8e7e6f8 merge: DLNA stop-before-load, shadow decoder for proxied tracks
cc
2026-08-30 12:21:35 -06:00
vicrodh 78a177a36d fix(qt): stop a DLNA renderer before a new URI; shadow-decode proxied tracks too
gmediarender ignored a SetAVTransportURI issued while it sat in
PAUSED_PLAYBACK (2026-08-30 smoke: a Plex row did not replace the Qobuz
item until Next), so a loaded renderer is stopped before the new URI.
Server-streamed (Plex/Jellyfin/Subsonic) tracks now also feed the
visualizer: a second, sequential download of the item into a progressive
buffer (LAN traffic, capped at 400 MB) gives the shadow decoder the bytes
the Range proxy never holds; cancelled with the track like the Qobuz one.

cc
2026-08-30 12:21:35 -06:00
vicrodh 3fc23aea40 merge: visualizer shadow decoder while casting
cc
2026-08-30 11:36:06 -06:00
vicrodh a8796ab91e feat(qt): keep the visualizer alive while casting (shadow decoder)
The FFT/scope surfaces are fed by the local engine's writer thread, which
is stopped while a renderer plays, so every visualizer animation froze
the moment a cast started. cast_viz decodes the same bytes the renderer
is served (progressive download buffer, cached track or local file)
silently on its own thread and pushes them into the VisualizerTap at
real-time pace, re-anchored to the renderer's reported position on each
poll (re-seek past 0.75 s of drift, paused with the renderer). Proxied
server-streamed tracks have no local bytes and stay dark.

cc
2026-08-30 11:35:57 -06:00
vicrodh d620990dad feat(player,cast): share one copy of in-memory track bytes across consumers
InMemorySource::from_shared decodes an Arc<Vec<u8>> without copying and
MediaServer::register_audio_shared serves the same Arc, so the cast media
server and the cast visualizer's shadow decoder can sit on one copy of a
100 MB track.

cc
2026-08-30 11:35:57 -06:00
vicrodh 44c108d4d2 merge: casting round 2 — progressive serving, clicked-track routing, source proxy, clean shutdown
cc
2026-08-30 11:13:21 -06:00
vicrodh 657bd9757d fix(qt): cast the clicked track, serve cold tracks progressively, proxy server-streamed sources, stop on exit
Four findings from the 2026-08-30 casting smoke, each with its cause:

- A Local Library click while casting played the OLD queue's current
  track on the renderer: that funnel stages its queue after the audible
  step, and the gate routed "the current track". route_play_remote_track
  hands the renderer the clicked track itself.
- Plex/Jellyfin/Subsonic rows were refused ("serves it over the network").
  The media server now proxies the source url with the ticket's request
  headers (exact size from the probe), closing the TODO the Slint
  service carried.
- A cold Qobuz track waited for the whole download before the renderer
  heard anything (5.5 s for 97 MB; every frontend did this). Uncached
  tracks are served progressively from the player's download buffer;
  cached ones keep the instant byte path. The download is cancelled when
  the track changes, on disconnect and on shutdown.
- Closing the app could leave the renderer playing: the poll held the
  service lock across renderer round trips and the 2 s bounded shutdown
  timed out before sending Stop. The poll handle now lives outside that
  lock and is aborted first. Chromecast: a toggle with nothing loaded
  casts the current track instead of asking an empty receiver to play,
  and IDLE counts as "ended" only after the track was seen PLAYING (an
  idle receiver app auto-advanced the whole queue).

cc
2026-08-30 10:50:47 -06:00
vicrodh 0451000bf0 test(cast): cover the RangeSource media entry
cc
2026-08-30 10:50:46 -06:00
vicrodh abc125bcfe feat(player): progressive external stream for cast renderers
Player::open_external_stream starts the CMAF download and returns a
shared buffer (exact final FLAC size known from the segment table) once
the first segments are in, instead of after the whole file. The finished
download is cached like a played stream. Core exposes it as
open_external_stream_resolved.

cc
2026-08-30 10:27:31 -06:00
vicrodh 976185912f fix(cast): serve arriving bytes, one thread per request, close the receiver app on disconnect
MediaServer gains a RangeSource entry whose bytes may still be arriving
(known final size, blocking reads) and serves every request on its own
thread so a slow media body no longer stalls the HEAD probe or a seek's
second Range request. Chromecast: play/pause/stop never launch the
Default Media Receiver (an app launched with nothing loaded sat IDLE and
read as "track ended"); disconnect stops the receiver app instead of
leaving it on screen with the last item.

cc
2026-08-30 10:27:31 -06:00
vicrodh c059c12bf3 merge: ALSA hardware volume hardening
cx
2026-08-30 09:14:57 -06:00
vicrodh c3556fe3ec feat(qbzd): expose ALSA hardware volume controls
Carry hardware-volume capability, selection, diagnostics, and status through the daemon API, CLI, and TUI while preserving fail-closed behavior on unsupported devices.

cx
2026-08-30 09:14:39 -06:00
vicrodh 96342e23bc feat(qt): expose ALSA hardware volume settings
Add capability-aware enablement, exact control selection, diagnostics, live volume synchronization, and translated unsupported-state feedback to the Qt settings surface.

cx
2026-08-30 09:14:29 -06:00
vicrodh 0e8614fe37 feat(audio): add guarded ALSA hardware volume control
Probe exact mixer element identities, retain stable device routes, and fail closed for ambiguous or switch-only controls. Synchronize player volume through ALSA events without changing direct PCM configuration.

cx
2026-08-30 09:14:16 -06:00
vicrodh 51b7e012f4 merge: qbzd successor warm-up, streaming gapless hand-off, inhibit backoff
cc
2026-08-29 21:53:08 -06:00
vicrodh 95f2d6519d merge: casting idle poll, single remote-renderer seam, volume coalescer
cc
2026-08-29 21:53:08 -06:00
vicrodh a9c886b0d3 fix(media-controls): stop re-asking login1 for an inhibitor every 2 s
`SleepInhibitor::acquire` runs on every Playing state update and retried
the `Inhibit` call each time it failed. On a headless daemon with no
logind session polkit answers `InteractiveAuthorizationRequired` forever,
which produced one WARN every ~2 s (369 lines in 11 min on the Pi smoke,
2026-08-29). Classify the failure: a policy denial is logged once at info
and never retried for this process; a transient error backs off 5 min.
Playback was never affected.

cc
2026-08-29 18:35:03 -06:00
vicrodh e90ea8cada fix(qbzd): warm the successor on every track start and stream cold gapless hand-offs
The daemon only prefetched the next track after a gapped advance, so every
edge after a seamless hand-off (and every QConnect/API play) reached the
10 s gapless window cold and had to materialize the whole next file in
time. Whether it made it was file size vs bandwidth (#699 follow-up on a
1 GB Pi: gapless "sometimes, varies by album"; alternating pattern).

Add a `WarmSuccessor` driver action fired once per current track id on
the first tick that sees it, for every entry point, run as a driver-owned
task that never blocks the control loop; drop the awaited prefetch from
`advance_and_play`. For a successor that is still not cached at arm time,
append its initial CMAF buffer as an incremental stream
(`queue_gapless_streaming`, the desktop's hand-off) and keep the
whole-file byte fetch only as fallback.

Tests: 2 new driver tests (warm once per track for any entry, seamless
edge warms the new successor); qbz-app 269 and qbzd 298 pass.

cc
2026-08-29 16:11:54 -06:00
vicrodh ba6273a5f1 fix(qml): drop the CastPicker quality height binding loop
cc
2026-08-29 15:31:20 -06:00
vicrodh 51f2803f95 fix(qml): TrackRow emits one playRequested per gesture
A double-click on a clickPlays row fired playRequested three times
(two releases plus doubleClicked) and restarted the track three times
within 230 ms. Debounce the release and skip the doubleClicked arm on
clickPlays rows.

cc
2026-08-29 15:31:20 -06:00
vicrodh c7be041543 fix(qt): route every audible start through one remote-renderer seam
The cast gate lived only in `play_queue_track`; every other entry point
(single track, album, artist, track lists, playlists, MyQBZ, the Local
Library funnel) only checked the QConnect peer, so clicking a track while
casting started the local backend beside the renderer (DLNA played one
track, the host another). `route_play_remote` (cast, then peer, then
local) is now the single gate, called from all 12 entries and from
`play_audible`; `route_play_to_peer` is private. A tripwire warning in
`play_resolved_offline_aware` names any future entry that skips it.

cc
2026-08-29 15:31:20 -06:00
vicrodh f19b870398 fix(dlna): skip SetVolume when the quantized 0-100 level is unchanged
cc
2026-08-29 15:31:20 -06:00
vicrodh 3a52638fce fix(cast): keep an idle Chromecast connected instead of dropping it after 5 s
`get_media_position` returned `NotConnected` whenever no media app had
been launched yet, and the 1 Hz poll folded that into "no answer", so a
connect made while paused hit LOST_POLL_MAX and disconnected after
exactly 5 s (reproduced three times on the 2026-08-29 smoke). Add a
typed `CastError::NoMediaSession`, and while idle prove liveness with
the receiver-level `get_status` request instead of the media session.

Also coalesce cast volume: a slider drag now hands the newest level to
one sender task every 120 ms instead of one Cast/SOAP round trip per
pixel.

cc
2026-08-29 15:31:20 -06:00
vicrodh bb43dea5b9 merge: integrate playback recovery and playlist menu
cx
2026-08-29 14:56:13 -06:00
vicrodh 3dbc1fc9d0 fix(qt): restore playlist header menu
cx
2026-08-29 14:55:30 -06:00
vicrodh 9e1f2c5559 fix(playback): recover stalled streams
cx
2026-08-29 14:55:21 -06:00
vicrodh 7d9d3b896e merge: dependency bumps for 2.1.0 (rusqlite 0.40, reqwest 0.13, rand 0.10, prost 0.14, …)
Verified by smoke on the release build (login, playback, scrobble, Local
Library, file dialog, cast discovery).

cc
2026-08-29 12:48:34 -06:00
vicrodh 8964544891 chore(deps): reqwest 0.13 (rustls-no-provider; query/form are opt-in features now)
cc
2026-08-29 09:00:56 -06:00
vicrodh 3785a7eb40 chore(deps): rfd 0.17 (xdg-portal; the async-std feature is gone)
cc
2026-08-29 08:53:42 -06:00
vicrodh 6d8c951a38 chore(deps): rusqlite 0.40 (u64 casts; a latent two-statement execute fixed)
rusqlite 0.40 removed ToSql/FromSql for u64 (SQLite integers are i64):
every u64 column now reads/writes through an explicit i64 cast. It also
rejects a multi-statement execute() instead of silently running only the
first statement — which is what catalog.rs's editions/logical_albums
prune had been doing all along; it is an execute_batch now, so the
logical_albums prune finally runs.

cc
2026-08-29 08:36:21 -06:00
vicrodh d89397415b chore(deps): mdns-sd 0.21 (ScopedIp addresses)
cc
2026-08-29 08:26:25 -06:00
vicrodh 1beb061198 chore(deps): mpris-server 0.10
cc
2026-08-29 08:26:25 -06:00
vicrodh 51e496e47f chore(deps): tokio-tungstenite 0.30
cc
2026-08-29 08:26:25 -06:00
vicrodh 957ce924b3 chore(deps): prost 0.14
cc
2026-08-29 08:24:28 -06:00
vicrodh 0b4c9e28b5 chore(deps): base64 0.23
cc
2026-08-29 08:24:28 -06:00
vicrodh 06453f8768 chore(deps): roxmltree 0.21
cc
2026-08-29 08:24:28 -06:00
vicrodh 0cded1e395 chore(deps): rand 0.10 everywhere (qbz-player, qbz-secrets were on 0.9)
RngCore/fill_bytes -> RngExt/fill, the trait names rand 0.10 exports.

cc
2026-08-29 08:24:28 -06:00
vicrodh c4d3fc7e3b chore(deps): cargo update — 194 semver-compatible bumps
cc
2026-08-29 08:23:36 -06:00
vicrodh 51daf760b7 docs+ci: two glibc floors — x86_64 2.35, arm64 desktop 2.39, qbzd 2.35 everywhere
Decided 2026-08-29: the arm64 Qt build only works on ubuntu-24.04-arm
(aqt's arm64 host tools segfault on 22.04-arm), so the arm64 desktop
app requires glibc 2.39; qbzd keeps building on 22.04-arm at 2.35 so a
trimmed Pi OS bookworm image stays a valid target. The floor gate asserts
per arch instead of warning; README states both floors.

cc
2026-08-29 08:08:14 -06:00
vicrodh a1763df250 merge: release workflows green on every platform (desktop-entry gate, Icon Composer icon, arm64 runner)
cc
2026-08-29 07:54:17 -06:00
vicrodh d66c993ade ci(macos): accept @rpath Qt install names with a bundle-relative LC_RPATH
Qt 6.9's macdeployqt leaves @rpath/Qt*.framework references resolved
through LC_RPATH @executable_path/../Frameworks; the old assert only
accepted a literal @executable_path and failed a correct bundle. Reject
absolute paths, require the rpath, and let the offscreen smoke prove it.

cc
2026-08-28 21:41:15 -06:00
vicrodh e70e5e4ab9 ci(linux-arm64): experiment — build aarch64 on ubuntu-24.04-arm (aqt qmake segfaults on 22.04-arm)
Both 6.8.3 and 6.9.3 arm64 qmake die with SIGSEGV on ubuntu-22.04-arm
with no missing libraries. Try the 24.04-arm image; the glibc floor gate
warns instead of failing for aarch64 until the floor is decided.

cc
2026-08-28 21:13:51 -06:00
vicrodh 860f1ad67f ci(linux): the AppDir check no longer expects .DirIcon (appimagetool writes it)
cc
2026-08-28 21:10:49 -06:00