How can I monitor progress changes on ExoPlayer
?
I tried to implement a hidden MediaController
and overriding setOnSeekBarChangeListener
methods, but for now without success. I'm wondering if there is another way to listen to the ExoPlayer
progress.

- 1,163
- 2
- 20
- 25

- 871
- 1
- 11
- 20
16 Answers
I know this question is very old. But, I landed on this while implementing ExoPlayer
. This is to help the others who do the same later on:)
So, I have followed the following methods to track progress of the playback. This is the way it is done in the ExoPlayer
Google Docs. It works as needed.
Checkout PlayerControlView.java
in Google ExoPlayer repository
updateProgressBar()
is the function to update the SeekBar
progress:
private void updateProgressBar() {
long duration = player == null ? 0 : player.getDuration();
long position = player == null ? 0 : player.getCurrentPosition();
if (!dragging) {
mSeekBar.setProgress(progressBarValue(position));
}
long bufferedPosition = player == null ? 0 : player.getBufferedPosition();
mSeekBar.setSecondaryProgress(progressBarValue(bufferedPosition));
// Remove scheduled updates.
handler.removeCallbacks(updateProgressAction);
// Schedule an update if necessary.
int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState();
if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) {
long delayMs;
if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) {
delayMs = 1000 - (position % 1000);
if (delayMs < 200) {
delayMs += 1000;
}
} else {
delayMs = 1000;
}
handler.postDelayed(updateProgressAction, delayMs);
}
}
private final Runnable updateProgressAction = new Runnable() {
@Override
public void run() {
updateProgressBar();
}
};
We call updateProgressBar()
within updateProgressAction
repeatedly until the playback stops.
The function is called the first time whenever there is a state change. We use removeCallbacks(Runnable runnable)
so that there is always one updateProgressAction
to care about.
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
updateProgressBar();
}
Hope this helps!

- 20,200
- 11
- 84
- 98

- 2,367
- 24
- 30
-
1Which actually means you are polling the state – Thomas Apr 10 '17 at 12:15
-
@Thomas which actually seems to be a kind of a right solution, because exoplayer itself generates progress events too frequently for UI. – kot331107 Dec 29 '17 at 17:19
-
@AnkitAggarwal I have custom progress bar how can i update progress while playing video plz help – Sagar Mar 06 '18 at 11:51
-
Well, as of today, with exoplayer version 2.14.0 , the above method has changed a lot, finding it hard to get the equivalent methods/variables for dragging, progressBarValue,... – AndroidDev Jun 10 '21 at 12:59
Just try this, its working for me :
handler = new Handler();
runnable = new Runnable() {
@Override
public void run() {
progressbar.setProgress((int) ((exoPlayer.getCurrentPosition()*100)/exoPlayer.getDuration()));
handler.postDelayed(runnable, 1000);
}
};
handler.postDelayed(runnable, 0);
Here,
getCurrentPosition()
: return The current playback position in milliseconds.getDuration()
: The duration of the track in millisecond.

- 8,489
- 55
- 66
I've found a pretty elegant solution using RxJava. This involves a polling pattern as well, but we make sure to use an interval to poll every 1 second.
public Observable<Long> playbackProgressObservable =
Observable.interval(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
The logic here is we create an Observable that will emit a sequential number every second. We then use the map
operator to transform the number into the current playback position.
public Observable<Long> playbackProgressObservable =
Observable.interval(1, TimeUnit.SECONDS)
.map( { exoPlayer.getCurrentPosition() } );
To finally hooked this together, just call subscribe, ad the progress updates will be emitted every second:
playbackProgressObservable.subscribe( { progress -> // Update logic here } )
Note: Observable.interval
runs on a default Scheduler
of Schedulers.computation()
. Therefore, you'll probably need to add an observeOn()
operator to make sure the results are sent to the right thread.
playbackProgressObservable
.observeOn(AndroidSchedulers.mainThread())
.subscribe(progress -> {}) // Update logic here
The above statement will give you a Disposable which must be disposed when you are done observing. You can do something like this ->
private var playbackDisposable: Disposable? = null
playbackDisposable = playbackProgressObservable
.observeOn(AndroidSchedulers.mainThead())
.subscribe(progress -> {}) // Update logic here
then to dispose the resource ->
playbackDisposable?.dispose()

- 733
- 6
- 15

- 402
- 3
- 5
-
This might not work in the new Exo player update. Player accessed on wrong thread warning might come – Samrat Aug 17 '21 at 05:41
Well, I did this through kotlin flow..
private fun audioProgress(exoPlayer: SimpleExoPlayer?) = flow<Int> {
while (true) {
emit(((exoPlayer?.currentPosition?.toFloat()?.div(exoPlayer.duration.toFloat())?.times(100))?.toInt()!!))
delay(1000)
}
}.flowOn(Dispatchers.IO)
then collect the progress like this...
val audioProgressJob = launch {
audioProgress(exoPlayer).collect {
MP_progress_bar.progress = it
}
}

- 961
- 9
- 13
-
2`exoPlayer.currentPosition` can only be accessed from Main Thread, otherwise it throws exception. – Jemshit Mar 04 '21 at 16:15
-
-
I did try and it throws exception at runtime. I prob. use newest version – Jemshit Mar 09 '21 at 18:32
-
-
Exception is documented on this page: https://exoplayer.dev/troubleshooting.html#what-do-player-is-accessed-on-the-wrong-thread-warnings-mean – Jemshit Mar 10 '21 at 11:18
-
If you change it to use `flowOn(Dispatchers.Main)`, this approach works fine. – CommonsWare Apr 23 '22 at 16:34
Not sure it is the best way, but I achieved this by overloading the TrackRenderer
.
I'm using videoPlayer.getBufferedPercentage()
, but you might be able to compute the percentage yourself as well, by just using TrackRenderer
's getBufferedPositionUs()
and getDurationUs()
public interface ProgressListener {
public void onProgressChange(long progress);
}
public class CustomVideoRenderer extends MediaCodecVideoTrackRenderer {
long progress = 0;
private final CopyOnWriteArraySet<ProgressListener> progressListeners = new CopyOnWriteArraySet();
// [...]
// Skipped constructors
// [...]
public void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
super.doSomeWork(positionUs, elapsedRealtimeUs);
long tmpProgress = videoPlayer.getBufferedPercentage();
if (tmpProgress != this.progress) {
this.progress = tmpProgress;
for (ProgressListener progressListener : this.progressListeners) {
progressListener.onProgressChange(progress);
}
}
}
public void addProgressListener(ProgressListener listener) {
this.progressListeners.add(listener);
}
}

- 312
- 3
- 14
To make it clear,there isn't a build in EventListener for the progress event, but you can call Handler.postDelayed inside you updateProgress() function to get the current progress
private void updateProgress(){
//get current progress
long position = player == null ? 0 : player.getCurrentPosition();
//updateProgress() will be called repeatedly, you can check
//player state to end it
handler.postDelayed(updateProgressAction,1000)
}
private final Runnable updateProgressAction = new Runnable() {
@Override
public void run() {
updateProgress();
}
};
for more details, see the source of PlaybackControlView.java inside Exoplayer

- 2,570
- 1
- 13
- 14
I'm not sure if it's the right approach, but I used EventBus and TimerTask
to update the progress of the audio being played.
In my MusicController class I put:
private void sendElapsedDuration() {
//To send the current elapsed time
final Timer t = new Timer();
Runnable r = new Runnable() {
@Override
public void run() {
t.schedule(new TimerTask() {
@Override
public void run() {
EventBus.getDefault().post(
new ProgressNotification(
player.getCurrentPosition(), player.getDuration())
);
if (player.getCurrentPosition() >= player.getDuration() ){
// The audio is ended, we pause the playback,
// and reset the position
player.seekTo(0);
player.setPlayWhenReady(false);
this.cancel();
// stopping the Runnable to avoid memory leak
mainHandler.removeCallbacks(this);
}
}
},0,1000);
}
};
if(player != null) {
if (player.getPlaybackState() != Player.STATE_ENDED)
mainHandler.postDelayed(r, 500);
else {
//We put the TimerTask to sleep when audio is not playing
t.cancel();
}
}
}
Then I called the method inside the onPlayerStateChanged
when adding the listener to my SimpleExoPlayer instance. The code above, sends the elapsed and total duration of the audio being played every 1 second (1000 ms) via the EventBus. Then inside the activity hosting the SeekBar:
@Subscribe(threadMode = ThreadMode.MAIN)
public void updateProgress(ProgressNotification pn) {
seekBar.setMax((int) pn.duration);
seekBar.setProgress((int) pn.currentPosition);
}

- 331
- 1
- 3
- 12
I had this problem too, and i found the solution on this link
But solution:
1. create a class like this:
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);
}
}
2. and finally use it in your code:
tracker = new ProgressTracker(player, new ProgressTracker.PositionListener() {
@Override
public void progress(long position) {
Log.i(TAG, "VideoViewActivity/progress: position=" + position);
}
});
3. in the last step dont forget call purgeHandler when you want release player (important)
tracker.purgeHandler();
player.release();
player = null;

- 711
- 7
- 19
Extend your current player class (SimpleExoPlayer for ex.) and add
public interface PlayerEventsListener {
void onSeek(int from, int to);
void onProgressUpdate(long progress);
}
private PlayerEventsListener mListener;
private Handler mHandler;
private Runnable mProgressUpdater;
private boolean isUpdatingProgress = false;
public SomePlayersConstructor(Activity activity, /*...*/) {
//...
mListener = (PlayerEventsListener) activity;
mHandler = new Handler();
mProgressUpdater = new ProgressUpdater();
}
// Here u gain access to seek events
@Override
public void seekTo(long positionMs) {
mListener.onSeek(-1, (int)positionMs/1000);
super.seekTo(positionMs);
}
@Override
public void seekTo(int windowIndex, long positionMs) {
mListener.onSeek((int)getCurrentPosition()/1000, (int)positionMs/1000);
super.seekTo(windowIndex, positionMs);
}
// Here u gain access to progress
public void startProgressUpdater() {
if (!isUpdatingProgress) {
mProgressUpdater.run();
isUpdatingProgress = true;
}
}
private class ProgressUpdater implements Runnable {
private static final int TIME_UPDATE_MS = 500;
@Override
public void run() {
mListener.onProgressUpdate(getCurrentPosition());
mHandler.postDelayed(mProgressUpdater, TIME_UPDATE_MS);
}
}
Then inside player activity just implement interface and start updates with player.startProgressUpdater();

- 353
- 3
- 8
If you want to accomplish this, just listen to onPositionDiscontinuity()
. It will give you information if the seekbar
is being scrub

- 734
- 1
- 16
- 42
if you are using the player view or the player control view fully or partially(for just the buttons or something) you could set progress listener directly from it:
PlayerControlView playerControlView = miniPlayerCardView.findViewById(R.id.playerView);
ProgressBar audioProgressBar = miniPlayerCardView.findViewById(R.id.audioProgressBar);
playerControlView.setProgressUpdateListener((position, bufferedPosition) -> {
int progressBarPosition = (int) ((position*100)/player.getDuration());
int bufferedProgressBarPosition = (int) ((bufferedPosition*100)/player.getDuration());
audioProgressBar.setProgress(progressBarPosition);
audioProgressBar.setSecondaryProgress(bufferedProgressBarPosition);
});

- 131
- 2
- 3
rx java implementation :
private val disposablesVideoControlsDisposable = CompositeDisposable()
fun showVideoControlsAndSimilarTray() {
videoSeekBar?.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) seekVideoProgress(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
val disposable = Observable.interval(0, 1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
calculateVideoProgress()
}
disposablesVideoControlsDisposable.add(disposable)
}
private fun calculateVideoProgress() {
val currentMill = exoPlayer.currentPosition
val totalMillis = exoPlayer.duration
if (totalMillis > 0L) {
val remainMillis = (totalMillis - currentMill).toFloat() / 1000
val remainMins = (remainMillis / 60).toInt()
val remainSecs = (remainMillis % 60).toInt()
videoProgressText.setText("$remainMins:${String.format("%02d", remainSecs)}")
seekBarProgress.set((currentMill.toFloat() / totalMillis * 100).toInt())
}
}
private fun seekVideoProgress(progress: Int) {
val seekMillis = exoPlayer.duration.toFloat() * progress / 100
exoPlayer.seekTo(seekMillis.toLong())
}
And finally when you are done :
fun disposeVideoControlsObservable() {
disposablesVideoControlsDisposable.clear()
}

- 497
- 3
- 8
Solution based on media3 PlayerControlView logic. Here is the function for identifying and updating the current position, and the entire code can be found in this GitHub Gist.
private fun updatePosition(player: Player? = this.player) {
handlerUpdatePosition.removeCallbacks(runnableUpdatePosition)
if (player == null) return
val position = player.currentPosition
playerPositionListener?.onPlayerPositionChanged(position)
val playbackState = player.playbackState
if (player.isPlaying)
handlerUpdatePosition.postDelayed(
runnableUpdatePosition,
player.playbackParameters.speed.let { playbackSpeed ->
if (playbackSpeed > 0) ((1000 - position % 1000) / playbackSpeed).toLong()
else MAX_UPDATE_INTERVAL_MS
}.coerceIn(MIN_UPDATE_INTERVAL_MS, MAX_UPDATE_INTERVAL_MS)
)
else if (playbackState != Player.STATE_ENDED && playbackState != Player.STATE_IDLE)
handlerUpdatePosition.postDelayed(runnableUpdatePosition, MAX_UPDATE_INTERVAL_MS)
}

- 111
- 1
- 6
Only use onTouchListener with the MotionEvent.ACTION_UP
SeekBar exo_progress = (SeekBar) findViewById(R.id.exo_progress);
exo_progress.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
//put your code here!!
}
return false;
}
});

- 612
- 8
- 9
This works at least with Exoplayer 2.
There is four playback states: STATE_IDLE, STATE_BUFFERING, STATE_READY and STATE_ENDED.
Checking playback state is easy to do. There is at least two solution: if-statement or switch-statement.
Whatever playback state is going on you can execute your method or set something else for example progressbar.
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ExoPlayer.STATE_ENDED) {
showControls();
Toast.makeText(getApplicationContext(), "Playback ended", Toast.LENGTH_LONG).show();
}
else if (playbackState == ExoPlayer.STATE_BUFFERING)
{
progressBar.setVisibility(View.VISIBLE);
Toast.makeText(getApplicationContext(), "Buffering..", Toast.LENGTH_SHORT).show();
}
else if (playbackState == ExoPlayer.STATE_READY)
{
progressBar.setVisibility(View.INVISIBLE);
}
}

- 13
- 2
it's simple
var player = SimpleExoPlayer.Builder(context).build();
player.addListener(object:Player.Listener{
override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
if (events.containsAny(
Player.EVENT_IS_LOADING_CHANGED,
Player.EVENT_PLAYBACK_STATE_CHANGED,
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_IS_PLAYING_CHANGED
)) {
log(msg="progres ${player.currentPosition}")
}
}
})
and you can view com.google.android.exoplayer2.ui.PlayerControlView.java at 1356 line

- 1,193
- 8
- 9