Introduction
Contact Information
- Name: Anuj
- IRC nick: anuj_
- Email: aanuj8619@gmail.com
- GitHub: anuj990
- LinkedIn: Anuj | LinkedIn
I am Anuj, a sophomore at the Indian Institute of Information Technology Jabalpur (IIITDM Jabalpur), pursuing B.Tech in Electronics and Communication Engineering.
I started my coding journey in my first semester with C++ for competitive programming. Through my college’s open source programme BSOC (BitByte Summer of Code) I found open source and Android development interesting. Since then my stack has been Kotlin, Jetpack Compose, and MVVM.
I am particularly interested in music focused products, as I regularly use them during long coding sessions. This makes ListenBrainz a project I naturally connect with, both as a developer and as a
user
Project Overview
The ListenBrainz Android app has two incomplete features. First, Listening Now which shows what the user is playing right now was removed from the UI at some point, even though the API integration is still in the codebase. Second, Remote Playback through Spotify or YouTube can only play one track. There is no queue, no auto next, nothing.
My proposal is to fix both by shared KMP module that watches all three playback sources BrainzPlayer, the LB WebSocket, and the Remote Playback queue and decides what the mini player should show
Deliverables
- Integrate Listening Now into the existing playback system, ensuring real-time updates
- Listening Now and Remote Playback into a single state model that drives the UI consistently
- Implement priority handling:
- BrainzPlayer state overrides Listening Now
- Listening Now shown when BrainzPlayer is inactive
- Add device detection:
- Detect whether Listening Now is from the current device or another device using Listen Submission Service
- Show queue only for current device playback
- Implement Remote Playback queue support
- Persist playback queue and state using Room to handle process death and app restarts
- Implement robust WebSocket handling
- Revamp Scaffold UI:
- Display BrainzPlayer / Listening Now
- Show queue when applicable
- Modularize core logic into a Kotlin Multiplatform module for reuse across platforms
Proposed System:
System Visualization
- BrainzPlayer is active → override Listening Now from server, show BrainzPlayer state in mini player
- BrainzPlayer is inactive → show Listening Now state only
- Listening Now is active from this Android device → show playlist queue that are auto played by Remote Playback integrations
- Listening Now is active from another device → show the track info only, no queue, since playback is happening elsewhere
PlaybackUiState
All UI states are represented in sealed class in commonMain, it receives one of four states and renders accordingly
sealed class PlaybackUiState {
object Idle : PlaybackUiState()
data class BrainzPlayerActive(val track: Track) : PlaybackUiState()
data class RemotePlaybackActive(
val track: Track, val queue: List<Track>
) : PlaybackUiState()
data class ListeningNowOtherDevice(val track: Track) : PlaybackUiState()
}
ListeningNowViewModel
The ViewModel extends KMP ViewModel() , it combines three flows BrainzPlayer state , ListeningNow stream and RemotePlayback queue into a single StateFlow. The resolveUiState() function applies the BrainzPlayer win priority described as above, each flow given a default value so that combine() can emit on first without waiting for all three to be ready avoiding UI freeze up
combine(
brainzPlayerRepository.playerState, // default: Inactive
listeningNowRepository.listeningNowFlow, // default: null
remotePlaybackController.queueState // default: Idle
) { playerState, listeningNow, queue ->
resolveUiState(playerState, listeningNow, queue)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), PlaybackUiState.Idle)
ListeningNowRepository
Lives in commonMain opens a Ktor websockets to LB socket API when the flow is collected(cold flow : no connection if nothing is observing), exponential backoff starting at 1 second, capping at 30 seconds, on every reconnection ListeningNowRepository makes one REST call to fetch the current Listening Now state before letting the WebSocket stream take over. This covers the gap. REST for the initial state on connect, WebSocket for updates after that is how the LB Socket API is designed to be used anyway, the connection lifetime is tied to ViewModelScope so it cleans up automatically when the ViewModel is cleared
RemotePlaybackController (expect/actual)
This interface lives in commonMain, androidMain provides SpotifyRemotePlaybackController, ios main provides no-op stubs that returns RemotePlaybackState.Unavailable A factory in androidMain picks the correct implementation based on the playlist’s track source
// commonMain
interface RemotePlaybackController {
val queueState: StateFlow<RemotePlaybackState>
suspend fun playPlaylist(tracks: List<Track>)
suspend fun skipToNext()
}
expect fun createRemotePlaybackController():RemotePlaybackController
Spotify Queue Manager and Track-End Detection
The spotify app remote sdk already present in spotify-app-remote gradle module as spotify-app-remote-release-0.8.0.aar,ListenServiceManager which already runs as a NotificationListenerService and monitors active MediaSessions on the device is queried for the active Spotify MediaController
The MediaController exposes an onPlaybackStateChanged() callback that fires with real state constants from the Android media framework. When Spotify finishes a track, the session transitions to PlaybackState.STATE_NONE or PlaybackState.STATE_STOPPED. This is a genuine state event from the Android system, not an inferred heuristic, so it reliably distinguishes a natural track end from a user pause
ListenServiceManager exposes getActiveMediaController(packageName) , a small addition to its existing session monitoring logic, returning the MediaController for the given package if one is currently active. The Spotify App Remote SDK still handles the actual play commands, the MediaController is only used for state observation. If Spotify is not installed, SpotifyAppRemote.connect() fails and RemotePlaybackState moves to Unavailable, so the UI renders gracefully rather than crashing
// androidMain inside SpotifyRemotePlaybackController
val spotifyController = listenServiceManager.getActiveMediaController(SPOTIFY_PACKAGE)
spotifyController?.registerCallback(object : MediaController.Callback() {
override fun onPlaybackStateChanged(state: PlaybackState?) {
if (state?.state == PlaybackState.STATE_NONE ||
state?.state == PlaybackState.STATE_STOPPED) {
scope.launch { playNext() }
}
}
})
Device Detection via ListenServiceManager
The LB server doesn’t tag ListeningNow events with a device identifier, to determine whether an event is from this device or another ListenServiceManager is modified to expose the last submitted track as internal state ,when an event arrives it will be compared to exposed state(same track name , same artist name,within a 10 second timestamp window that covers the three possible delays : device to server latency , server processing and websocket push back)
Queue Persistence with Room DB
The active ListenBrainz playlist queue including the full track list and current index will be persisted using Room in androidMain to ensure continuity across process death and app restarts.
On each queue update (e.g., play, skip), both the queue and current index will be stored atomically using Room transactions to maintain consistency. On app launch, this state is restored and playback resumes from the last known position.
The schema will be designed to remain flexible:
- A single row approach (storing the queue as a serialized JSON with current index) offers simplicity and fast read/write
- A normalized approach (separate table for tracks with position + a queue state row for current index) provides better scalability and supports future operations like reordering or partial updates.
Koin Wiring
// commonMain
val playbackModule = module {
single<ListeningNowRepository> { ListeningNowRepositoryImpl(get()) }
viewModelOf(::ListeningNowViewModel)
}
// androidMain
val androidPlaybackModule = module {
single<RemotePlaybackController> { createRemotePlaybackController() }
single { QueuePersistenceDao(get()) }
}
YouTube Intent Launch, Foreground Only
YouTube has no App Remote SDK,Youtube is Handled via Android Intents only, When a playlist track has Youtube Source, the app fires an Intent to open You tube directly, there is no in app playback , no queue , no background playback for YT tracks , this is TOS safe and require no API key or Library
SoundCloud Background Queue Playback
SoundCloud is the right replacement for background continuous playback for yt. LB server already resolves tracks to SoundCloud stream URLs; there is no SoundCloud SDK involved on the client side. When a playlist track has a SoundCloud source, the app requests the resolved stream URL from the LB API and passes it directly to ExoPlayer, which is already present in the project inside BrainzPlayer
ExoPlayer handles background playback natively. Track end detection uses Player.Listener.onPlaybackStateChanged with STATE_ENDED a clean callback, no heuristics required unlike Spotify. When a track ends, the queue manager fetches the next resolved URL from the LB API and passes it to ExoPlayer.same will be implemented for iOS
A boolean flag buildQueue on the playlist model controls whether the queue manager activates for a given playlist. When buildQueue = true, continuous background playback runs. When false (YouTube case), the controller is bypassed entirely
Known Hurdles
Hurdle 1 : Spotify SDK Has No Track-End Event
Spotify has no onTrackEnd() using ListenServiceManager.getActiveMediaController() to observe PlaybackState.STATE_NONE / STATE_STOPPED from the Android media framework directly, as described above
Hurdle 2 : Device Detection Timestamp Window
Three delays stack between the moment a track starts on this phone and the moment the WebSocket event arrives back,network latency to the LB server, server processing, and the WebSocket push back to the app. On a good connection this is 1-2 seconds; on a slow connection it can reach 8-9 seconds. The 10-second detection window covers realistic worst cases. The known tradeoff is a false positive when two devices start playing the same track within 10 seconds of each other; this is an accepted limitation since the server does not provide device identifiers. Clock skew between the device clock and the LB server clock is handled by comparing relative elapsed times rather than raw timestamp subtraction, making the comparison clock independent. The window can be made configurable via a preference.
Hurdle 3: WebSocket State Gap After Reconnection
When the WebSocket reconnects after a network interruption or app backgrounding, the server pushes the current Listening Now state, but the app may have missed state changes during the gap. On every reconnect, ListeningNowRepository makes one REST call to the LB API to fetch the current Listening Now state before handing off to the WebSocket stream. This covers the gap cleanly and follows the intended usage of the LB Socket API, REST for the initial state, WebSocket for incremental updates.
Hurdle 4: Race Condition at Startup
Kotlin’s combine() waits for all three input flows to emit at least once before producing any output. At startup, BrainzPlayer state may emit immediately while the WebSocket connection is still opening, which would leave the ViewModel waiting and the mini player blank. The fix is to give each source a default initial value Inactive for BrainzPlayer, null for Listening Now, and Idle for Remote Playback queue state so resolveUiState() has something to work with from the very first emission and returns PlaybackUiState.Idle until real data arrives.
Timeline
Community Bonding (May 8-Jun 1)
Discuss Room DB queue schema design with mentor. Finalize Scaffold front layer mockups in Figma. Map the exact changes needed in ListenServiceManager to expose the last submitted track as internal state. Study ExoPlayer integration points inside BrainzPlayer and the Spotify App Remote SDK MediaSession API. Get familiar with the LB Socket API’s expected REST + WebSocket usage sequence.
| Week | Priority | Timeline Detail |
|---|---|---|
| Jun 2 - 8 | High | Define PlaybackUiState sealed class in commonMain. Set up RemotePlaybackFactory interface. Create Koin module skeletons in commonMain and androidMain |
| Jun 9 - 15 | Critical | Implement Ktor WebSocket to LB Socket API. Exponential backoff 1s to 30s cap. One REST call on every reconnect before WebSocket stream takes over, tie connection lifetime to viewModelScope |
| Jun 16 - 22 | Critical | Implement combine() with default initial values. Implement resolveUiState() with BrainzPlayer wins priority. Wire to Koin via viewModelOf() |
| Jun 23 - 29 | Critical | Add getActiveMediaController(packageName). Implement SpotifyRemotePlaybackController. Handle Spotify not installed via Unavailable state |
| Jun 30 - Jul 6 | Critical | Fetch resolved SoundCloud stream URLs from LB API, passed to ExoPlayer. Use Player.Listener STATE_ENDED for clean track-end detection. Add YouTube intent launch. Add buildQueue flag on playlist model |
| Jul 7 - 13 | High | Modify ListenServiceManager to expose the last submitted track. Implement isThisDevice() with clock-skew-safe relative elapsed time comparison and 10-second configurable window. Implement Room DB schema queue track list and current index |
Midterm Evaluation (July 14 - 18)
Submit midterm evaluation. Use this week as a buffer to address any slippage, fix blockers found by mentor, and stabilise the core flows before touching UI.
| Week | Priority | Timeline Detail |
|---|---|---|
| Jul 19 - Aug 1 | High | Two weeks most visible deliverable. Redesign mini-player Composable to render all four PlaybackUiState variants. Implement queue list UI. Wire ListeningNowViewModel via koinViewModel() |
| Aug 2 - 8 | High | Connect all pieces end-to-end. Fix wiring issues. Verify Room DB restore on app restart. Test Spotify not installed, SoundCloud URL failure, WebSocket drop mid session, queue exhausted, YouTube intent |
| Aug 9 - 15 | Medium | Unit tests for resolveUiState() and isThisDevice(). One integration test for WebSocket reconnect. Fix anything broken |
| Aug 16 - 22 | High | KDoc for all public interfaces and ViewModels. Code cleanup. Final mentor review. Buffer for last-minute fixes before final evaluation |
Extras
These are additional features I would like to work on if time permits:
-
Configurable device detection window : expose the 10-second isThisDevice() threshold as a user preference in Settings.Users on consistently slow mobile connections could raise it; users on fast WiFi could tighten it to reduce false positives
-
Queue reordering UI:The Room DB schema stores tracks with a position index. If the schema is normalized, drag-to-reorder becomes a natural extension of the queue list UI.
Post GSOC Plan
-
Listening Together feature: the entire real time infrastructure built here Ktor WebSocket to LB Socket API, device detection, ListeningNowViewModel is the exact foundation needed for a “listen along” feature where friends can follow each other’s playback live. This is a natural product extension that reuses everything built in GSoC with minimal new infrastructure.
-
Widget For easy access to current state : I am planning to implement Widget for Easy access to current state
Community Affinities
Tell us about the computer(s) you have available for working on your SoC project?
I will be using my Asus TUF Gaming A15 running Fedora KDE/Win 11 on dual boot. It handles Android Studio and Gradle builds comfortably.
When did you first start programming?
I started with C++ in my first semester of college for competitive programming. I then found Android development through BSOC, my college’s open source programme. Kotlin is now my primary language.
What type of music do you listen to?
Sufi and Ghazal music my most-played artists are Nusrat Fateh Ali Khan and Jagjit Singh ,some of my Mbid’s are
- a39a2243-3f86-4c02-82f0-1200fa611d3e
- a78b6b9a-d7ea-4f59-81df-20ddc3046f9e
- 64d905e4-7d08-4dc7-976a-7bbcfe653f3c
- 19f55b19-2ec3-41c3-9a8c-b55e89774ca3
What aspects of the project interest you the most?
The architectural challenge,one coherent UI. The API integrations already exist, the Spotify SDK module is already in the codebase, ExoPlayer is already in BrainzPlayer. The problem is design: how do you unify them without creating a tangled mess, in a KMP module that also works on iOS? That is the kind of problem I want to solve.
Have you contributed to other Open Source projects?
Yes, I contributed to open source earlier through my college’s open source programme BSOC (BitByte Summer of Code)
How much time do you have available, and how would you plan to use it?
I can dedicate 20-25 hours per week to the project. College exams end before the coding period begins so there will be no academic conflicts. I will keep stay active on IRC and the MetaBrainz forums to keep the mentor updated on progress and blockers.
Link to Proposal PDF