GSoC 2025: Integrate Listening now and BrainzPlayer in ListenBrainz

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:

  1. First, we check if BrainzPlayer is active.
  2. If not, we get the listening now from the socket implementation which is already there.
  3. 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 a BroadcastReceiver.
  • QueuePlayerService will manage the list of tracks, track the current index, and interface with the RemotePlaybackHandler 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
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
2 Likes

(No Metabrainz staff)

Sorry, but I think you’re too late. The cutoff point was the 31/03

@RustyNova its okay, he has been contributing for the past few months.

Oh, didn’t recognize the username. I though it was a lost GSOC student

Hi, @Gautam_coder the proposal does touch the main idea of the project but only on surface. I would like to see more details on the implementation of it. Some things I find missing here are:

  1. The queue should be globally editable and be persisted to DB. Maybe we can see some sort of interface implementation on what operations could be handled?
  2. A notification that says ListenBrainz is handling playback should be tied to this service.
  3. There is no mention of this service being of type foreground service which it should be.
  4. Any implications of using Spotify in Background.
  5. Any implications of launching intents from background for Youtube music playback.
  6. Can the ListenSubmission service be incorporated here in any way? Just ideas are fine, so we can build upon that.
  7. I don’t see the pseudo-code (what functions will be used from the repo itself?) that will be used to attain the switching between players. Afaik, the graph in your proposal is already laid out in the idea itself.
  8. What happens if we do not have any remote playback mechanism available?
  9. Are local songs and remote songs in a single playlist possible?

@Jasjeet thanks for the review i have updated my proposal keeping the mentioned points in mind. Sorry for the delay.