Skip to content

Integrate AirPlay 2 receiver into the launcher - #170

Draft
rudysev wants to merge 1 commit into
starbrightlab:mainfrom
rudysev:airplay-integration
Draft

Integrate AirPlay 2 receiver into the launcher#170
rudysev wants to merge 1 commit into
starbrightlab:mainfrom
rudysev:airplay-integration

Conversation

@rudysev

@rudysev rudysev commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Hey @starbrightlab ,

This PR implements a new feature: AirPlay 2 receiver — audio streaming, screen mirroring, and AirPlay video (HLS handoff, so a cast of e.g. YouTube hands over a video URL rather than mirroring pixels) for iOS/iPadOS/macOS devices. The Portal appears in Control Center like an Apple TV or HomePod, with optional PIN pairing and now-playing / transport control (DACP) from the sender.

It was tested mirroring Spotify and YouTube on Portal Gen2 and Portal+ Gen1. It relies on android-airplay-server (https://github.com/jqssun/android-airplay-server), vendored under airplay/ with a single local patch and a sync script (airplay/UPSTREAM.md documents the vendor line).

It is tested to work with iOS but is a large feature, which affects:

  • A new :airplay Gradle module bundling a native stack (UxPlay + OpenSSL + libplist + FFmpeg via JNI/CMake, arm64-only) and its four submodules — so the build now needs the NDK to configure.
  • App-wide dependency bumps to satisfy the module: media3 1.5.1 → 1.11.0-beta01 and androidx.core 1.10.1 → 1.15.0, plus lifecycle/coroutines. media3 is shared with the screensaver's VideoTranscoder, so that path moves onto the newer (beta) media3 too — worth a look on real hardware.
  • A new foreground service + launcher surfaces (AirPlayActivity, pairing flow), wired in the same "opt-in from a pairing screen" style as the phone remote.
  • Speaker / multi-room ownership — a live AirPlay session takes the speakers and hands them back, so it coordinates with the Snapcast multi-room relay.

Care should be taken with the licensing — you have an MIT License but this uses GNU GPL-3.0 (the vendored FairPlay/UxPlay native code), and I don't want to push a licensing change to you. There's a tripwire comment in airplay/build.gradle.kts about not cutting a public self-updating release that bundles it; it is not enforced in the build, so that would need a real gate before any release includes :airplay.

I have further built this feature into a standalone application, but it really belongs in Immortal, similar to remote control via phone.

Please take your time to review this work and decide if and/or how you want to port or land it.

Add the :airplay module (vendored from jqssun/android-airplay-server) and
wire it into Immortal as a native feature: audio streaming, screen mirroring,
and AirPlay video from an iPhone/iPad/Mac.

Host integration (com.immortal.launcher):
- AirPlayConfig: SharedPreferences config mapped onto the module's AirPlayOptions.
  No server-name field — the Portal advertises under FleetConfig.name, the same
  name the phone remote and Home Assistant use.
- AirPlayControl: runtime lifecycle. ensureRunning() from ImmortalApp/BootReceiver,
  applyConfig() from the settings hook, and a SessionWatcher that raises the cast
  surface only for video (audio-only stays headless) and makes AirPlay audio and
  multi-room mutually exclusive.
- AirPlayActivity: full-screen cast surface. One SurfaceView per renderer, swapped
  at the mirror->video handover; letterboxes by scaling (not resizing) so the
  decoder is never disturbed mid-stream.
- AirPlayPairActivity + the `airplay` SettingsDomain: one declarative definition
  drives the on-device screen and the phone remote. Home-header glyph + modal
  mirror the phone-remote quick-connect.

Module facade fix: AirPlayEngine.reconfigure restarts via start() (ACTION_START_
SERVER) rather than service.startServer(), which ordered startForegroundService
after startForeground and got the process killed on every settings change.

Build: catalog gains media3 1.11.0-beta01 (shared with VideoTranscoder), oboe,
lifecycle-service, androidx.media; app declares ndkVersion so AGP strips the
packaged .so files. CI installs the NDK (AGP resolves it while configuring the
module). Four native deps are submodules at upstream's pins.

Verified on gen-1 Portal+ (aloha, Android 9) and gen-2 Portal 10" (omni,
Android 10): Spotify audio, YouTube mirroring, and the mirror->video handover.

Not for release: the native library links GPL-3.0 code, so any APK bundling
:airplay is GPL-3.0 as a whole while Immortal is MIT. See the TODO(licensing)
tripwire in airplay/build.gradle.kts. Dev/debug builds only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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