Commit Graph
16 Commits
Author SHA1 Message Date
vicrodh 03ac0a7164 fix(cast): redact media path tokens in media server request logs
Session tokens in /audio/<token>/<id> must not appear in log bundles. Port
of the remaining delta of 9aab6233 (pr/605) onto current pre-release: add
redact_media_uri() to media_server.rs and redact the URL in the three
handle_request logs (request, 404 parse, 403 token mismatch). The device.rs
side and the CSPRNG tokens already landed via PR #586.

km
2026-07-22 16:49:23 -06:00
vicrodh 722ca933ef fix(cast): serve DSD and hi-res PCM to DLNA renderers (#646)
DSD containers (.dsf/.dff) fell through to application/octet-stream in the
media-server MIME map, which strict renderers reject (AVTransport 714), and
the serve path read the WHOLE requested range into RAM before emitting a
single header byte — a 32/384 PCM or DSD track (0.7-1.4 GB) stalled the
response long enough for renderer fetch timeouts and blocked the
single-threaded server for every other request (the reported 'had to
restart QBZ' wedge).

- content_type_for_path + the cast_service UI mirror gain audio/x-dsf /
  audio/x-dff (de-facto DLNA MIMEs; HQPlayer plays native DSF/DFF).
- open_range streams file entries straight from disk (seek + bounded Take);
  headers go out immediately and RAM stays bounded. In-memory (Qobuz)
  entries keep the bounded slice copy. 0-byte entries keep the old 500.
- DSD-aware cast quality label (was the nonsense '1-bit / 2822.4 kHz').
- Best-effort renderer stop() when DLNA load/play fails so it doesn't sit
  half-loaded on a faulted URI.

cargo check + 7/7 qbz-cast tests (new streaming + MIME coverage).

km
2026-07-21 12:29:11 -06:00
vicrodh cd928c92a1 feat(cast): expose whether a Chromecast discovery id is stable (#638)
The mDNS TXT id record is the Cast device UUID - stable across restarts
and renames, directly comparable to a DLNA UDN. When a device omits it,
discovery falls back to the mDNS fullname, which is derived from the
friendly name and silently detaches the moment the user renames the
speaker. The per-renderer quality cap (fix 4) must not be keyed on such
an id - a cap that silently stops applying is the failure mode #638
exists to eliminate - so DiscoveredDevice now says which case it is.

Additive field on the discovery DTO only; no behavior change for
existing consumers. Verified: the struct has a single construction site.

cc
2026-07-20 15:22:38 -06:00
vicrodh 142142e317 fix(cast): clear residual DLNA transport state on connect
A renderer left mid-playback by a previous session re-requests its
orphaned CurrentURI against the fresh media server (new path token ->
403 loop) and reports a stale PLAYING position. That poisons
cast_max_position and trips the premature-STOPPED end-detection guard
into auto-advancing the first cast track. Issue a best-effort AVTransport
Stop right after connect so the transport starts clean.

cc
2026-07-17 21:22:35 -06:00
vicrodh 1638da05dc chore(deps): bump getrandom to 0.4 in qbz-cast
getrandom::getrandom() was renamed to getrandom::fill() in 0.3; adopt fill() at the cast token call site. Consolidates onto the getrandom 0.4.2 that tempfile and rand already pull in.

cc
2026-07-17 20:20:51 -06:00
Afonso Jorge Ramos a0bf1328d6 fix(cast): use CSPRNG media path tokens and redact URIs in logs
Replace RandomState-based tokens with getrandom, avoid logging full
media base URLs, and redact /audio/<token>/… in DLNA info logs.
2026-07-11 23:01:05 +02:00
vicrodh 1261e98cc0 fix(cast): stop hoarding full-track buffers in the DLNA media server
register_audio inserted every cast track's complete bytes into the
entries map and NOTHING ever removed them — not stop, not disconnect;
only app shutdown dropped the server. A DLNA session therefore grew by
one full track per auto-advance (issue #550: 50 MB at launch to
4.5 GB, never released). Three changes:
- register_audio/register_file evict every other entry — only the
  track being cast is servable anyway.
- stop_if_cast and disconnect clear the entries outright, so stopping
  actually releases the buffers.
- MediaSource::Data is Arc-backed: the per-request entry clone no
  longer copies the whole track (it did, on every GET/HEAD/range
  request), and an in-flight response keeps its own Arc so eviction
  can never truncate an ongoing range request.

Fixes #550

cc
2026-07-05 15:08:34 -06:00
vicrodh 3ee5d5d7e3 fix(cast): DLNA discovery, DIDL features and Play self-heal for strict renderers
Three device-side gaps that kept strict renderers (Signalyst HQPlayer 6)
from working:
- Services were matched by the hard-coded ":1" URN, missing renderers
  that only announce AVTransport/RenderingControl :2/:3. Match the
  service name at any UPnP version (unit-tested; recursion over
  embedded devices preserved via services_iter).
- DIDL protocolInfo advertised "*" as its 4th field; strict renderers
  cross-check it against the response headers. Advertise the same
  DLNA.ORG_OP/FLAGS the media server sends.
- HQPlayer clears its transport at natural end-of-track, so a bare Play
  (manual button or late auto-advance) faulted 702 "No contents" or
  no-op'd. play() now pre-checks an idle transport (STOPPED/
  NO_MEDIA_PRESENT — PAUSED untouched so resume keeps position) and
  re-asserts the last SetAVTransportURI first, plus a bounded retry net
  (6 attempts, linear backoff) for raw-status 701/702 settle races.

Based on the DLNA hardening contributed in PR #547; retry-net comment
corrected against rupnp 3.0's actual error mapping (non-200 becomes
HttpErrorCode before any SOAP parse — the UPnPError arm only covers
fault-inside-200).

cc
2026-07-05 15:05:16 -06:00
vicrodh 691918e267 fix(cast): serve DLNA media strict-renderer compatible
Strict DLNA renderers (e.g. Signalyst HQPlayer 6) HEAD-probe the media
URL before playing and reject chunked bodies: tiny_http auto-selected
Transfer-Encoding: chunked (dropping Content-Length) past its 32 KiB
threshold, so those renderers played the buffered seconds and went
STOPPED. Answer HEAD with 200 + Content-Type/Accept-Ranges/DLNA
headers, pin Identity encoding with an explicit Content-Length on all
media responses (200/206/HEAD), and advertise transferMode/
contentFeatures on GET too. Range handling is unchanged. Neutral for
renderers that never HEAD-probe and ignore the extra headers.

Based on the DLNA hardening contributed in PR #547.

cc
2026-07-05 15:04:30 -06:00
vicrodh d525e4670a fix(cast): install rustls CryptoProvider before TLS connect
rust_cast opens a rustls TLS channel to the Chromecast. In the full binary both
the aws-lc-rs and ring providers are enabled (reqwest pulls ring, rust_cast pulls
aws-lc-rs), so rustls can't auto-select one and the worker thread panicked with
"Could not automatically determine the process-level CryptoProvider" — surfacing
as "Connection error: Thread response error".

Add qbz_cast::ensure_crypto_provider() (installs aws-lc-rs once, idempotent) and
call it at the top of ChromecastHandle::connect and DlnaConnection::connect.

cc
2026-06-18 12:02:44 -06:00
vicrodh 8dc5f9c5c5 feat(cast): qbz-cast crate hardening for the Slint port (Phase 1)
Additive, backward-compatible enhancements to the shared cast crate (the
Tauri shims are pure re-exports, so existing callers are unaffected).

- DLNA timeouts: pause/stop/seek/set_volume now run through a shared
  run_action() with a 10s timeout (was none) so a hung renderer can't block
  the caller forever. New DlnaError::Timeout / CastError::Timeout variants.
- DLNA mute: add set_mute / get_mute (RenderingControl SetMute/GetMute,
  Master channel) — was missing alongside set_volume.
- DLNA capability detection is now version-agnostic (matches AVTransport:2/:3,
  not only :1) so newer renderers report has_av_transport correctly.
- Media server endpoint auth: served path is now /audio/<token>/<id> with a
  128-bit per-session token (RandomState-seeded, no new dep). Token lives in
  the PATH, not a query string, so DLNA renderers that strip queries still
  authenticate. Mismatch -> 403.
- register_file content-type parity: content_type_for_path now mirrors the
  rich local-cast MIME map (audio/x-ape, mp3/ogg/opus/aac, m4a/alac->audio/mp4)
  instead of the poorer table; add register_file_with_content_type so the
  caller can supply an exact MIME. Local files stream from disk (no full-RAM
  read) per decision D4.

Unit-tested: tokened-path parsing, rich MIME map, token randomness.

cc
2026-06-18 09:25:39 -06:00
vicrodh bfcd769055 fix(cast): heartbeat the Chromecast every 5s to keep the connection alive
Google Cast closes a control connection that goes roughly 10 seconds without a heartbeat PING. QBZ pinged every 25 seconds, so any cast connection left idle between connect and load — for example while the track downloads, or across a delayed second play attempt — was dropped by the receiver. The next LOAD then failed with EPIPE (broken pipe): the device chimed on connect but never played audio, and the heartbeat thread logged repeating broken-pipe warnings.

Pinging every 5s is the cadence the Cast protocol expects and keeps the connection alive through those idle gaps.

Fixes #439.

cc
2026-05-16 14:38:29 -06:00
vicrodh edededcf8c chore(cast): remove AirPlay subsystem (dead code)
AirPlay was scaffolded in both crates/qbz-cast/src/airplay and
src-tauri/src/cast/airplay but never worked at runtime: the RAOP
sender required an Apple MFi-licensed stack with no free/open
alternative, and the backend state was already disabled via the
commented `.manage(airplay_state)` line in src-tauri/src/lib.rs.
The CastPicker UI already hid the AirPlay tab pending streaming
support that was never going to ship.

Delete:
- crates/qbz-cast/src/airplay/        (entire directory)
- src-tauri/src/cast/airplay/         (entire directory)
- 10x v2_airplay_* commands from commands_v2/library.rs + their
  tauri::generate_handler entries
- AirPlayError from qbz-cast errors
- 'airplay' from CastProtocol + every case arm in castStore.ts,
  CastPicker.svelte, AudioOutputBadges.svelte
- Corresponding i18n keys if present

Zero behavior change for Chromecast / DLNA / QConnect / MPRIS. If
AirPlay ever becomes feasible we'll revisit from scratch with a
real streaming backend.

cc
2026-04-16 19:04:56 -06:00
Afonso Jorge Ramos 9823dfb680 fix(macos): enable x86_64 cross-compilation
- Upgrade rust_cast 0.18.1 → 0.21 (switches from openssl to rustls,
  removing the openssl-sys dependency that blocked x86_64 cross-compile)
- Split mupdf sys-lib features to Linux-only (on macOS there are no GTK
  symbol conflicts, and system libs don't support x86_64 cross-compile
  from Apple Silicon)
- Update release-macos workflow to build both aarch64 and x86_64 DMGs
  using a matrix strategy
2026-04-01 02:40:55 +02:00
vicrodh de29f1e2e0 chore(qconnect): checkpoint current poc state
cx
2026-03-13 17:28:05 -06:00
vicrodh 63994c0a20 refactor(cast): extract qbz-cast crate
Extract casting functionality (Chromecast, AirPlay, DLNA) to standalone
qbz-cast crate. This crate is Tauri-agnostic and can be used by TUI/CLI
clients.

- Move device connection, discovery, threading, and media server to crate
- Keep only Tauri commands in src-tauri/src/cast/
- Thin re-exports in src-tauri for backwards compatibility
- Remove duplicate dependencies from src-tauri Cargo.toml
2026-02-15 14:51:06 -06:00