diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 81a45c9c24..6438cbdd68 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,10 @@ ### dev-v2 (not yet released) ### +* Fix reporting of internal position discontinuities via + `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is + added to disambiguate position adjustments during seeks from other types of + internal position discontinuity. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, 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 d72f747940..9233b016f5 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 @@ -499,6 +499,8 @@ import java.util.Locale; return "PERIOD_TRANSITION"; case Player.DISCONTINUITY_REASON_SEEK: return "SEEK"; + case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + return "SEEK_ADJUSTMENT"; case Player.DISCONTINUITY_REASON_INTERNAL: return "INTERNAL"; default: 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 95d5d96163..f0f1c23c2b 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 @@ -23,12 +23,14 @@ import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; 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.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; +import com.google.android.exoplayer2.upstream.Allocator; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -56,7 +58,7 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(); assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.bufferReadCount); @@ -73,7 +75,7 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setManifest(manifest).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); testRunner.assertManifestsEqual(manifest); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); @@ -91,7 +93,9 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); assertEquals(3, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); @@ -136,7 +140,9 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setRenderers(videoRenderer, audioRenderer) .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); assertEquals(1, audioRenderer.positionResetCount); assertTrue(videoRenderer.isEnded); @@ -198,7 +204,7 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setMediaSource(firstSource).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertNoPositionDiscontinuities(); // The first source's preparation completed with a non-empty timeline. When the player was // re-prepared with the second source, it immediately exposed an empty timeline, but the source // info refresh from the second source was suppressed as we re-prepared with the third source. @@ -226,6 +232,16 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); assertTrue(renderer.isEnded); } @@ -250,6 +266,12 @@ public final class ExoPlayerTest extends TestCase { .setMediaSource(mediaSource).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); assertTrue(renderer.isEnded); } @@ -300,6 +322,63 @@ public final class ExoPlayerTest extends TestCase { assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); } + public void testSeekDiscontinuity() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuity") + .seek(10).build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setTimeline(timeline) + .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + } + + public void testSeekDiscontinuityWithAdjustment() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, + TrackGroupArray trackGroupArray, Allocator allocator) { + return new FakeMediaPeriod(trackGroupArray) { + @Override + public long seekToUs(long positionUs) { + return positionUs + 10; // Adjusts the requested seek position. + } + }; + } + }; + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuityAdjust") + .waitForPlaybackState(Player.STATE_READY).seek(10).build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource) + .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK, + Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); + } + + public void testInternalDiscontinuity() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, + TrackGroupArray trackGroupArray, Allocator allocator) { + return new FakeMediaPeriod(trackGroupArray) { + boolean discontinuityRead; + @Override + public long readDiscontinuity() { + if (!discontinuityRead) { + discontinuityRead = true; + return 10; // Return a discontinuity. + } + return C.TIME_UNSET; + } + }; + } + }; + ActionSchedule actionSchedule = new ActionSchedule.Builder("testInternalDiscontinuity") + .waitForPlaybackState(Player.STATE_READY).build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource) + .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_INTERNAL); + } + public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = 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 d28f72e739..ff00f9de91 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 @@ -467,7 +467,8 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { int prepareAcks = msg.arg1; int seekAcks = msg.arg2; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false); + handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL); break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { @@ -485,11 +486,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_SEEK_ACK: { boolean seekPositionAdjusted = msg.arg1 != 0; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted); + handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted, + DISCONTINUITY_REASON_SEEK_ADJUSTMENT); break; } case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: { - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true); + @DiscontinuityReason int discontinuityReason = msg.arg1; + handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true, discontinuityReason); break; } case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { @@ -515,7 +518,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareAcks, int seekAcks, - boolean positionDiscontinuity) { + boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { Assertions.checkNotNull(playbackInfo.timeline); pendingPrepareAcks -= prepareAcks; pendingSeekAcks -= seekAcks; @@ -536,9 +539,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } if (positionDiscontinuity) { for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity( - seekAcks > 0 ? DISCONTINUITY_REASON_INTERNAL : DISCONTINUITY_REASON_PERIOD_TRANSITION - ); + listener.onPositionDiscontinuity(positionDiscontinuityReason); } } } 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 e4bb11c51f..d7b2b4cbf4 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 @@ -514,6 +514,10 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); @@ -875,7 +879,10 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); if (periodPositionUs != playbackInfo.positionUs) { - playbackInfo.positionUs = periodPositionUs; + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); resetRendererPosition(periodPositionUs); } @@ -1262,7 +1269,8 @@ import java.io.IOException; playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, 0, playbackInfo).sendToTarget(); } if (readingPeriodHolder.info.isFinal) { 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 af653ec2bd..dc703f924a 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 @@ -243,7 +243,7 @@ public interface Player { */ @Retention(RetentionPolicy.SOURCE) @IntDef({DISCONTINUITY_REASON_PERIOD_TRANSITION, DISCONTINUITY_REASON_SEEK, - DISCONTINUITY_REASON_INTERNAL}) + DISCONTINUITY_REASON_SEEK_ADJUSTMENT, DISCONTINUITY_REASON_INTERNAL}) public @interface DiscontinuityReason {} /** * Automatic playback transition from one period in the timeline to the next. The period index may @@ -254,10 +254,15 @@ public interface Player { * Seek within the current period or to another period. */ int DISCONTINUITY_REASON_SEEK = 1; + /** + * Seek adjustment due to being unable to seek to the requested position or because the seek was + * permitted to be inexact. + */ + int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; /** * Discontinuity introduced internally by the source. */ - int DISCONTINUITY_REASON_INTERNAL = 2; + int DISCONTINUITY_REASON_INTERNAL = 3; /** * Register a listener to receive events from the player. The listener's methods will be called on diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 591e63dc5b..6730bf1c7f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; @@ -38,6 +39,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -261,14 +263,14 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public ExoPlayerTestRunner build() { if (supportedFormats == null) { - supportedFormats = new Format[] { VIDEO_FORMAT }; + supportedFormats = new Format[] {VIDEO_FORMAT}; } if (trackSelector == null) { trackSelector = new DefaultTrackSelector(); } if (renderersFactory == null) { if (renderers == null) { - renderers = new Renderer[] { new FakeRenderer(supportedFormats) }; + renderers = new Renderer[] {new FakeRenderer(supportedFormats)}; } renderersFactory = new RenderersFactory() { @Override @@ -317,11 +319,11 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private final LinkedList timelines; private final LinkedList manifests; private final LinkedList periodIndices; + private final ArrayList discontinuityReasons; private SimpleExoPlayer player; private Exception exception; private TrackGroupArray trackGroups; - private int positionDiscontinuityCount; private boolean playerWasPrepared; private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, @@ -337,6 +339,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.timelines = new LinkedList<>(); this.manifests = new LinkedList<>(); this.periodIndices = new LinkedList<>(); + this.discontinuityReasons = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(1); this.playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); @@ -439,13 +442,24 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } /** - * Asserts that the number of reported discontinuities by - * {@link Player.EventListener#onPositionDiscontinuity(int)} is equal to the provided number. - * - * @param expectedCount The expected number of position discontinuities. + * Asserts that {@link Player.EventListener#onPositionDiscontinuity(int)} was not called. */ - public void assertPositionDiscontinuityCount(int expectedCount) { - Assert.assertEquals(expectedCount, positionDiscontinuityCount); + public void assertNoPositionDiscontinuities() { + Assert.assertTrue(discontinuityReasons.isEmpty()); + } + + /** + * Asserts that the discontinuity reasons reported by + * {@link Player.EventListener#onPositionDiscontinuity(int)} are equal to the provided values. + * + * @param discontinuityReasons The expected discontinuity reasons. + */ + public void assertPositionDiscontinuityReasonsEqual( + @DiscontinuityReason int... discontinuityReasons) { + Assert.assertEquals(discontinuityReasons.length, this.discontinuityReasons.size()); + for (int i = 0; i < discontinuityReasons.length; i++) { + Assert.assertEquals(discontinuityReasons[i], (int) this.discontinuityReasons.get(i)); + } } /** @@ -522,7 +536,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - positionDiscontinuityCount++; + discontinuityReasons.add(reason); periodIndices.add(player.getCurrentPeriodIndex()); }