Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Notification Title during playback #951

Closed
AmrSubZero opened this issue Jan 8, 2024 · 5 comments
Closed

Update Notification Title during playback #951

AmrSubZero opened this issue Jan 8, 2024 · 5 comments
Assignees
Labels

Comments

@AmrSubZero
Copy link

Using Media3 ExoPlayer@1.2.0 with a PlaybackService that extends MediaSessionService

using setMediaNotificationProvider to create a Notification for API below 33.

public class PlaybackService extends MediaSessionService {

    public void onCreate() {

        // Initialize ExoPlayer
        ExoPlayer player = new ExoPlayer.Builder(this, ...).build();

        setMediaNotificationProvider(new MediaNotification.Provider() {
            @NonNull
            @Override
            public MediaNotification createNotification(@NonNull MediaSession mediaSession, @NonNull ImmutableList<CommandButton> customLayout, @NonNull MediaNotification.ActionFactory actionFactory, @NonNull Callback onNotificationChangedCallback) {

                createMediaNotification(mediaSession);

                return new MediaNotification(PLAYBACK_NOTIFICATION_ID, notificationBuilder.build());
            }

            // ...
        });

        // ...

    });

    public void createMediaNotification(MediaSession mediaSession) {

        MediaMetadata metadata = mediaSession.getPlayer().getMediaMetadata();

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(PlaybackService.this, PLAYBACK_CHANNEL_ID)
            // ...
            .setContentTitle(metadata.title)
            .setContentText(metadata.description)
            .setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession));

    }

}

in MainActivity i set the MediaMetaData (title/description)

List<MediaItem> mediaItems = new ArrayList<>();

MediaItem mediaItem = new MediaItem.Builder()
    .setMediaId(ITEM_ID)
    .setMediaMetadata(new MediaMetadata.Builder()
            .setTitle(ITEM_TITLE)
            .setDescription(ITEM_DESCRIPTION)
            // ..
    .setUri(ITEM_URI)
.build();

mediaItems.add(mediaItem);

player.setMediaItems(mediaItems);

player.prepare();
player.play();

TLDR: i need to update title/description of the media playback notification (during playback), after it's already set before using .setMediaMetadata(new MediaMetadata.Builder().setTitle(..)) and .setContentTitle(metadata.title)

Now, what i'm trying to achieve, lets say at some point (during playback) i need to update the title/description (or MediaMetaData title/description) of the Notification for the current playing media item, wether it is the Media3 Session Notification or Legacy Notification below API Level 33

// a method in MainActivity
private void onPlayerProgressChanged(long current_playback_time) {

    if(current_playback_time >= something) {
        
        // Here need to update the notification title/description
        // Like .setDescription("Something else")
    }

}

Here's how it's currently working:

Notifications

I can't seem to find any documentation or workaround to achieve this approach.

How can i achieve this?

@tianyif
Copy link
Contributor

tianyif commented Jan 8, 2024

Hi @AmrSubZero,

You can utilize the replaceMediaItem to achieve this.

MediaItem oldItem = ... // Your original item
MediaItem newItem = oldItem
                        .buildUpon()
                        .setTitle("new title")
                        .setDescription("new description")
                        .build();
player.replaceMediaItem(itemIndex, newItem);

In this way, the player can emit an EVENT_MEDIA_METADATA_CHANGED event, and media3 session MediaNotificationManager or legacy PlayerNotificationManager can update the notification when receive the metadata changed event.

Hope this helps!

@AmrSubZero
Copy link
Author

AmrSubZero commented Jan 10, 2024

Hello @tianyif

Actually the player.replaceMediaItem() did work updating the title, but i think it does not suit my case, here's why

The logic, since Media3 or ExoPlayer does not have built in Position tracker/listener (as far as i know), so i built my own using (Runnable/Handler) runs every 1 second.

See here
// ProgressTracker is a custom class that tracks player.getCurrentPosition() every 1 second.
ProgressTracker tracker;

MediaMetadata metadata = new MediaMetadata.Builder()....build();

player.addListener(new Player.Listener() {

    @Override
    public void onIsPlayingChanged(boolean isPlaying) {

        if(isPlaying) {

            tracker = new ProgressTracker(player, new ProgressTracker.PositionListener() {

                @Override
                public void progress(long current_position) {

                    if(current_position >= something) {

                        // Replace the current MediaItem with new Title
                        MediaItem mediaItem = player.getCurrentMediaItem();
                        if(mediaItem != null) {
                            mediaItem
                                .buildUpon()
                                .setMediaMetadata(metadata.buildUpon()
                                        .setTitle("Part: x")
                                        .build())
                                .build();
                            player.replaceMediaItem(0, mediaItem);
                        }
                    }
                }

            }

        }else {
            // Purge the position tracker (handler.removeCallbacks())
            if(tracker != null) { tracker.purgeHandler(); }
        }

    }

}


@Override
public void onDestroy() {

    // Purge the position tracker (handler.removeCallbacks())
    if(tracker != null) { tracker.purgeHandler(); }
}

// The position tracker class (ProgressTracker.java)

public class ProgressTracker implements Runnable {

    public interface PositionListener{
        public void progress(long position);
    }

    private final Player player;
    private final Handler handler;
    private PositionListener positionListener;

    public ProgressTracker(Player player, PositionListener positionListener) {
        this.player = player;
        this.positionListener = positionListener;
        handler = new Handler();
        handler.post(this);
    }

    public void run() {
        long position = player.getCurrentPosition();
        positionListener.progress(position);
        handler.postDelayed(this, 1000);
    }

    public void purgeHandler() {
        handler.removeCallbacks(this);
    }
}

Then i check if the current position equals something, there i need to update the meta title like "Part: 13", this runs very frequently!

Using player.replaceMediaItem() kinda resets the current position of the player for the second it did a replace, then it gets back to normal position.

See here
position: 50		--> first
position: 845
position: 1846
position: 2849
position: 3858
-- called player.replaceMediaItem()
position: 49		-- > should be 4xxx
position: 1051		-- > should be 5xxx
position: 6890
position: 7892
-- called player.replaceMediaItem()
position: 118		-- > should be 8xxx
position: 9994
position: 10996
position: 11999
position: 12993
position: 13994
-- called player.replaceMediaItem()
position: 115		-- > should be 14xxx
position: 16030
position: 17031
position: 18033
-- called player.replaceMediaItem()
position: 124		-- > should be 19xxx
position: 1124		-- > should be 20xxx
position: 2125		-- > should be 21xxx
position: 22074
-- called player.replaceMediaItem()
position: 101		-- > should be 23xxx
position: 1103		-- > should be 24xxx
position: 25080
position: 26080
position: 27081
-- called player.replaceMediaItem()
position: 100		-- > should be 28xxx
position: 1101		-- > should be 29xxx
position: 2102		-- > should be 30xxx
position: 31124
position: 32125
-- called player.replaceMediaItem()
position: 75		-- > should be 33xxx
position: 34126
position: 35128

Therefore, when i get incorrect current position things go wrong.

also is it a best practice calling player.replaceMediaItem() for a time like every 5 seconds? i can think it will break someway somehow!

So, my hopes:

  • another way to update the current playback (notification/mediasession) title without needing to replace the current media item.
  • a proper way to track current playback position (maybe built-in?) that doesn't get affected with things like player.replaceMediaItem()

Thanks!

@tianyif
Copy link
Contributor

tianyif commented Jan 10, 2024

@AmrSubZero,

Thanks for the details!

Did you reproduce this on demo-session app? I tried to reproduce on demo app with pasting your snippet in DemoPlaybackService and adding some simple logs to check the positions. The positions I got keep increasing. My metadata update frequency was even higher (after I got the current position per 1 sec, I replaced the media item with a new title).

a proper way to track current playback position (maybe built-in?)

I'm not sure if you're suggesting something like a current position listener. We had such requests (eg. google/ExoPlayer#3980) before, and the short answer is sending a huge amount of callbacks according to the position update frequency (per 10 ms) is less efficient than polling the progress infrequently. And the ProgressTracker should be sufficient for tracking the playback position for most purposes.

@AmrSubZero
Copy link
Author

AmrSubZero commented Jan 11, 2024

Hello @tianyif thanks for keeping up!

after hours of debugging and trying several methods, here's what i've discovered!

  • We have Exoplayer player inside the MediaSessionService
// PlaybackService.java

public class PlaybackService extends MediaSessionService {

    ExoPlayer player;

    @Override
    public void onCreate() {

        player = new ExoPlayer.Builder(this).build();
    }
}
  • and we have MediaController player in MainActivity which i receive as documented
// MainActivity.java

MediaController player;

ListenableFuture<MediaController> controllerFuture;

@Override
protected void onStart() {

    SessionToken sessionToken =
            new SessionToken(this, new ComponentName(this, PlaybackService.class));
    controllerFuture =
            new MediaController.Builder(this, sessionToken).buildAsync();
    controllerFuture.addListener(() -> {

        try {

            player = controllerFuture.get();

        } catch (Exception e) { e.printStackTrace(); }

    }, MoreExecutors.directExecutor());

}

When i tested getting current position from both (with different methods) as we discussed, with doing player.replaceMediaItem() frequently (every 1 second) :

  • MediaSessionService ExoPlayer player always giving the correct current position and it never gets interrupted by calling player.replaceMediaItem() no matter if clicking pause and play still correct.

  • MainActivity MediaController player somehow gets interrupted by calling player.replaceMediaItem() and giving incorrect (position reset) right after calling the .replaceMediaItem (happens if i pause and continue playing again)

i tried several methods to track current position here's by getting it directly from PlayerView

See here
public class MainActivity extends AppCompatActivity {

    MediaController player;

    ListenableFuture<MediaController> controllerFuture;

    PlayerView playerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        playerView = findViewById(R.id.playerView);
    }

    private void getCurrentPlayerPosition() {

        Log.i("PLAYBACK_STATE", "Playback Position: " + player.getCurrentPosition());

        if (player.isPlaying()) {
            playerView.postDelayed(this::getCurrentPlayerPosition, 1000);
        }
    }

    @Override
    protected void onStart() {

        SessionToken sessionToken =
                new SessionToken(this, new ComponentName(this, PlaybackService.class));
        controllerFuture =
                new MediaController.Builder(this, sessionToken).buildAsync();
        controllerFuture.addListener(() -> {

            try {

                player = controllerFuture.get();

                // Set Player to the PlayerView
                playerView.setPlayer(player);

                player.addListener(new Player.Listener() {

                    @Override
                    public void onIsPlayingChanged(boolean isPlaying) {

                        if(isPlaying) {
                            // Track the current playback position (every 1 second)
                            playerView.postDelayed(MainActivity.this::getCurrentPlayerPosition, 1000);
                        }
                }

            } catch (Exception e) { e.printStackTrace(); }

        }, MoreExecutors.directExecutor());

    }

}

Either way, updaing the MediaMetaData title (frequently) is working now by calling .replaceMediaItem() thanks to your support.

But i'm still curious, why the current poisition gets interrupted (when pausing and playing) from MediaController player in MainActivity and it does not from ExoPlayer player in MediaSessionService

Here is a repo of projects for both ways.

Thanks! 👏

@tianyif
Copy link
Contributor

tianyif commented Jan 12, 2024

Hi @AmrSubZero,

Thanks for the impressive details! Yes, I could reproduce the interrupted position with the test controller app with connecting to a session from the demo session app.

The reason being this is based on the four facts -

  1. When the MediaItem is replaced at the MediaController, the PlayerInfo (including the position info) got masked by at the controller side, with the default position of the new item (default is 0).
  2. Meanwhile, it is expected to receive the player info update from the session triggered by mediaMetadata change, after the MediaItem is replaced at the session player.
  3. The session side is regularly sending the SessionPositionInfo to the controller (default interval is 3s), and controller will then update its playerInfo.SessionPositionInfo.
  4. When getCurrentPosition() at the controller side, the returned current position is interpolated by the latest playerInfo.SessionPositionInfo.positionInfo.positionMs and the elapsed time from the last update.

Then I added some logs and found that -

2024-01-11 14:31:01.172 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [1] Replace MediaItem, masked position: 0
2024-01-11 14:31:01.373 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [2] onPlayerInfoUpdated update position to: 81
2024-01-11 14:31:02.131 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [2] onPlayerInfoUpdated update position to: 980
2024-01-11 14:31:02.179 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [4] controller latest playerInfo.SessionPositionInfo.positionInfo.positionMs: : 980
2024-01-11 14:31:02.179 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   I  position: 1043
2024-01-11 14:31:02.181 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [1] Replace MediaItem, masked position: 0
2024-01-11 14:31:02.323 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [2] onPlayerInfoUpdated update position to: 1038
2024-01-11 14:31:03.182 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [4] controller latest playerInfo.SessionPositionInfo.positionInfo.positionMs: : 1038
2024-01-11 14:31:03.182 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   I  position: 1919
2024-01-11 14:31:03.185 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [1] Replace MediaItem, masked position: 0
2024-01-11 14:31:04.187 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [4] controller latest playerInfo.SessionPositionInfo.positionInfo.positionMs: : 0
2024-01-11 14:31:04.187 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  WRONG position estimate: 1003
2024-01-11 14:31:04.187 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   I  position: 1003
2024-01-11 14:31:04.190 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [1] Replace MediaItem, masked position: 0
2024-01-11 14:31:05.124 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [3] Periodically update SessionPositionInfo from position: 0 , to: 3980
2024-01-11 14:31:05.191 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [4] controller latest playerInfo.SessionPositionInfo.positionInfo.positionMs: : 3980
2024-01-11 14:31:05.191 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   I  position: 4053
2024-01-11 14:31:05.194 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [1] Replace MediaItem, masked position: 0
2024-01-11 14:31:05.285 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [2] onPlayerInfoUpdated update position to: 4116
2024-01-11 14:31:06.195 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [4] controller latest playerInfo.SessionPositionInfo.positionInfo.positionMs: : 4116
2024-01-11 14:31:06.195 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   I  position: 5049
2024-01-11 14:31:06.199 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [1] Replace MediaItem, masked position: 0
2024-01-11 14:31:06.235 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [2] onPlayerInfoUpdated update position to: 5055
2024-01-11 14:31:07.199 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   W  [4] controller latest playerInfo.SessionPositionInfo.positionInfo.positionMs: : 5055
2024-01-11 14:31:07.199 24535-24535 PLAYBACK_STATE          androidx.media3.testapp.controller   I  position: 6027

The wrong position was marked as WRONG, which is 1003. I marked the logs corresponding to each above steps, and you can see between replacing the item and getting the wrong position 1003, there is neither [2] and [3] that update the SessionPositionInfo. The elapsed time since the last update is 1003, so plus masked position which is 0, we wrongly got 1003.

My rough thought is around whether we should use the default position 0 to mask the current position when the item is being replaced (corresponding to the above [1]). It makes sense in the case that the current playing item is completely removed and the default position of the next item will be immediately used, but in the case that the current playing item is replaced, using the current position instead of 0 would be a better option.

We will further look into and provide a proper solution to it. Thanks a lot for all the details and repo of the project!

copybara-service bot pushed a commit that referenced this issue Feb 29, 2024
When the controller replaces the current item, the masking position will be changed to the default position of the new item for a short while, before the correct position comes from the session. This will interrupt the current position fetched from the controller when the playback doesn't interrupted by the item replacing.

Issue: #951

#minor-release

PiperOrigin-RevId: 611417539
@tianyif tianyif closed this as completed Mar 4, 2024
SheenaChhabra pushed a commit that referenced this issue Apr 8, 2024
When the controller replaces the current item, the masking position will be changed to the default position of the new item for a short while, before the correct position comes from the session. This will interrupt the current position fetched from the controller when the playback doesn't interrupted by the item replacing.

Issue: #951

PiperOrigin-RevId: 611417539
(cherry picked from commit 1bdc58d)
l1068 pushed a commit to l1068org/media that referenced this issue Apr 15, 2024
When the controller replaces the current item, the masking position will be changed to the default position of the new item for a short while, before the correct position comes from the session. This will interrupt the current position fetched from the controller when the playback doesn't interrupted by the item replacing.

Issue: androidx#951

PiperOrigin-RevId: 611417539
(cherry picked from commit 1bdc58d)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants