49

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.

Aleksandar G
  • 1,163
  • 2
  • 20
  • 25
ascallonisi
  • 871
  • 1
  • 11
  • 20

16 Answers16

38

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!

Sergey Glotov
  • 20,200
  • 11
  • 84
  • 98
Ankit Aggarwal
  • 2,367
  • 24
  • 30
  • 1
    Which 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
20

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.
SANAT
  • 8,489
  • 55
  • 66
14

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()
Link182
  • 733
  • 6
  • 15
Felipe Roriz
  • 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
4

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
            }
        }
abhi
  • 961
  • 9
  • 13
3

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);
    }

}
Nezo
  • 312
  • 3
  • 14
2

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

ufxmeng
  • 2,570
  • 1
  • 13
  • 14
2

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);
    }
Javad Arjmandi
  • 331
  • 1
  • 3
  • 12
2

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;
m.sajjad.s
  • 711
  • 7
  • 19
0

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();

Sough
  • 353
  • 3
  • 8
0

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

Raditya Kurnianto
  • 734
  • 1
  • 16
  • 42
0

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);
        });
Mikias
  • 131
  • 2
  • 3
0

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()
}
ROHIT LIEN
  • 497
  • 3
  • 8
0

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)
}
whitipet
  • 111
  • 1
  • 6
-1

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;
                }
            });
-2

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);
    }

}
-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

HongSec Park
  • 1,193
  • 8
  • 9