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
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
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
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
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
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
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
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
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
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
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
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
- 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
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