Skip to content

Add experimental --no-audio-advertise option (narrow AirPlay advertisement) - #534

Closed
erseco wants to merge 2 commits into
FDH2:masterfrom
erseco:no-audio-advertise
Closed

Add experimental --no-audio-advertise option (narrow AirPlay advertisement)#534
erseco wants to merge 2 commits into
FDH2:masterfrom
erseco:no-audio-advertise

Conversation

@erseco

@erseco erseco commented Jul 3, 2026

Copy link
Copy Markdown

Summary

  • Opt-in, experimental --no-audio-advertise [bits|raop|both] option that narrows the AirPlay/DNS-SD advertisement (clearing the feature bits claiming audio support and/or skipping _raop._tcp registration) so macOS/iOS are less likely to route audio to UxPlay during mirroring.
  • -a/-as 0 behavior is unchanged; they now emit a startup warning explaining they don't affect the advertisement.
  • Adds a small CTest-based unit test harness (none existed) covering CLI parsing, exact feature-bitmask values, and help text.
  • Adds specs/no-audio-advertisement.md and docs/manual-test-no-audio-advertise.md.

Context

Relates to #533, and to the discussion in #303, #442, #463, #489. Clearing the audio-support feature bit (bits mode) reproduces the known ~60s client TEARDOWN with no fix found — this PR documents that rather than claiming to solve it, and adds a raop-only mode (untested before) in case it behaves differently. Opening mainly for feedback given your deeper protocol knowledge here.

Test plan

Update (2026-07-04)

Scope — what this PR is, and what a complete "video-only mirroring" needs

To be upfront about how far this actually gets, after real-hardware testing:

Solid and mergeable on its own (independent of the audio-routing question):

  • The GET /info BLE-path fix (9072b77).
  • The CTest unit-test harness (none existed before) + the spec/docs.

--no-audio-advertise is an honest but incomplete advertisement lever: both keeps audio on the Mac, but the client tears the video down after ~30–90s (the #303/#442/#489 watchdog). So it is opt-in/experimental and not a standalone "video-only" mode.

A genuinely working indefinite video-only mirror is possible, but takes two more pieces beyond this PR (documented in #533; the receiver half is on a separate event-channel-experiment branch, intentionally not included here):

  1. Receiver side: start the session with audio advertised (so the ~60s watchdog never arms), then, mid-session over a newly-implemented AirPlay event channel, send a "downgraded" updateInfo (audio feature bits cleared) and withdraw _raop._tcp / clear the live _airplay audio bits. The client then tears down only its audio stream and mirrors video indefinitely (verified 240s+, no watchdog). This is new: it defeats the teardown that No obvious solution to "iOS clients do play audio on client with video mirrored on server if feature bit 9 (audio support) is disabled on server" , but close video connection after 60 secs(was:Can the audio be directed to Bluetooth headphones or remain on the mobile phone? #303/Is there a way to run UxPlay so that only video is streamed, and the iPhone leaves the audio paths alone #442/Feature request: need video only mode so that audio can be played with no latency #489 concluded was unsolvable.
  2. Client side (unavoidable): macOS does not release its audio output-device selection when the receiver drops audio — it stays pinned to the now-dead AirPlay device (silence) until something reselects the local output. No receiver-side signal changes this (feature bits, supportedFormats, statusFlags, mDNS withdrawal all tested negative), and it is not receiver-fixable: even a real Mac acting as an AirPlay receiver advertises full audio support (features=0x4A7FCFD5, _raop._tcp registered) — Apple has no "video-only" receiver mode. So a complete experience needs either a one-time manual switch of the Mac's audio output back to local, or a small macOS-side CoreAudio helper that reverts the default output when it becomes AirPlay.

In short: this PR's --no-audio-advertise + the event-channel branch get "video mirrors forever, audio stays off the receiver," but the last step (macOS keeping/returning its output to local) is inherently client-side. Happy to take any of this in whatever direction you prefer — merge just the BLE fix + tests, keep the experimental flag, or fold in the event-channel work.

erseco added 2 commits July 3, 2026 10:46
macOS/iOS may auto-select UxPlay as the system audio output during
mirroring even when -a/-as 0 is used, since that flag only disables
local server-side playback and does not change the AirPlay/DNS-SD
advertisement. Add an opt-in, experimental --no-audio-advertise
[bits|raop|both] flag that narrows the actual advertisement (clearing
the audio-support feature bits and/or skipping _raop._tcp
registration), add a startup warning when -a is used alone, and add a
CTest-based unit test harness (the project had none) covering the new
CLI parsing, the exact AirPlay feature-bitmask regression, and help
text.

Per upstream issues FDH2#303/FDH2#442/FDH2#489, clearing the audio-support feature
bit is known to cause some AirPlay clients to TEARDOWN mirroring after
~60 seconds, with no fix found so far; this is documented as a known,
unresolved risk in specs/no-audio-advertisement.md rather than claimed
as solved.
The raop TXT buffer is only built by dnssd_register_raop(), so with
--no-audio-advertise raop|both the BLE service-discovery/GET /info
response carried an empty txtRAOP data node. Skip emitting a TXT
record whose buffer was never built (length 0), keeping the GET /info
path consistent with the mDNS advertisement while preserving the
existing fallback where a failed (not skipped) _raop._tcp registration
still serves its TXT over BLE (both backends build the TXT before the
network registration step).

Verified on real hardware (Ubuntu 25.10 server, macOS Mac17,3 client):
the CSeq-less 'GET /info?txtAirPlay?txtRAOP RTSP/1.0' probe now returns
txtAirPlay only in raop/both modes.
@erseco

erseco commented Jul 4, 2026

Copy link
Copy Markdown
Author

Pushed 9072b77 addressing the BLE service-discovery gap @fduncanh pointed out in #533: raop_handler_info() served both TXT records from in-memory buffers regardless of mDNS registration, so --no-audio-advertise raop|both was returning an empty txtRAOP data node on the GET /info path. An unbuilt (length-0) TXT record is now omitted from the response; the failed-registration BLE fallback is unaffected because both mDNS backends build the TXT buffer before the network registration step. Spec and manual-test doc updated accordingly (including an nc-based wire probe for the BLE-shaped request that needs no BLE hardware).

I've also now run the manual test plan on real hardware (Ubuntu 25.10 server / macOS Mac17,3 client, srcvers 940.23.1) — full results posted in #533 (#533). Summary:

I also tested the event-channel hypothesis from #489 (full details and a significant finding in #533: a receiver-initiated "hot audio downgrade" over the event channel gets indefinite video-only mirroring — the ~60s watchdog only applies to sessions that start without audio). That experiment lives on a separate branch (https://github.com/erseco/UxPlay/tree/event-channel-experiment) and is deliberately not part of this PR.

@fduncanh
fduncanh force-pushed the master branch 2 times, most recently from 97faa76 to 9c24ed2 Compare July 30, 2026 18:15
@fduncanh

Copy link
Copy Markdown
Collaborator

@erseco

this is very interesting for experiments, but won't get added to the uxplay codebase.

It was interesting to learn that an event channel can be used modify the FEATURES bitstring stored by the client .

@fduncanh fduncanh closed this Jul 30, 2026
@erseco

erseco commented Jul 30, 2026

Copy link
Copy Markdown
Author

Thanks for taking the time to review this, and sorry I didn’t follow up sooner.

I eventually stopped pursuing this approach because, although the event-channel experiment was very interesting, it still couldn’t make macOS automatically switch back to the local audio output. For my actual use case, I ended up using a small macOS automation to select the correct audio device after mirroring starts.

Still, I learned a lot from the experiment, and I’m glad some of the findings were useful. A bit of overengineering for a small problem, but that’s part of the fun!

Please feel free to close #533 as well. Thanks again for your time and feedback.

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.

2 participants