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
This commit is contained in:
tonihei 2017-10-03 02:46:04 -07:00 committed by Oliver Woodman
parent 1495b9a473
commit 5baddfb56a
6 changed files with 85 additions and 20 deletions

View File

@ -209,6 +209,11 @@ import java.util.Locale;
Log.d(TAG, "]");
}
@Override
public void onSeekProcessed() {
Log.d(TAG, "seekProcessed");
}
// MetadataOutput
@Override

View File

@ -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 {
pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET;
if (statusCode != CommonStatusCodes.SUCCESS) {
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;
for (EventListener listener : listeners) {
listener.onSeekProcessed();
}
}
}
}
}

View File

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

View File

@ -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);
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: {

View File

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

View File

@ -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.
}
}
/**