Skip to content

xnstad/android-media-control-bridge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jellyfin Remote — Android Bridge

Small Android service that exposes simple HTTP endpoints to remote-control whatever is currently playing on the phone (Finamp, Jellyfin, Spotify, etc.) and to read now-playing metadata & album art. Designed to pair with a PWA or other controller running on the same LAN/VPN.

  • Controls: play/pause, previous/next, seek, volume ±
  • Metadata: JSON snapshot of the active MediaSession (title/artist/album/duration/position)
  • Album art: served as JPEG
  • CORS-friendly: responds to preflight, wide-open Access-Control-* headers
  • Auth: shared secret via X-Secret header per request

⚠️ Intended for LAN/VPN use. If you expose it to the internet, put it behind TLS and an allow-list / reverse proxy.


How it works

The app runs a foreground Service (BridgeService) that:

  • Hosts a tiny embedded HTTP server (TinyHttpServer)
  • Uses a NotificationListenerService (NotifAccess) to read active MediaSessions
  • Invokes MediaController.TransportControls for commands
  • Reads MediaMetadata for track info and artwork

All responses include permissive CORS headers and OPTIONS preflight support.


Endpoints

All endpoints require the header: X-Secret: <your secret> (Preflight OPTIONS does not. See Art in browsers below.)

Control (POST)

POST /play
POST /pause
POST /toggle
POST /next
POST /prev
POST /seek?ms=<milliseconds>
POST /volup
POST /voldown
  • seek: absolute position in milliseconds
  • Response: 200 ok on success, 400 no active session if nothing to control

Info (GET)

GET /sessions

Returns an array of visible sessions (package, state, title, artist) — useful for debugging.

GET /nowplaying

Returns a JSON snapshot:

{
  "hasSession": true,
  "clientPackage": "com.xxxx.finamp",
  "clientLabel": "Finamp",
  "isPlaying": true,
  "title": "Track Title",
  "artist": "Artist",
  "album": "Album",
  "durationMs": 247000,
  "positionMs": 122000,
  "volume": 9,
  "maxVolume": 15
}
GET /art

Returns current art as image/jpeg.

Art in browsers: <img src> can’t send custom headers, so your X-Secret won’t reach the server. Fetch the image as a blob with X-Secret, then set img.src = URL.createObjectURL(blob) (see example below).

CORS / Preflight

  • Access-Control-Allow-Origin: *
  • Access-Control-Allow-Headers: X-Secret, Content-Type
  • Access-Control-Allow-Methods: POST, GET, OPTIONS
  • Cache-Control: no-store

OPTIONS replies 204 No Content with the same CORS headers.


Quick start

1) Build & install

  • Open in Android Studio.
  • Recommended minSdk 28+, target latest stable.
  • Run on your phone (debug build is fine).

2) First run

  • Open the app.
  • Set a strong Bridge Secret and (optionally) change Port (default 8765).
  • Tap Save — the service restarts. Status shows HTTP: http://<phone-ip>:<port>.

3) Grant notification access

  • The app prompts for it, or go to: Settings → Notifications → Notification access and enable JF Bridge Notification Access. Without this, no sessions are visible (you’ll see 400 no active session).

4) Test from your computer

# Toggle play/pause
curl -i -X POST http://PHONE_IP:8765/toggle -H 'X-Secret: yoursecret'

# Now playing JSON
curl -i http://PHONE_IP:8765/nowplaying -H 'X-Secret: yoursecret'

# Save album art
curl -o art.jpg http://PHONE_IP:8765/art -H 'X-Secret: yoursecret'

5) Use from a web app

// Control
await fetch(`http://${phone}:8765/toggle`, {
  method: 'POST',
  headers: { 'X-Secret': secret }
});

// Now playing
const np = await fetch(`http://${phone}:8765/nowplaying`, {
  headers: { 'X-Secret': secret },
  cache: 'no-store'
}).then(r => r.json());

// Art as blob (so we can send the header)
const res = await fetch(`http://${phone}:8765/art?ts=${Date.now()}`, {
  headers: { 'X-Secret': secret },
  cache: 'no-store'
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
img.src = url;
// later: URL.revokeObjectURL(url);

Permissions & foreground service

Requested permissions:

  • INTERNET, ACCESS_NETWORK_STATE, WAKE_LOCK
  • FOREGROUND_SERVICE and FOREGROUND_SERVICE_MEDIA_PLAYBACK
  • POST_NOTIFICATIONS (Android 13+, optional but recommended)

Android 12+ requires calling startForeground() from onStartCommand() (not onCreate()). This project follows that.


Project structure

  • BridgeService.kt – Foreground service; starts/stops HTTP server; executes media commands
  • TinyHttpServer.kt – Minimal HTTP server with routing, CORS, preflight, JSON/binary writers
  • NotifAccess.ktNotificationListenerService to query active MediaSessions
  • Utils.kt – small helpers (e.g., current IP)
  • AndroidManifest.xml – permissions, services, notification channel, optional cleartext LAN setting

Security

  • Auth is a shared secret in the X-Secret header. Choose a strong random value.

  • Do not expose this plain-HTTP service directly to the internet.

  • If remote access is required, use a reverse proxy with:

    • TLS
    • IP allow-listing or VPN
    • Optional extra auth (e.g., basic auth)

Troubleshooting

  • 401 Unauthorized Missing/incorrect X-Secret. For album art, ensure you fetch as a blob so the header is included.

  • 400 no active session No visible MediaSession or notification access not granted. Start playback in the app and verify access.

  • Service start errors / “ForegroundServiceStartNotAllowed” Ensure notifications are permitted on Android 13+; this repo calls startForeground() in onStartCommand().

  • Controls work but metadata doesn’t update Some apps delay metadata. The bridge snapshots the top active session; try a quick pause/play. Confirm NotifAccess is enabled.


License

GPL-3.0


Credits

  • Android MediaSessionManager / MediaController APIs
  • Inspired by controlling Jellyfin/Finamp sessions from a lightweight PWA

About

Small app to expose your android devices' media controls (playback, volume) to the local network with a small API.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages