Private
Public Access
Two causes, both fixed: - Plex keeps one file stream per device and cuts the one in progress when another starts, so the read-ahead and the player cut each other off. Whole-file fetches (read-ahead, downloads, download-first) now ask for the file as a download (?download=1), which Plex leaves out of that rule; the plain address follows for servers that refuse downloads. - Http.abort closed the socket of a response already read to the end, whose connection was back in the pool and could be carrying the read-ahead. abort now leaves finished responses alone (test/PoolAbortTest.java). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
12 KiB
Markdown
175 lines
12 KiB
Markdown
# Plexamp Lite
|
||
|
||
A small Plexamp-style music player for Plex, built for the Samsung Galaxy Mini (GT-S5570:
|
||
Android 2.2/2.3, 240×320 ldpi screen, 600 MHz ARMv6, 384 MB RAM). Runs on any Android 2.2+ phone, and comes
|
||
in a second build for Android 4.1 and later (Xperia J, Galaxy S II…) that uses what those phones can do: see
|
||
*Android 4.1 build*. Formerly Minamp (the package name, `com.minamp`, stays so updates install over it).
|
||
|
||
## Language
|
||
|
||
The interface is in **Galician** by default, whatever language the phone is set to. English is
|
||
available under *Axustes → Idioma*. All text lives in `tools/strings.py` (key → English, Galician),
|
||
which generates `res/values/strings.xml` (Galician) and `res/values-en/strings.xml`; the connection
|
||
log stays in English since it's meant for troubleshooting.
|
||
|
||
## Install
|
||
|
||
1. On the phone: **Settings → Applications → Unknown sources** (tick it).
|
||
2. Copy `PlexampLite.apk` (Android 4.1+: `PlexampLite-api16.apk`; Tenda picks the right one by itself) to the SD card (USB cable, or download it in the browser) and open it
|
||
with a file manager, or from a computer: `adb install PlexampLite.apk`.
|
||
3. Plexamp Lite installs to internal storage (about 1 MB): an app on the SD card is killed whenever the
|
||
phone shares its SD card over USB, which stopped the music.
|
||
|
||
## Sign in
|
||
|
||
Open Plexamp Lite → **Sign in with Plex**. It shows a 4-character code; on any other device go to
|
||
**plex.tv/link** and type it in. Plexamp Lite then lists your servers (owned and shared) and music libraries.
|
||
|
||
*Connect to a server address* is the manual fallback: an IP/hostname plus an optional X-Plex-Token.
|
||
|
||
## Streaming from anywhere
|
||
|
||
After sign-in Plexamp Lite knows every address Plex advertises for your server and picks the best
|
||
working one, re-checking whenever the phone switches network:
|
||
|
||
| Order | Connection | When it's used |
|
||
|---|---|---|
|
||
| 1 | Whatever worked last time | Tried alone first, so a normal start costs one request (if that was a remote address, the home addresses get 2 s first) |
|
||
| 2 | Home network, plain HTTP | On your Wi-Fi (fast: no encryption work for the phone) |
|
||
| 3 | Home network, HTTPS (`*.plex.direct`) | On your Wi-Fi if plain HTTP is refused, or if *Settings → Encrypt at home too* is on |
|
||
| 4 | Remote, HTTPS | Away, if Remote Access is set up on the server |
|
||
| 5 | Plex Relay | Away, when the server isn't reachable directly (bandwidth-capped; Plexamp Lite caps itself at 192 kbps) |
|
||
|
||
Secure addresses are tried one at a time: on a 600 MHz phone a TLS handshake takes seconds, and
|
||
several at once make each of them time out. Plexamp Lite uses TLS 1.2 with session resumption, so only
|
||
the first connection to each server pays the full cost; later ones resume in a fraction of it.
|
||
|
||
For the best remote experience enable **Settings → Remote Access** on your Plex Media Server.
|
||
Without it, Relay still works.
|
||
|
||
Android 2.x can't do modern TLS, so Plexamp Lite bundles its own TLS 1.2/1.3 stack (Bouncy Castle,
|
||
renamed to `com.minamp.bc` so it doesn't clash with the old copy inside Android) and verifies
|
||
certificates against Mozilla's root store (from `certifi`). A tiny loopback proxy feeds the
|
||
phone's media player, so HTTPS streams and relay work the same as local ones. If the phone's
|
||
clock has been reset to before 2024 the certificate date check is skipped rather than failing.
|
||
|
||
## Playback and quality
|
||
|
||
* MP3, AAC (M4A) and Ogg Vorbis play directly. FLAC, ALAC etc. are transcoded to MP3 by the
|
||
server (Android 2.2/2.3 can't decode FLAC).
|
||
* **Menu → Settings**: separate quality for home Wi-Fi (default *Original*) and for away/mobile
|
||
data (default *192 kbps MP3*); 320/256/192/128/96 kbps available. *Quality over Bluetooth*
|
||
(default 192 kbps) caps both while the sound goes to a Bluetooth speaker: on the Mini, Wi-Fi and
|
||
Bluetooth share one radio, and a lighter stream keeps the speaker from stuttering.
|
||
* Seeking in a transcoded track restarts the transcode at the new position.
|
||
* **Read-ahead:** while a track plays, the next two in the queue are fetched whole in the background
|
||
(one at a time) and then played from the file; without an SD card only the next one is kept. Plex keeps one
|
||
file stream per device and cuts the one in progress when another starts, so whole-file fetches (read-ahead,
|
||
downloads, *Download tracks before playing*) ask for the file as a download (`?download=1`), which Plex leaves
|
||
out of that rule; otherwise the read-ahead and the player cut each other off. Android 2.x's player keeps only a few seconds of stream buffer, so on weak
|
||
Wi-Fi shared with a Bluetooth speaker it used to go silent for seconds at a time.
|
||
* If a stream fails, Plexamp Lite re-picks the connection and resumes; if it fails again it downloads
|
||
the whole track first and plays from the SD card. "Download tracks before playing" forces that
|
||
mode (useful on flaky mobile data). Playback that stalls ("playing" but the time doesn't move
|
||
for 10 s) is recovered the same way.
|
||
* A Bluetooth speaker that drops its link for a moment pauses Plexamp Lite, which resumes by itself if the
|
||
speaker is back within 20 s. Why playback paused or stopped is written to the connection log,
|
||
which also goes to logcat (tag `PlexampLite`, readable over USB with `adb logcat`).
|
||
* Plays are reported to Plex (Now Playing in the Plex dashboard, play counts, resume position). For testing
|
||
against a real server, an empty file named `PlexampLite-noreport` at the top of the SD card turns that off
|
||
(`adb shell touch /sdcard/PlexampLite-noreport`; delete it afterwards).
|
||
|
||
## Features
|
||
|
||
* Home: Library radio, Loved radio (rated 4★+ / loved), Time-travel radio (pick a decade),
|
||
Random album, Recently added and Recently played shelves.
|
||
* Artists / Albums (5 sort orders) / Playlists tabs with paging and fast scroll; Search.
|
||
* Artist page with *Play all* and *Artist radio*; album and playlist pages with *Play*/*Shuffle*.
|
||
* Long-press anything: Play next, Add to queue, Go to album/artist, Love.
|
||
* Now Playing: big art on a backdrop coloured from the cover, seek bar, shuffle, repeat
|
||
(queue/track), love, sleep timer (15–90 min or end of track), where it's streaming from.
|
||
* **Lyrics**: the microphone button in Now Playing shows the track's lyrics from Plex in place of the art (as in
|
||
the Mac app). It's a lasting setting: it stays on for the following tracks and after restarts. Timed (LRC)
|
||
lyrics follow the song with the current line lit (tap a line to jump there; scrolling by hand pauses the
|
||
follow for a few seconds); plain-text lyrics just scroll. With several versions, timed ones win, else the
|
||
longest text (a local `.txt` can be a broken copy).
|
||
* **Home-screen widget** (4×1): art, title, artist, previous / play-pause / next; tap the art for Now Playing.
|
||
* Up Next queue: jump, play next, remove, clear; the queue and position survive restarts.
|
||
* Downloads for offline listening: long-press a track, album, artist or playlist -> *Download*;
|
||
*Menu -> Downloads* shows progress, space used and free, play/shuffle, retry and remove. Files go to
|
||
the SD card. MP3 originals are kept as they are (unless above a lower *Download quality* in
|
||
Settings); everything else is transcoded to MP3, 320 kbps by default. Downloaded tracks play from
|
||
the SD card, offline too, and plays made offline are reported to Plex later.
|
||
* *Menu -> Close Plexamp Lite* stops the music and closes every screen.
|
||
* Wired headset button: 1 click play/pause, 2 next, 3 previous; Bluetooth media keys;
|
||
pauses for calls and when headphones are unplugged.
|
||
|
||
## Android 4.1 build
|
||
|
||
One build per **Android level**, never per phone (the same pattern as Tenda): `build/PlexampLite.apk` for
|
||
Android 2.2+ and `build/PlexampLite-api16.apk` for 4.1+, from the same source, package and key.
|
||
`versionCode` is the manifest's × 100 + the level (15 → 1508 and 1516), so a phone that moves up a level
|
||
gets the newer build as an ordinary update, and Tenda gives each phone the build for its Android. Everything is
|
||
shared in `src/`; what only one level can do is in `platform/api8/…/Platform.java` and
|
||
`platform/api16/…/Platform.java` (the same methods; keep them in step), plus `platform/api16/res`, which
|
||
overrides a few resources. The Android 4.1 build adds:
|
||
|
||
* **ExoPlayer 2.18** (`platform/api16/…/ExoEngine.java`) instead of Android's MediaPlayer, behind the small
|
||
`Engine` interface the player service uses (`MediaPlayerEngine` on Android 2.x):
|
||
* **gapless playback**: when the next track's file is already here (read ahead or downloaded), it's queued
|
||
in the player and starts with no gap;
|
||
* a buffer of up to 3 minutes instead of seconds, so Wi-Fi gaps don't silence it;
|
||
* errors that say what failed. A track ExoPlayer can't decode is retried with Android's MediaPlayer.
|
||
Read-ahead, the stream proxy, the transcodes and the recovery steps are the same in both builds.
|
||
* A **notification** with the art and ⏮ ⏯ ⏭ × (pull it down for a larger one); swiping it away while paused stops.
|
||
* **Lock-screen controls** with the art (RemoteControlClient).
|
||
* The **widget** can be resized; two rows or taller shows a larger layout.
|
||
* Android 4's **Holo** dialogs and text fields; a ⋮ button on phones without a Menu key.
|
||
* **Metered networks**: a phone's Wi-Fi hotspot counts as away, like mobile data.
|
||
|
||
The Galaxy Mini's build is unchanged in how it plays, and also got the widget and the lyrics.
|
||
|
||
## Building (no Android SDK needed)
|
||
|
||
```
|
||
tools/setup_toolchain.sh # once: fetches aapt2, the API 8 android.jar, builds dx, renames Bouncy Castle
|
||
tools/fetch_exoplayer.sh # once: ExoPlayer 2.18.7 + Guava (checksums pinned) into ~/android-toolchain
|
||
./build.sh # -> build/PlexampLite.apk (Android 2.2+) and build/PlexampLite-api16.apk (4.1+)
|
||
./build.sh 16 # one level only
|
||
```
|
||
|
||
Also needed for the Android 4.1 build: the API 16 `android.jar` in `~/android-toolchain/platforms/android-16/`,
|
||
R8 (`d8.jar` from build-tools 34, in `~/android-toolchain/calendar-tools/`) and a JDK 11+ (`build.sh` finds
|
||
Homebrew's openjdk@17). ExoPlayer 2.18.7 is the newest that needs only Guava; R8 (`tools/r8-rules.pro`) keeps
|
||
the app whole and shrinks ExoPlayer and Guava to what it uses, in one dex.
|
||
|
||
Per level `build.sh` runs `aapt2` (resources), `javac` (Java 8 bytecode checked against that level's
|
||
`android.jar`), then for Android 2.x the Dalvik 2.x link check and `dx` (dex 035), for 4.1 R8; then `tools/signapk.py`, which writes the v1 signature in the
|
||
classic Android SignApk format and aligns the APK. (Modern `jarsigner` output does *not* install on
|
||
Android 4.2 and older: it names digests `SHA-1-Digest` and labels the PKCS#7 signer algorithm in a
|
||
way old `JarUtils` can't read, so the phone treats the APK as unsigned.) The debug key in `signing/` (password `minamp123`) is what this APK is
|
||
signed with, so rebuilt APKs install over it.
|
||
|
||
Tests (desktop JVM, no device):
|
||
|
||
* `test/PlexTest.java` + `test/mock_plex.py`: PIN sign-in, server discovery, connection
|
||
picking/fallback, TLS with a wildcard plex.direct-style certificate (ECDSA leaf, RSA root),
|
||
untrusted-root rejection, browsing, search, artwork, and the stream proxy (Range requests over
|
||
TLS, de-chunking transcodes, aborting).
|
||
* `test/QueueTest.java`: shuffle/unshuffle, repeat modes, play next, remove, move, clear.
|
||
* `test/LyricsTest.java`: LRC (several stamps a line, offsets, word stamps) and plain-text lyrics
|
||
(`javac -d /tmp/lt src/com/minamp/util/Lrc.java test/LyricsTest.java && java -cp /tmp/lt LyricsTest`).
|
||
* `test/PoolAbortTest.java` (`test/run_pool_abort.sh`): aborting a response that's already finished leaves
|
||
its pooled connection, now carrying another request, alone.
|
||
* `test/PreferHomeTest.java` + `test/identity_server.py`: after being away, the home address wins
|
||
again as soon as it answers.
|
||
* `test/NetTest.java`: the bundled TLS + HTTP client against real servers; `test/HandshakeProfile.java`
|
||
samples where handshake time goes (run with `-Xint`).
|
||
|
||
## Limits
|
||
|
||
* Android 2.x build: no gapless playback (no API for it), and the notification has no playback buttons
|
||
(pre-Android 3 can't): tap it to open Now Playing, or use the widget.
|
||
* Android 4.1 build: gapless only into a track whose file is already on the phone; FLAC is still transcoded
|
||
(ExoPlayer has no FLAC decoder on Android 4.1).
|