From 5baddfb56a9db55bd87e2dbc296fd9e28d1c9558 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 3 Oct 2017 02:46:04 -0700 Subject: [PATCH] Add onSeekProcessed callback to Player interface. This is useful to determine when a seek request was processed by the player and all playback state changes (mostly to BUFFERING) have been performed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170826793 --- .../android/exoplayer2/demo/EventLogger.java | 5 +++ .../exoplayer2/ext/cast/CastPlayer.java | 23 +++++++----- .../android/exoplayer2/ExoPlayerTest.java | 36 +++++++++++++++++++ .../android/exoplayer2/ExoPlayerImpl.java | 12 +++++-- .../exoplayer2/ExoPlayerImplInternal.java | 17 ++++----- .../com/google/android/exoplayer2/Player.java | 12 +++++++ 6 files changed, 85 insertions(+), 20 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 5c2b40e630..68f7ddfd21 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -209,6 +209,11 @@ import java.util.Locale; Log.d(TAG, "]"); } + @Override + public void onSeekProcessed() { + Log.d(TAG, "seekProcessed"); + } + // MetadataOutput @Override diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 9d3636f8ac..ffb06ed232 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -41,7 +41,6 @@ import com.google.android.gms.cast.framework.SessionManager; import com.google.android.gms.cast.framework.SessionManagerListener; import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult; -import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import java.util.List; @@ -115,6 +114,7 @@ public final class CastPlayer implements Player { private int currentWindowIndex; private boolean playWhenReady; private long lastReportedPositionMs; + private int pendingSeekCount; private int pendingSeekWindowIndex; private long pendingSeekPositionMs; @@ -333,11 +333,16 @@ public final class CastPlayer implements Player { } else { remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback); } + pendingSeekCount++; pendingSeekWindowIndex = windowIndex; pendingSeekPositionMs = positionMs; for (EventListener listener : listeners) { listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); } + } else if (pendingSeekCount == 0) { + for (EventListener listener : listeners) { + listener.onSeekProcessed(); + } } } @@ -536,7 +541,7 @@ public final class CastPlayer implements Player { } } int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); - if (this.currentWindowIndex != currentWindowIndex) { + if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { this.currentWindowIndex = currentWindowIndex; for (EventListener listener : listeners) { listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION); @@ -831,18 +836,18 @@ public final class CastPlayer implements Player { @Override public void onResult(@NonNull MediaChannelResult result) { int statusCode = result.getStatus().getStatusCode(); - if (statusCode == CastStatusCodes.REPLACED) { - // A seek was executed before this one completed. Do nothing. - } else { + if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) { + Log.e(TAG, "Seek failed. Error code " + statusCode + ": " + + CastUtils.getLogString(statusCode)); + } + if (--pendingSeekCount == 0) { pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; - if (statusCode != CommonStatusCodes.SUCCESS) { - Log.e(TAG, "Seek failed. Error code " + statusCode + ": " - + CastUtils.getLogString(statusCode)); + for (EventListener listener : listeners) { + listener.onSeekProcessed(); } } } - } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 9f172dc802..2971aaf779 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -28,6 +28,8 @@ import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import junit.framework.TestCase; @@ -259,4 +261,38 @@ public final class ExoPlayerTest extends TestCase { assertTrue(renderer.isEnded); } + public void testSeekProcessedCallback() throws Exception { + Timeline timeline = new FakeTimeline( + new TimelineWindowDefinition(true, false, 100000), + new TimelineWindowDefinition(true, false, 100000)); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") + // Initial seek before timeline preparation finished. + .pause().seek(10).waitForPlaybackState(Player.STATE_READY) + // Re-seek to same position, start playback and wait until playback reaches second window. + .seek(10).play().waitForPositionDiscontinuity() + // Seek twice in concession, expecting the first seek to be replaced. + .seek(5).seek(60).build(); + final List playbackStatesWhenSeekProcessed = new ArrayList<>(); + Player.EventListener eventListener = new Player.DefaultEventListener() { + private int currentPlaybackState = Player.STATE_IDLE; + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + currentPlaybackState = playbackState; + } + + @Override + public void onSeekProcessed() { + playbackStatesWhenSeekProcessed.add(currentPlaybackState); + } + }; + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setEventListener(eventListener).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + assertEquals(3, playbackStatesWhenSeekProcessed.size()); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(0)); + assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(1)); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 75e08aadc6..2222660469 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -482,10 +482,11 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingWindowIndex = 0; maskingWindowPositionMs = 0; } - if (msg.arg1 != 0) { - for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK); + for (Player.EventListener listener : listeners) { + if (msg.arg1 != 0) { + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_INTERNAL); } + listener.onSeekProcessed(); } } break; @@ -516,6 +517,11 @@ import java.util.concurrent.CopyOnWriteArraySet; listener.onTimelineChanged(timeline, manifest); } } + if (pendingSeekAcks == 0 && sourceInfo.seekAcks > 0) { + for (Player.EventListener listener : listeners) { + listener.onSeekProcessed(); + } + } break; } case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 838a44b4ee..765b2a7634 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -701,12 +701,12 @@ import java.io.IOException; timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - playbackInfo = new PlaybackInfo(firstPeriodIndex, 0); - eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to // (firstPeriodIndex,0) isn't ignored. playbackInfo = new PlaybackInfo(firstPeriodIndex, C.TIME_UNSET); setState(Player.STATE_ENDED); + eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, new PlaybackInfo(firstPeriodIndex, 0)) + .sendToTarget(); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); return; @@ -1031,7 +1031,7 @@ import java.io.IOException; MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs, positionUs); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest, playbackInfo, processedInitialSeekCount); } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { @@ -1182,22 +1182,23 @@ import java.io.IOException; int processedInitialSeekCount) { int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; - // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler. - playbackInfo = new PlaybackInfo(firstPeriodIndex, 0); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to // (firstPeriodIndex,0) isn't ignored. playbackInfo = new PlaybackInfo(firstPeriodIndex, C.TIME_UNSET); setState(Player.STATE_ENDED); + // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler. + notifySourceInfoRefresh(manifest, new PlaybackInfo(firstPeriodIndex, 0), + processedInitialSeekCount); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); } private void notifySourceInfoRefresh(Object manifest) { - notifySourceInfoRefresh(manifest, 0); + notifySourceInfoRefresh(manifest, playbackInfo, 0); } - private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) { + private void notifySourceInfoRefresh(Object manifest, PlaybackInfo playbackInfo, + int processedInitialSeekCount) { eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 3dd702b85f..983ef878f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -134,6 +134,13 @@ public interface Player { */ void onPlaybackParametersChanged(PlaybackParameters playbackParameters); + /** + * Called when all pending seek requests have been processed by the player. This is guaranteed + * to happen after any necessary changes to the player state were reported to + * {@link #onPlayerStateChanged(boolean, int)}. + */ + void onSeekProcessed(); + } /** @@ -186,6 +193,11 @@ public interface Player { // Do nothing. } + @Override + public void onSeekProcessed() { + // Do nothing. + } + } /**