GSoC 2025: Integrate Listening now and BrainzPlayer in ListenBrainz
Contact Information
Name: Gautam Shorewala
IRC nick: gautam_coder
Email: gautamcoder4019k@gmail.com
GitHub: GautamCoder4019k
LinkedIn: https://www.linkedin.com/in/gautamcoder/
Mobile Number: +91 7980131738
I am Gautam Shorewala, a third-year student at Dayananda Sagar College of Engineering. I love building Android apps and have worked with Kotlin, Jetpack Compose, and other modern tools. Right now, I’m interning as an Android Developer at Eight, where I help improve app features and performance. I also worked at ChatWise, fixing bugs and adding new features to make the app better. I enjoy taking part in coding competitions and hackathons. When I’m not coding, I like listening to music and solving coding challenges for fun.
Project Overview
ListenBrainz Android currently supports music playback through BrainzPlayer, but it is limited to local music files only. The “Listening Now” feature, available in earlier iterations of the app, allowed users to track their currently playing song across devices.
This project aims to restore and enhance the “Listening Now” feature in a more efficient way. If the user is listening via BrainzPlayer, the app will override the server-provided “Listening Now” state with BrainzPlayer’s current playback. Otherwise, it will fetch the Listening Now data from the ListenBrainz API.
Additionally, this project will fix existing bugs in BrainzPlayer, revamp the player UI to integrate listen now, and integrate automatic playback support for remote services such as Spotify and YouTube Music.
My Contributions
I have been contributing to ListenBrainz since December 2024 and have worked on fixing major bugs in BrainzPlayer as well as adding some new features like Dynamic player background and HueSound.
Implementation Details
1. BrainzPlayer Bug Fixes and Enhancements
Currently there are few bugs in BrainzPlayer which hampers the UX of the app.
Bug 1: Playlist Mismatch Issue
Description:
When the user plays a song from one playlist and then switches to another playlist containing the same song, tapping on the song which is currently playing in a different playlist does not update the player’s playlist. The UI correctly displays the new playlist, but the ExoPlayer continues playing the old playlist.
Cause & Fix:
fun playOrToggleSong(mediaItem: Song, toggle: Boolean = false, playListId: String) {
if (brainzPlayerServiceConnection.transportControls == null) return
val isSamePlaylist = curentplayingPlaylistId == playListId
val isPrepared = playbackState.value.isPrepared
if (isPrepared && mediaItem.mediaID == currentlyPlayingSong.value.toSong.mediaID && !isSamePlaylist) {
playbackState.value.let { playbackState ->
when {
playbackState.isPlaying -> if (toggle) brainzPlayerServiceConnection.transportControls?.pause() else {}
playbackState.isPlayEnabled -> {
mediaItem.lastListenedTo = System.currentTimeMillis()
viewModelScope.launch { songRepository.updateSong(mediaItem) }
brainzPlayerServiceConnection.transportControls?.play()
}
else -> Unit
}
}
} else {
mediaItem.lastListenedTo = System.currentTimeMillis()
viewModelScope.launch { songRepository.updateSong(mediaItem) }
brainzPlayerServiceConnection.transportControls?.playFromMediaId(mediaItem.mediaID.toString(), null)
}
}
Here, the problem arises because the logic only checks if the mediaID is the same as the currently playing song. If it matches, the player either plays or pauses the current song without verifying if the playlist itself has changed.
Since the ExoPlayer playlist is only updated when onPrepareFromMediaId() is called, the player keeps using the old playlist unless explicitly refreshed.
Adding an additional check for the playlist will help fix this bug.
Bug 2: BackdropScaffold Navigation Issue
Description:
When the player screen is visible and the user taps on the Settings icon in the top bar, the app navigates to the Settings screen but leaves the player screen open. This creates a cluttered UI and hampers the user experience.
Cause & Fix:
The issue occurs because the BackdropScaffoldState is not checked in TopBar while navigating. Adding that to the TopBar will help fix this issue.
2. Listening Now Integration
In the above diagram, we can see how the flow for Listening Now will work:
- First, we check if BrainzPlayer is active.
- If not, we get the listening now from the socket implementation which is already there.
- If the user is on the same device then the playlist will be shown to him and the playlist will be auto played by the remote playback integrations
We’ll define an enum to track the active player:
enum class PlayerSourceType {
BRAINZ_PLAYER,
LISTENING_NOW_REMOTE
}
To detect whether BrainzPlayer is active, we check if any song is currently playing via brainzPlayerConnection
. If not, we fallback to Listening Now data from the server.
The UI will observe this value using a state-holder like MutableStateFlow
, updating the visual layout accordingly.
3. Creating and Storing the Queue in DB
To maintain the queue persistently, we’ll store it using RoomDB. This will allow retrieval and editing of the queue even after app restarts. A repository pattern will be used for modularity and testability.
Sample code
@Entity(tableName = "playback_queue")
data class QueueEntity(
@PrimaryKey val id: Int,
val mediaId: String,
val title: String,
val artist: String,
val source: String
)
@Dao
interface QueueDao {
@Query("SELECT * FROM playback_queue")
suspend fun getQueue(): Flow<List<QueueEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addToQueue(song: QueueEntity)
@Query("DELETE FROM playback_queue")
suspend fun clearQueue()
}
class QueueRepository(private val dao: QueueDao) {
suspend fun getQueue() = dao.getQueue()
suspend fun addToQueue(song: QueueEntity) = dao.addToQueue(song)
suspend fun clearQueue() = dao.clearQueue()
}
The queue can have both remote and local songs in a single playlist. The source
field can be used to determine whether we are going to use Exoplayer
to play the song locally or use RemotePlaybackHandler
to play the song remotely.
Example usage:
@Inject
private lateinit val remotePlaybackHandler: RemotePlaybackHandler
@Inject
private lateinit val brainzPlayerServiceConnection: BrainzPlayerServiceConnection
fun playNextTrack() {
val nextTrack = songQueue[currentPlayingPosition+1]
if (nextTrack != null) {
when (nextTrack.source) {
"local" -> brainzPlayerServiceConnection.transportControls?.playFromMediaId(nextTrack.mediaId, null)
"spotify" -> {
Uri.parse(spotifyId).lastPathSegment?.let { trackId ->
remotePlaybackHandler.playUri(trackId){
//handle error
}
}
}
"youtube" -> {
remotePlaybackHandler.apply {
val result = playOnYoutube {
withContext(ioDispatcher) {
searchYoutubeMusicVideoId(
event.metadata.trackMetadata.trackName,
event.metadata.trackMetadata.artistName
)
}
}
}
}
}
}
}
4. Background queue playback using Remote Integration:
Currently, users cannot play entire playlists using remote playback services like Spotify or YouTube Music. To solve this:
- The
ListenSubmissionService
will be enhanced to detect playback events such as next or previous triggered via notification or media controls. - These events will be routed to the new
QueuePlayerService
using aBroadcastReceiver
. QueuePlayerService
will manage the list of tracks, track the current index, and interface with theRemotePlaybackHandler
to trigger playback.
QueuePlayerService
will be a persistent, foreground service and show a persistent notification that will not provide any playback controls but will clearly display a message such as:
“ListenBrainz is playing your playlist”
This will help in increasing the transparency and inform the user that the app is actively handling their playback session.
Sample code for the QueuePlayerService
@AndroidEntryPoint
class QueuePlayerService : Service() {
@Inject
lateinit var remotePlaybackHandler: RemotePlaybackHandlerImpl
private val songQueue = mutableListOf<TrackMetadata>()
private var currentSongIndex = 0
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private val trackChangeReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_PLAY_NEXT_SONG) {
handleSongChange()
}
}
}
override fun onCreate() {
super.onCreate()
LocalBroadcastManager.getInstance(this).registerReceiver(
trackChangeReceiver,
IntentFilter(ACTION_PLAY_NEXT_SONG)
)
scope.launch {
remotePlaybackHandler.connectToSpotify { error ->
Log.e("Failed to connect to Spotify: ${error.actualResponse}")
}
}
}
private fun handleSongChange() {
if (songQueue.isNotEmpty() && currentSongIndex < songQueue.size()-1) {
currentSongIntex += 1
playSongWithSpotify(songQueue[currentSongIndex])
}
}
private fun playSongWithSpotify(song: TrackMetadata) {
val spotifyId = song.additionalInfo?.spotifyId
Uri.parse(spotifyId).lastPathSegment?.let { trackId ->
remotePlaybackHandler.playUri(
trackId = trackId,
onFailure = {Log.e("Error playing from spotify")}
)
}
}
override fun onDestroy() {
scope.launch {
remotePlaybackHandler.disconnectSpotify()
}
scope.cancel()
LocalBroadcastManager.getInstance(this).unregisterReceiver(trackChangeReceiver)
super.onDestroy()
}
}
5. Remote Playback Considerations:
Spotify
- Requires an active internet connection and user authentication.
- A check for Spotify connection will be added to the service to prevent unexpected disconnections.
- The connection will be disconnected in
onStop()
to prevent memory leaks.
YouTube Music
- Playback via intents is restricted by Android’s background execution policies.
- On Android 15+, launching activities or intents from the background is blocked unless you have special permissions (like
SYSTEM_ALERT_WINDOW
). - As a workaround we can use a
PendingIntent
from a notification that the user can tap to manually trigger the next song.
If no remote playback mechanism is available, users will not be allowed to add the songs to the queue. A proper message will be shown to inform the user of the cause.
6.UI Enhancements for Listening Now:
We will be reusing most of the existing components from the existing brainzPlayer to show the Listening now. The mock ui for listening now.
MiniPlayer
Player
Timeline
Before Community Bonding (April 8 - May 7)
-
Set up the development environment and get familiar with the MetaBrainz ecosystem.
-
Work on adding features and fix bugs in the app.
-
Discuss the idea with the mentors.
Community Bonding Period (May 8 - June 1)
-
Get familiar with ListenBrainz Android codebase, especially BrainzPlayer and ListenSubmissionService.
-
Analyze existing Listening Now implementation from previous iterations.
-
Study the RemotePlaybackHandlerImpl and how it interfaces with Spotify and YouTube Music.
-
Finalize the requirements and design of the project
Phase 1 - Integrate Listen Now(June 2 - July 13)
Week 1-2: June 2 - June 15
-
Design and implement Listening Now UI components.
-
Fix existing bugs in BrainzPlayer.
Week 3-4: June 16 - June 29
-
Implement different states to show Listening Now & BrainzPlayer.
-
Update miniplayer and player UI to show Listening Now.
-
Improve playlist UI of the player.
-
Ensure seamless switching between BrainzPlayer and Listen Now states.
Week 5-6: June 30 - July 13
-
Test the listening now functionality ensuring proper functioning.
-
Write unit tests & UI tests.
-
Start next phase early if Phase 1 is completed.
Midterm Evaluation: July 14 - July 18
-
Submit progress report.
-
Fix any bugs identified during testing.
-
Gather mentor feedback for improvements.
Phase 2 - Automatic Remote Playback (July 19 - August 25)
Week 7-8: July 19 - August 1
-
Start implementing QueuePlayerService.
-
Connect RemotePlaybackHandlerImpl with QueuePlayerService.
Week 9-10: August 2 - August 15
-
Handle song change from the notification.
-
Ensure proper background queue playback for different android versions.
-
Ensure proper error handling
Week 11-12: August 16 - August 25
-
Test the functionality of the feature.
-
Fix any edge case bugs.
Final Evaluation: August 26 - September 1
-
Final polishing of UI and functionality.
-
Discuss final refinements with mentors.
-
Submit the project for final evaluation.
Community Affinities
What type of music do I listen to?
I mostly listen to rock and pop music.
What aspects of MusicBrainz/ListenBrainz interest me the most?
ListenBrainz Android interests me the most since I like listening to music most of the time and ListenBrainz helps me keep track of my listening habits, which I find very useful.
Have you ever used MusicBrainz Picard or any of our projects in the past?
Yes, I have used ListenBrainz to keep track of my music listening habits and have contributed to improving the ListenBrainz Android app.
Programming Precedents
When did you first start programming?
I first started programming in 2020 with Java being my first programming language.
Have you contributed to other open source projects?
ListenBrainz is my first open source project to which I have contributed, and it has been a very good experience for me.
Practical Requirements
What computer(s) do you have available for working on your SoC project?
Lenovo IdeaPad Gaming 3: AMD Ryzen 5 5600H Processor with Integrated AMD Radeon graphics and NVIDIA GTX 1650/RTX 3050, 16GB RAM running on Windows 10/11 Home, 1TB SSD.
How much time do you have available per week, and how would you plan to use it?
I have 25-30 hours a week available for this project. My typical working hours would be 19:00 - 00:00 (GMT +5:30). I don’t have any commitments during GSoC. Moreover, my college department has assured me of attendance concessions from my classes for the duration of GSoC. Work will be affected during weeks 2-3 since i have my exams during that time.
Tech stacks that I am familiar with:
- Languages: Kotlin, XML
- Frameworks: Jetpack Compose, Hilt
- Libraries: Retrofit, ExoPlayer, Media3
- Databases: Room, Firebase
- Tools: Git, GitHub, Android Studio