EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

README.md

211 lines · 14.0 KB

Changed in: 0.5.0

1# Earhorn Music 0.5.0
2
3**A UPnP/DLNA music player for Android TV, phones, Android Auto and the GNU/Linux desktop.** Point it at the server already running on your NAS, browse and queue from the remote, your phone or your desk, and play through the speakers you already own: no cloud account, no second library to maintain, no telemetry.
4
5[**Website**](https://faveat.software/earhorn/) · [**Download**](https://faveat.software/earhorn/#download) · [**Browse the code**](https://faveat.software/earhorn/browse/) · [**All releases**](https://faveat.software/earhorn/browse/history.html) · [**Tip jar**](https://ko-fi.com/faveat)
6
7License: GPLv3+ · Android 8.0 (API 26) and newer · Kotlin 2.0 · Screenshots are on the [website](https://faveat.software/earhorn/).
8
9## What's in this zip
10
11| Folder | What it is |
12|---|---|
13| `android/` | The Android app (TV, phones, tablets, ChromeOS, Android Auto): a complete Gradle project with its wrapper. The app module is `android/app/`. |
14| `desktop/` | The GNU/Linux desktop app: a Kotlin Multiplatform project with a `shared/` module and the `desktop/` UI module. It has its own [README](desktop/README.md). |
15| `LICENSE` | The GNU General Public License, version 3. |
16
17The ready-built packages (two Android APKs, an AppImage and a .deb) are separate downloads on the [website](https://faveat.software/earhorn/#download).
18
19## Why Earhorn?
20
21Every UPnP server already tags your music properly: Composer, Work, Performer, Ensemble, Conductor. Most players flatten that into "Artist / Album / Track" and throw the rest away. Earhorn renders what the server sends, faithfully.
22
23- **One APK, three surfaces.** Android TV (D-pad, Leanback launcher), phone (compact touch layout) and Android Auto (Car App Library). Same codebase, same queue, same MediaSession. The desktop app brings the same library to GNU/Linux.
24- **No cloud, no account, no sync.** UPnP/DLNA discovery on your local network. Your server, your speakers, your files.
25- **Cast to speaker groups.** The `play` flavor bundles Google Cast: send audio to a Chromecast, Nest Audio or a speaker group. The queue loads on the receiver, so tracks advance without the phone. You can also follow a Cast device another app is playing to, and control it.
26- **Device-local audio.** No server? Browse and play the files on the device itself via MediaStore. Same UI, same queue, same transport.
27- **Classical-listener defaults.** 28 configurable metadata fields. The defaults surface Composer, Conductor, Work, Performer, Ensemble, sample rate and bit depth. Toggle what you care about.
28- **Gapless playback, ReplayGain.** Media3 `setMediaItems()` queues the whole album for gapless transitions. ReplayGain modes: off, track, album, with adjustable pre-amp.
29- **Zero telemetry.** No analytics, no crash reporting. What goes over the network: UPnP/DLNA discovery on your LAN and your server's streams and album art; Chromecast discovery and the Cast devices you play to or follow; and time-synced lyrics lookups to lrclib.net (the track's title, artist, album and length), which you can switch off in Settings.
30
31## Building the Android app
32
33Needs JDK 17 and the Android SDK with platform 35 (Android Studio Ladybug or newer installs both). Tell Gradle where the SDK is, either with `ANDROID_HOME` or a `local.properties` file in `android/` containing `sdk.dir=` and the SDK's path.
34
35```bash
36cd android
37./gradlew :app:assembleLibreRelease # without Google Play Services
38./gradlew :app:assemblePlayRelease # with Google Cast
39```
40
41APKs land in `android/app/build/outputs/apk/<flavor>/release/`. Release builds are signed with your machine's debug key (`~/.android/debug.keystore`), so they install for testing; sign with your own key before distributing. For debug builds, use `assembleLibreDebug` or `assemblePlayDebug`.
42
43Sideload to an Android TV device:
44
45```bash
46adb connect <tv-ip>:5555
47adb install -r app/build/outputs/apk/libre/release/app-libre-release.apk
48```
49
50### Your own build values (optional)
51
52Two features take an address on your own network: the station overlay (a musicnow `/status` address) and the default address of the web control panel in Settings. Public builds leave both empty, which turns those features off. For your own builds, put them in `android/app/earhorn-private.properties`:
53
54```properties
55earhorn.musicnowStatusUrl=http://<host>:8642/status
56earhorn.domEthaDefaultUrl=http://<host>/mediacontrol.html
57```
58
59Never commit or publish that file. To build without it even when it's there, add `-Pearhorn.public` to the Gradle command.
60
61## Building the desktop app
62
63Needs JDK 17 (newer JDKs aren't supported by this Gradle and Kotlin) and libvlc. On Debian and Ubuntu: `sudo apt install openjdk-17-jdk vlc dpkg-dev`.
64
65```bash
66cd desktop
67./gradlew :desktop:run # run from source
68./gradlew :desktop:packageDeb :desktop:packageAppImage # build the .deb and AppImage
69```
70
71See [desktop/README.md](desktop/README.md) for the desktop app's features and settings.
72
73## Distribution
74
75| Target | Flavor | Notes |
76|--------|--------|-------|
77| Google Play (TV, phone, tablet, ChromeOS) | `play` | Bundles Google Cast |
78| F-Droid / Fire TV / GrapheneOS | `libre` | Zero GMS dependencies |
79| Android Auto | both | Car App Library `CarAppService` with full browse tree |
80| GNU/Linux | `desktop/` | Compose Multiplatform, .deb and AppImage |
81
82Package namespace: `dev.stave`. Application IDs: `dev.stave` (play), `dev.stave.libre` (libre).
83
84## What works (Android)
85
86- **UPnP/DLNA discovery.** SSDP via jUPnP 3.0.3. `NetworkInterfaceResolver` binds to the real LAN interface so VPN clients (NetGuard, ProtonVPN, Tailscale) don't break discovery.
87- **Browse.** ContentDirectory `Browse(DIRECT_CHILDREN)` for any container. MinimServer's `indexTags` structure renders faithfully; synthetic entries (`>> Show All`, `[folder view]`, `[untagged]`) render in distinct italic sage-green typography.
88- **Playback.** Media3 ExoPlayer hosted by a `MediaLibraryService`. Hardware media keys, Android Auto transport and lock-screen controls all bind to the same session. FLAC, MP3, WAV, AAC, ALAC.
89- **Cast sender (play flavor).** `CastIntegration` with queue loading, a stream proxy (`CastStreamProxy`), device volume control and disconnect handling. Queue navigation via `queueNext`/`queuePrev`/`queueJumpTo`, so receiver-side track advancement works.
90- **Cast follow.** Observe a Cast device that another app is playing to: its track, art and queue, with transport and volume control.
91- **Android Auto.** Five-file `cartemplate` module: `EarhornCarAppService`, `EarhornCarSession`, `CarSessionMediaBridge`, `NowPlayingCarScreen`, `AlbumDetailCarScreen`.
92- **Album art.** Coil 3 with animated GIF/WebP support, fetched from `upnp:albumArtURI`. 256 MB disk cache. Now Playing renders art full-bleed edge-to-edge at 1:1 aspect ratio with no cropping.
93- **Device-local audio.** `LocalMediaScanner` indexes on-device audio via MediaStore. Browsable by artist, album, genre, song.
94- **Queue.** Persistent save/restore via DataStore. Load/save as M3U playlists.
95- **Search.** Cross-server ContentDirectory search with history persistence. Voice search with system IME fallback.
96- **Lyrics.** Time-synced lyrics fetched from lrclib.net; falls back to server-side sibling `.lrc` files. Rendered in Inter (sans) with line-by-line scroll.
97- **Album notes.** `AlbumInfoLoader` reads ID3 COMMENT / Vorbis DESCRIPTION from the first track, falls back to sibling `.txt` / `review.txt` / `info.txt` files in the same UPnP directory.
98- **Random picker.** Shuffle by Albums or Tracks from the active server or local library.
99- **Sleep timer.** Standard intervals.
100- **Album disambiguation.** "More from this album" filters by both album title and artist, so "Greatest Hits" by Queen doesn't pull in "Greatest Hits" by Fleetwood Mac.
101
102## Architecture (Android)
103
104One app module (`android/app/`), two product flavors (`play`, `libre`) sharing one `main` source set. 101 Kotlin source files in `main`, 3 in `play` (Cast), 1 in `libre` (Cast stub).
105
106```
107dev/stave/
108 MainActivity.kt — single activity
109 EarhornApp.kt — Application class; Hilt entry point
110 ui/
111 theme/ — Color / Type / Shape / Theme + WindowSize helpers
112 nav/ — StaveNavHost — top-level navigation graph
113 servers/ — Server list + "On this device" (compact only)
114 browse/ — Container browsing, album grid, synthetic entries
115 detail/ — Album view: art + metadata + track list
116 nowplaying/ — Full player + persistent bottom bar
117 queue/ — Queue management
118 search/ — Cross-server + local search
119 local/ — Device-local audio hub, albums, artists, songs
120 playlist/ — M3U playlist read/write
121 lyrics/ — LRC parsing + synchronized display
122 settings/ — Metadata field toggles, playback settings
123 about/ — About screen
124 welcome/ — First-run onboarding
125 guide/ — In-app help
126 more/ — "More tracks from..." overflow
127 random/ — Shuffle-by-tag picker
128 metadata/ — Per-track metadata sheet
129 cast/ — Cast device picker
130 dometha/ — Web control panel
131 components/ — StaveFocusable, Marquee, ActionSheet, AlbumArt,
132 OnScreenKeyboard, dialogs, TvLongPress
133 cartemplate/ — Android Auto integration
134 domain/model/ — MediaServer, DidlItem, MetadataField
135 domain/renderer/ — Renderer interface + PlaybackState
136 upnp/ — jUPnP wrappers, ContentDirectoryClient, DidlParser
137 playback/ — PlaybackService, LocalRenderer, CoilBitmapLoader
138 data/ — DataStore settings, queue persistence, search history
139 local/ — LocalMediaScanner
140 lyrics/ — LrcParser, LyricsData, LyricsRepository
141 playlist/ — M3uPlaylistStore
142 di/ — Hilt modules
143 cast/ — Raw Cast protocol (follow mode) and the station overlay;
144 flavor-specific: play (CastIntegration, CastStreamProxy,
145 EarhornCastOptionsProvider) vs libre (stub)
146```
147
148### Layout dispatch
149
150Screens use `isCompactWidth()` from `WindowSize.kt` to branch between TV and phone layouts within the same composable. All code paths resolve typography through `staveTypography()`: Fraunces for display/headline/title, Inter for body/label.
151
152### Font architecture
153
154Fraunces (serif, variable, opsz 144) for display, headline and title scales. Inter (sans) for body and label scales. The on-screen keyboard renders all characters in Fraunces except numerals, which use Inter. `withInterDigits()` in `Type.kt` wraps any string so that digit characters (0–9) render in Inter regardless of the parent typography scale, applied at every call site where dynamic text touches a serif scale.
155
156### Theme
157
158"Quoyle": amber/sage/rose on warm charcoal. 2dp amber focus border with soft amber backplate on TV. Serif display (Fraunces) + sans chrome (Inter). Numerals always Inter.
159
160## Dependencies (Android)
161
162Versions are set in `android/gradle/libs.versions.toml`.
163
164| Library | Version | Purpose |
165|---------|---------|---------|
166| Compose BOM | 2024.11.00 | UI framework |
167| androidx.tv | foundation 1.0.0-alpha11, material 1.0.0 | TV-native Compose |
168| Media3 | 1.5.0 | ExoPlayer + MediaLibraryService |
169| jUPnP | 3.0.3 | UPnP/DLNA discovery + ContentDirectory |
170| Jetty | 9.4.48 | HTTP server for jUPnP |
171| Coil | 3.0.2 | Image loading (compose, okhttp, gif) |
172| OkHttp | 4.12.0 | HTTP |
173| Hilt | 2.52 | Dependency injection |
174| Room | 2.6.1 | Saved servers |
175| DataStore | 1.1.1 | Preferences persistence |
176| Car App Library | 1.7.0-rc01 | Android Auto |
177| Cast framework | 22.1.0 | Google Cast sender (play only) |
178| MediaRouter | 1.7.0 | Cast device picker (play only) |
179| Kotlin | 2.0.21 | Language |
180| AGP | 8.7.3 | Build system |
181| Gradle | 8.11.1 | Build tool (wrapper, checksum-pinned) |
182
183compileSdk 35, minSdk 26, targetSdk 35.
184
185## Known debugging territory
186
187**Multicast.** Some routers block SSDP multicast (IGMP snooping, guest VLANs, mesh systems). Confirm server and device are on the same subnet. `WAKE_LOCK` is required by jUPnP's `AndroidRouter` to hold a `WifiLock`.
188
189**VPN interference.** `NetworkInterfaceResolver` pins jUPnP's multicast to the real LAN interface so a VPN `tun` device doesn't capture SSDP.
190
191**ExoPlayer + 24/192 FLAC.** Software decoder works; hardware offload varies by TV chipset. Check `logcat | grep ExoPlayer`.
192
193**Resource shrinking.** Disabled (`isShrinkResources = false`). AGP's resource shrinker has historically replaced the launcher icon with a surviving drawable when references are manifest-only.
194
195**ChromeOS.** Same Play Store APK installs via ARC. Multicast and SSDP work through ARC's bridged network.
196
197## Documentation
198
199- `android/DESIGN.md`: product spec, UX, screen-by-screen treatment, MinimServer integration model
200- `android/FONTS.md`: Fraunces / Inter installation notes
201- `desktop/README.md` and the other documents in `desktop/`: the desktop app
202
203## License
204
205GNU General Public License, version 3 or later (GPLv3+). See [LICENSE](LICENSE).
206
207Copyright (C) 2026 Patrick T. Lafferty.
208
209A [Faveat Software](https://faveat.software) project. Faveat Software deploys exclusively under GPLv3+ to ensure that technical labour remains a public utility, immune to proprietary enclosure.
210
211If Earhorn Music is useful to you, [say thanks on Ko-fi](https://ko-fi.com/faveat).

As a page

Earhorn Music 0.5.0

A UPnP/DLNA music player for Android TV, phones, Android Auto and the GNU/Linux desktop. Point it at the server already running on your NAS, browse and queue from the remote, your phone or your desk, and play through the speakers you already own: no cloud account, no second library to maintain, no telemetry.

Website · Download · Browse the code · All releases · Tip jar

License: GPLv3+ · Android 8.0 (API 26) and newer · Kotlin 2.0 · Screenshots are on the website.

What's in this zip

FolderWhat it is
android/The Android app (TV, phones, tablets, ChromeOS, Android Auto): a complete Gradle project with its wrapper. The app module is android/app/.
desktop/The GNU/Linux desktop app: a Kotlin Multiplatform project with a shared/ module and the desktop/ UI module. It has its own README.
LICENSEThe GNU General Public License, version 3.

The ready-built packages (two Android APKs, an AppImage and a .deb) are separate downloads on the website.

Why Earhorn?

Every UPnP server already tags your music properly: Composer, Work, Performer, Ensemble, Conductor. Most players flatten that into "Artist / Album / Track" and throw the rest away. Earhorn renders what the server sends, faithfully.

  • One APK, three surfaces. Android TV (D-pad, Leanback launcher), phone (compact touch layout) and Android Auto (Car App Library). Same codebase, same queue, same MediaSession. The desktop app brings the same library to GNU/Linux.
  • No cloud, no account, no sync. UPnP/DLNA discovery on your local network. Your server, your speakers, your files.
  • Cast to speaker groups. The play flavor bundles Google Cast: send audio to a Chromecast, Nest Audio or a speaker group. The queue loads on the receiver, so tracks advance without the phone. You can also follow a Cast device another app is playing to, and control it.
  • Device-local audio. No server? Browse and play the files on the device itself via MediaStore. Same UI, same queue, same transport.
  • Classical-listener defaults. 28 configurable metadata fields. The defaults surface Composer, Conductor, Work, Performer, Ensemble, sample rate and bit depth. Toggle what you care about.
  • Gapless playback, ReplayGain. Media3 setMediaItems() queues the whole album for gapless transitions. ReplayGain modes: off, track, album, with adjustable pre-amp.
  • Zero telemetry. No analytics, no crash reporting. What goes over the network: UPnP/DLNA discovery on your LAN and your server's streams and album art; Chromecast discovery and the Cast devices you play to or follow; and time-synced lyrics lookups to lrclib.net (the track's title, artist, album and length), which you can switch off in Settings.

Building the Android app

Needs JDK 17 and the Android SDK with platform 35 (Android Studio Ladybug or newer installs both). Tell Gradle where the SDK is, either with ANDROID_HOME or a local.properties file in android/ containing sdk.dir= and the SDK's path.

cd android
./gradlew :app:assembleLibreRelease     # without Google Play Services
./gradlew :app:assemblePlayRelease      # with Google Cast

APKs land in android/app/build/outputs/apk/<flavor>/release/. Release builds are signed with your machine's debug key (~/.android/debug.keystore), so they install for testing; sign with your own key before distributing. For debug builds, use assembleLibreDebug or assemblePlayDebug.

Sideload to an Android TV device:

adb connect <tv-ip>:5555
adb install -r app/build/outputs/apk/libre/release/app-libre-release.apk

Your own build values (optional)

Two features take an address on your own network: the station overlay (a musicnow /status address) and the default address of the web control panel in Settings. Public builds leave both empty, which turns those features off. For your own builds, put them in android/app/earhorn-private.properties:

earhorn.musicnowStatusUrl=http://<host>:8642/status
earhorn.domEthaDefaultUrl=http://<host>/mediacontrol.html

Never commit or publish that file. To build without it even when it's there, add -Pearhorn.public to the Gradle command.

Building the desktop app

Needs JDK 17 (newer JDKs aren't supported by this Gradle and Kotlin) and libvlc. On Debian and Ubuntu: sudo apt install openjdk-17-jdk vlc dpkg-dev.

cd desktop
./gradlew :desktop:run                                     # run from source
./gradlew :desktop:packageDeb :desktop:packageAppImage     # build the .deb and AppImage

See desktop/README.md for the desktop app's features and settings.

Distribution

TargetFlavorNotes
Google Play (TV, phone, tablet, ChromeOS)playBundles Google Cast
F-Droid / Fire TV / GrapheneOSlibreZero GMS dependencies
Android AutobothCar App Library CarAppService with full browse tree
GNU/Linuxdesktop/Compose Multiplatform, .deb and AppImage

Package namespace: dev.stave. Application IDs: dev.stave (play), dev.stave.libre (libre).

What works (Android)

  • UPnP/DLNA discovery. SSDP via jUPnP 3.0.3. NetworkInterfaceResolver binds to the real LAN interface so VPN clients (NetGuard, ProtonVPN, Tailscale) don't break discovery.
  • Browse. ContentDirectory Browse(DIRECT_CHILDREN) for any container. MinimServer's indexTags structure renders faithfully; synthetic entries (>> Show All, [folder view], [untagged]) render in distinct italic sage-green typography.
  • Playback. Media3 ExoPlayer hosted by a MediaLibraryService. Hardware media keys, Android Auto transport and lock-screen controls all bind to the same session. FLAC, MP3, WAV, AAC, ALAC.
  • Cast sender (play flavor). CastIntegration with queue loading, a stream proxy (CastStreamProxy), device volume control and disconnect handling. Queue navigation via queueNext/queuePrev/queueJumpTo, so receiver-side track advancement works.
  • Cast follow. Observe a Cast device that another app is playing to: its track, art and queue, with transport and volume control.
  • Android Auto. Five-file cartemplate module: EarhornCarAppService, EarhornCarSession, CarSessionMediaBridge, NowPlayingCarScreen, AlbumDetailCarScreen.
  • Album art. Coil 3 with animated GIF/WebP support, fetched from upnp:albumArtURI. 256 MB disk cache. Now Playing renders art full-bleed edge-to-edge at 1:1 aspect ratio with no cropping.
  • Device-local audio. LocalMediaScanner indexes on-device audio via MediaStore. Browsable by artist, album, genre, song.
  • Queue. Persistent save/restore via DataStore. Load/save as M3U playlists.
  • Search. Cross-server ContentDirectory search with history persistence. Voice search with system IME fallback.
  • Lyrics. Time-synced lyrics fetched from lrclib.net; falls back to server-side sibling .lrc files. Rendered in Inter (sans) with line-by-line scroll.
  • Album notes. AlbumInfoLoader reads ID3 COMMENT / Vorbis DESCRIPTION from the first track, falls back to sibling .txt / review.txt / info.txt files in the same UPnP directory.
  • Random picker. Shuffle by Albums or Tracks from the active server or local library.
  • Sleep timer. Standard intervals.
  • Album disambiguation. "More from this album" filters by both album title and artist, so "Greatest Hits" by Queen doesn't pull in "Greatest Hits" by Fleetwood Mac.

Architecture (Android)

One app module (android/app/), two product flavors (play, libre) sharing one main source set. 101 Kotlin source files in main, 3 in play (Cast), 1 in libre (Cast stub).

dev/stave/
  MainActivity.kt               — single activity
  EarhornApp.kt                  — Application class; Hilt entry point
  ui/
    theme/                       — Color / Type / Shape / Theme + WindowSize helpers
    nav/                         — StaveNavHost — top-level navigation graph
    servers/                     — Server list + "On this device" (compact only)
    browse/                      — Container browsing, album grid, synthetic entries
    detail/                      — Album view: art + metadata + track list
    nowplaying/                  — Full player + persistent bottom bar
    queue/                       — Queue management
    search/                      — Cross-server + local search
    local/                       — Device-local audio hub, albums, artists, songs
    playlist/                    — M3U playlist read/write
    lyrics/                      — LRC parsing + synchronized display
    settings/                    — Metadata field toggles, playback settings
    about/                       — About screen
    welcome/                     — First-run onboarding
    guide/                       — In-app help
    more/                        — "More tracks from..." overflow
    random/                      — Shuffle-by-tag picker
    metadata/                    — Per-track metadata sheet
    cast/                        — Cast device picker
    dometha/                     — Web control panel
    components/                  — StaveFocusable, Marquee, ActionSheet, AlbumArt,
                                   OnScreenKeyboard, dialogs, TvLongPress
  cartemplate/                   — Android Auto integration
  domain/model/                  — MediaServer, DidlItem, MetadataField
  domain/renderer/               — Renderer interface + PlaybackState
  upnp/                          — jUPnP wrappers, ContentDirectoryClient, DidlParser
  playback/                      — PlaybackService, LocalRenderer, CoilBitmapLoader
  data/                          — DataStore settings, queue persistence, search history
  local/                         — LocalMediaScanner
  lyrics/                        — LrcParser, LyricsData, LyricsRepository
  playlist/                      — M3uPlaylistStore
  di/                            — Hilt modules
  cast/                          — Raw Cast protocol (follow mode) and the station overlay;
                                   flavor-specific: play (CastIntegration, CastStreamProxy,
                                   EarhornCastOptionsProvider) vs libre (stub)

Layout dispatch

Screens use isCompactWidth() from WindowSize.kt to branch between TV and phone layouts within the same composable. All code paths resolve typography through staveTypography(): Fraunces for display/headline/title, Inter for body/label.

Font architecture

Fraunces (serif, variable, opsz 144) for display, headline and title scales. Inter (sans) for body and label scales. The on-screen keyboard renders all characters in Fraunces except numerals, which use Inter. withInterDigits() in Type.kt wraps any string so that digit characters (0–9) render in Inter regardless of the parent typography scale, applied at every call site where dynamic text touches a serif scale.

Theme

"Quoyle": amber/sage/rose on warm charcoal. 2dp amber focus border with soft amber backplate on TV. Serif display (Fraunces) + sans chrome (Inter). Numerals always Inter.

Dependencies (Android)

Versions are set in android/gradle/libs.versions.toml.

LibraryVersionPurpose
Compose BOM2024.11.00UI framework
androidx.tvfoundation 1.0.0-alpha11, material 1.0.0TV-native Compose
Media31.5.0ExoPlayer + MediaLibraryService
jUPnP3.0.3UPnP/DLNA discovery + ContentDirectory
Jetty9.4.48HTTP server for jUPnP
Coil3.0.2Image loading (compose, okhttp, gif)
OkHttp4.12.0HTTP
Hilt2.52Dependency injection
Room2.6.1Saved servers
DataStore1.1.1Preferences persistence
Car App Library1.7.0-rc01Android Auto
Cast framework22.1.0Google Cast sender (play only)
MediaRouter1.7.0Cast device picker (play only)
Kotlin2.0.21Language
AGP8.7.3Build system
Gradle8.11.1Build tool (wrapper, checksum-pinned)

compileSdk 35, minSdk 26, targetSdk 35.

Known debugging territory

Multicast. Some routers block SSDP multicast (IGMP snooping, guest VLANs, mesh systems). Confirm server and device are on the same subnet. WAKE_LOCK is required by jUPnP's AndroidRouter to hold a WifiLock.

VPN interference. NetworkInterfaceResolver pins jUPnP's multicast to the real LAN interface so a VPN tun device doesn't capture SSDP.

ExoPlayer + 24/192 FLAC. Software decoder works; hardware offload varies by TV chipset. Check logcat | grep ExoPlayer.

Resource shrinking. Disabled (isShrinkResources = false). AGP's resource shrinker has historically replaced the launcher icon with a surviving drawable when references are manifest-only.

ChromeOS. Same Play Store APK installs via ARC. Multicast and SSDP work through ARC's bridged network.

Documentation

  • android/DESIGN.md: product spec, UX, screen-by-screen treatment, MinimServer integration model
  • android/FONTS.md: Fraunces / Inter installation notes
  • desktop/README.md and the other documents in desktop/: the desktop app

License

GNU General Public License, version 3 or later (GPLv3+). See LICENSE.

Copyright (C) 2026 Patrick T. Lafferty.

A Faveat Software project. Faveat Software deploys exclusively under GPLv3+ to ensure that technical labour remains a public utility, immune to proprietary enclosure.

If Earhorn Music is useful to you, say thanks on Ko-fi.