App crashes on startup when the control port is taken or a MediaSession still exists #11

Closed
opened 2026-08-26 01:57:29 +02:00 by benjamin · 1 comment
Owner

Problem

Two unguarded failure points during onCreate(), both of which throw instead of degrading:

1. MediaSession uses the default (empty) id. PlayerController.kt:26 builds MediaSession.Builder(context, player).build() without setId(). Media3 keeps a static SESSION_ID_TO_SESSION_MAP guarded by a static lock; the constructor throws IllegalStateException("Session ID must be unique. ID=") if a session with that id is still registered in the process. If a previous PlayerController has not been released when a new MainActivity is created, this crashes before anything else runs.

2. start(0, true) has no error handling. ControlServer.startServer() (ControlServer.kt:73-74) calls it directly from onCreate(). A bind failure — port 8765 held by another process, or an OEM quirk on some TV — surfaces as an uncaught IOException and takes the whole app down at launch.

Failure scenario

Neither is likely on the daily path (configChanges avoids most recreations, and 8765 is usually free), but both fail hard rather than gracefully: the app dies at startup with a system crash dialog, and the viewer has no way to understand or recover from it. For an app whose whole point is being idiot-proof for family members, "port busy" should not equal "app won't start".

Suggested fix

  • Give the session a stable explicit id, e.g. .setId("castarr"), and make sure release() always runs before a new one is built
  • Wrap start() in a runCatching; on failure keep the app fully usable without the phone remote and show a plain-language note in the settings ("Handy-Fernbedienung nicht verfügbar")

Found by multi-agent code review; verified against media3 1.4.1 bytecode and current PlayerController.kt / ControlServer.kt.

Note: the same review flagged blocking socket writes on the main thread, cross-thread ExoPlayer reads and non-volatile state sharing — all of which are already fixed in this codebase (sendExecutor for all writes, payloads built on the main thread via post {}, StateFlow for channel data).

## Problem Two unguarded failure points during `onCreate()`, both of which throw instead of degrading: **1. `MediaSession` uses the default (empty) id.** `PlayerController.kt:26` builds `MediaSession.Builder(context, player).build()` without `setId()`. Media3 keeps a static `SESSION_ID_TO_SESSION_MAP` guarded by a static lock; the constructor throws `IllegalStateException("Session ID must be unique. ID=")` if a session with that id is still registered in the process. If a previous `PlayerController` has not been released when a new `MainActivity` is created, this crashes before anything else runs. **2. `start(0, true)` has no error handling.** `ControlServer.startServer()` (`ControlServer.kt:73-74`) calls it directly from `onCreate()`. A bind failure — port 8765 held by another process, or an OEM quirk on some TV — surfaces as an uncaught `IOException` and takes the whole app down at launch. ## Failure scenario Neither is likely on the daily path (`configChanges` avoids most recreations, and 8765 is usually free), but both fail hard rather than gracefully: the app dies at startup with a system crash dialog, and the viewer has no way to understand or recover from it. For an app whose whole point is being idiot-proof for family members, "port busy" should not equal "app won't start". ## Suggested fix - Give the session a stable explicit id, e.g. `.setId("castarr")`, and make sure `release()` always runs before a new one is built - Wrap `start()` in a `runCatching`; on failure keep the app fully usable without the phone remote and show a plain-language note in the settings ("Handy-Fernbedienung nicht verfügbar") Found by multi-agent code review; verified against media3 1.4.1 bytecode and current `PlayerController.kt` / `ControlServer.kt`. Note: the same review flagged blocking socket writes on the main thread, cross-thread ExoPlayer reads and non-volatile state sharing — all of which are already fixed in this codebase (`sendExecutor` for all writes, payloads built on the main thread via `post {}`, `StateFlow` for channel data).
Author
Owner

Fixed in dd612fc, shipped in v0.9.0.

  • The MediaSession gets an explicit id: MediaSession.Builder(context, player).setId("castarr").build(), so it no longer collides on the default empty id.
  • startServer() wraps start() in runCatching and exposes a running flag. A bind failure is logged and the app continues normally — everything except the phone remote keeps working.
  • AppState.remoteAvailable carries that flag into the UI: the Handy-Fernbedienung card then says "Fernbedienung nicht verfügbar — Port belegt. Neustart des Fernsehers hilft meistens." instead of the app dying at launch with a system crash dialog.
  • stopServer() only calls stop() when the server actually started, so teardown after a failed bind is clean too.
Fixed in dd612fc, shipped in v0.9.0. - The MediaSession gets an explicit id: `MediaSession.Builder(context, player).setId("castarr").build()`, so it no longer collides on the default empty id. - `startServer()` wraps `start()` in `runCatching` and exposes a `running` flag. A bind failure is logged and the app continues normally — everything except the phone remote keeps working. - `AppState.remoteAvailable` carries that flag into the UI: the Handy-Fernbedienung card then says "Fernbedienung nicht verfügbar — Port belegt. Neustart des Fernsehers hilft meistens." instead of the app dying at launch with a system crash dialog. - `stopServer()` only calls `stop()` when the server actually started, so teardown after a failed bind is clean too.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: be-nj/castarr#11