diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 0510055942..90ba31c623 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -19,12 +19,17 @@
([#4788](https://github.com/google/ExoPlayer/issues/4788)).
* SubRip: Add support for alignment tags, and remove tags from the displayed
captions ([#4306](https://github.com/google/ExoPlayer/issues/4306)).
+* Fix issue where buffered position is not updated correctly when transitioning
+ between periods
+ ([#4899](https://github.com/google/ExoPlayer/issues/4899)).
+* IMA extension:
+ * For preroll to live stream transitions, project forward the loading position
+ to avoid being behind the live window.
### 2.9.0 ###
-* Turn on Java 8 compiler support for the ExoPlayer library. Apps that depend
- on ExoPlayer via its source code rather than an AAR may need to add
- `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their
+* Turn on Java 8 compiler support for the ExoPlayer library. Apps may need to
+ add `compileOptions { targetCompatibility JavaVersion.VERSION_1_8 }` to their
gradle settings to ensure bytecode compatibility.
* Set `compileSdkVersion` and `targetSdkVersion` to 28.
* Support for automatic audio focus handling via
@@ -346,18 +351,18 @@
begins, and poll the audio timestamp less frequently once it starts
advancing ([#3841](https://github.com/google/ExoPlayer/issues/3841)).
* Add an option to skip silent audio in `PlaybackParameters`
- ((#2635)[https://github.com/google/ExoPlayer/issues/2635]).
+ ([#2635](https://github.com/google/ExoPlayer/issues/2635)).
* Fix an issue where playback of TrueHD streams would get stuck after seeking
due to not finding a syncframe
- ((#3845)[https://github.com/google/ExoPlayer/issues/3845]).
+ ([#3845](https://github.com/google/ExoPlayer/issues/3845)).
* Fix an issue with eac3-joc playback where a codec would fail to configure
- ((#4165)[https://github.com/google/ExoPlayer/issues/4165]).
+ ([#4165](https://github.com/google/ExoPlayer/issues/4165)).
* Handle non-empty end-of-stream buffers, to fix gapless playback of streams
with encoder padding when the decoder returns a non-empty final buffer.
* Allow trimming more than one sample when applying an elst audio edit via
gapless playback info.
* Allow overriding skipping/scaling with custom `AudioProcessor`s
- ((#3142)[https://github.com/google/ExoPlayer/issues/3142]).
+ ([#3142](https://github.com/google/ExoPlayer/issues/3142)).
* Caching:
* Add release method to the `Cache` interface, and prevent multiple instances
of `SimpleCache` using the same folder at the same time.
diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
index 6395ea4c24..20e27d8d48 100644
--- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
+++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java
@@ -350,8 +350,7 @@ public class SampleChooserActivity extends Activity
? null
: new DrmInfo(drmScheme, drmLicenseUrl, drmKeyRequestProperties, drmMultiSession);
if (playlistSamples != null) {
- UriSample[] playlistSamplesArray = playlistSamples.toArray(
- new UriSample[playlistSamples.size()]);
+ UriSample[] playlistSamplesArray = playlistSamples.toArray(new UriSample[0]);
return new PlaylistSample(sampleName, drmInfo, playlistSamplesArray);
} else {
return new UriSample(
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
index 51e724bee1..73602d85aa 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
@@ -153,6 +153,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
onDisabled();
}
+ @Override
+ public final void reset() {
+ Assertions.checkState(state == STATE_DISABLED);
+ onReset();
+ }
+
// RendererCapabilities implementation.
@Override
@@ -247,6 +253,15 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
// Do nothing.
}
+ /**
+ * Called when the renderer is reset.
+ *
+ *
The default implementation is a no-op.
+ */
+ protected void onReset() {
+ // Do nothing.
+ }
+
// Methods to be called by subclasses.
/** Returns the formats of the currently enabled stream. */
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
index cc16c43b05..6ccda2b8e9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -187,7 +187,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
extensionRendererMode, renderersList);
buildCameraMotionRenderers(context, extensionRendererMode, renderersList);
buildMiscellaneousRenderers(context, eventHandler, extensionRendererMode, renderersList);
- return renderersList.toArray(new Renderer[renderersList.size()]);
+ return renderersList.toArray(new Renderer[0]);
}
/**
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 5fb6202d2f..e1f942147d 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
@@ -619,7 +619,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (playbackInfo.startPositionUs == C.TIME_UNSET) {
// Replace internal unset start position with externally visible start position of zero.
playbackInfo =
- playbackInfo.fromNewPosition(
+ playbackInfo.resetToNewPosition(
playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs);
}
if ((!this.playbackInfo.timeline.isEmpty() || hasPendingPrepare)
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 6810c86558..0c6c3ca202 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
@@ -448,7 +448,11 @@ import java.util.Collections;
seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true);
if (newPositionUs != playbackInfo.positionUs) {
playbackInfo =
- playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs);
+ playbackInfo.copyWithNewPosition(
+ periodId,
+ newPositionUs,
+ playbackInfo.contentPositionUs,
+ getTotalBufferedDurationUs());
if (sendDiscontinuity) {
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
}
@@ -483,8 +487,12 @@ import java.util.Collections;
// A MediaPeriod may report a discontinuity at the current playback position to ensure the
// renderers are flushed. Only report the discontinuity externally if the position changed.
if (periodPositionUs != playbackInfo.positionUs) {
- playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs,
- playbackInfo.contentPositionUs);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ playbackInfo.periodId,
+ periodPositionUs,
+ playbackInfo.contentPositionUs,
+ getTotalBufferedDurationUs());
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
}
} else {
@@ -496,10 +504,8 @@ import java.util.Collections;
// Update the buffered position and total buffered duration.
MediaPeriodHolder loadingPeriod = queue.getLoadingPeriod();
- playbackInfo.bufferedPositionUs =
- loadingPeriod.getBufferedPositionUs(/* convertEosToDuration= */ true);
- playbackInfo.totalBufferedDurationUs =
- playbackInfo.bufferedPositionUs - loadingPeriod.toPeriodTime(rendererPositionUs);
+ playbackInfo.bufferedPositionUs = loadingPeriod.getBufferedPositionUs();
+ playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();
}
private void doSomeWork() throws ExoPlaybackException, IOException {
@@ -647,7 +653,9 @@ import java.util.Collections;
periodPositionUs = newPeriodPositionUs;
}
} finally {
- playbackInfo = playbackInfo.fromNewPosition(periodId, periodPositionUs, contentPositionUs);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ periodId, periodPositionUs, contentPositionUs, getTotalBufferedDurationUs());
if (seekPositionAdjusted) {
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
}
@@ -981,6 +989,7 @@ import java.util.Collections;
mediaClock.onRendererDisabled(renderer);
ensureStopped(renderer);
renderer.disable();
+ renderer.reset();
}
private void reselectTracksInternal() throws ExoPlaybackException {
@@ -993,7 +1002,7 @@ import java.util.Collections;
MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
boolean selectionsChangedForReadPeriod = true;
- TrackSelectorResult newTrackSelectorResult = null;
+ TrackSelectorResult newTrackSelectorResult;
while (true) {
if (periodHolder == null || !periodHolder.prepared) {
// The reselection did not change any prepared periods.
@@ -1022,8 +1031,12 @@ import java.util.Collections;
newTrackSelectorResult, playbackInfo.positionUs, recreateStreams, streamResetFlags);
if (playbackInfo.playbackState != Player.STATE_ENDED
&& periodPositionUs != playbackInfo.positionUs) {
- playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs,
- playbackInfo.contentPositionUs);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ playbackInfo.periodId,
+ periodPositionUs,
+ playbackInfo.contentPositionUs,
+ getTotalBufferedDurationUs());
playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
resetRendererPosition(periodPositionUs);
}
@@ -1097,12 +1110,10 @@ import java.util.Collections;
}
// Renderers are ready and we're loading. Ask the LoadControl whether to transition.
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
- long bufferedPositionUs = loadingHolder.getBufferedPositionUs(!loadingHolder.info.isFinal);
- return bufferedPositionUs == C.TIME_END_OF_SOURCE
+ boolean bufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
+ return bufferedToEnd
|| loadControl.shouldStartPlayback(
- bufferedPositionUs - loadingHolder.toPeriodTime(rendererPositionUs),
- mediaClock.getPlaybackParameters().speed,
- rebuffering);
+ getTotalBufferedDurationUs(), mediaClock.getPlaybackParameters().speed, rebuffering);
}
private boolean isTimelineReady() {
@@ -1167,7 +1178,7 @@ import java.util.Collections;
resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
} catch (IllegalSeekPositionException e) {
playbackInfo =
- playbackInfo.fromNewPosition(getFirstMediaPeriodId(), C.TIME_UNSET, C.TIME_UNSET);
+ playbackInfo.resetToNewPosition(getFirstMediaPeriodId(), C.TIME_UNSET, C.TIME_UNSET);
throw e;
}
pendingInitialSeekPosition = null;
@@ -1180,7 +1191,7 @@ import java.util.Collections;
long positionUs = periodPosition.second;
MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodUid, positionUs);
playbackInfo =
- playbackInfo.fromNewPosition(
+ playbackInfo.resetToNewPosition(
periodId, periodId.isAd() ? 0 : positionUs, /* contentPositionUs= */ positionUs);
}
} else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
@@ -1194,7 +1205,7 @@ import java.util.Collections;
long startPositionUs = defaultPosition.second;
MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodUid, startPositionUs);
playbackInfo =
- playbackInfo.fromNewPosition(
+ playbackInfo.resetToNewPosition(
periodId,
periodId.isAd() ? 0 : startPositionUs,
/* contentPositionUs= */ startPositionUs);
@@ -1213,7 +1224,7 @@ import java.util.Collections;
long startPositionUs = defaultPosition.second;
MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodUid, startPositionUs);
playbackInfo =
- playbackInfo.fromNewPosition(
+ playbackInfo.resetToNewPosition(
periodId,
/* startPositionUs= */ periodId.isAd() ? 0 : startPositionUs,
/* contentPositionUs= */ startPositionUs);
@@ -1252,7 +1263,9 @@ import java.util.Collections;
}
// Actually do the seek.
long seekPositionUs = seekToPeriodPosition(periodId, periodId.isAd() ? 0 : contentPositionUs);
- playbackInfo = playbackInfo.fromNewPosition(periodId, seekPositionUs, contentPositionUs);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ periodId, seekPositionUs, contentPositionUs, getTotalBufferedDurationUs());
return;
}
@@ -1264,7 +1277,9 @@ import java.util.Collections;
// The previously playing ad should no longer be played, so skip it.
long seekPositionUs =
seekToPeriodPosition(periodId, periodId.isAd() ? 0 : contentPositionUs);
- playbackInfo = playbackInfo.fromNewPosition(periodId, seekPositionUs, contentPositionUs);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ periodId, seekPositionUs, contentPositionUs, getTotalBufferedDurationUs());
return;
}
}
@@ -1420,8 +1435,12 @@ import java.util.Collections;
MediaPeriodHolder oldPlayingPeriodHolder = playingPeriodHolder;
playingPeriodHolder = queue.advancePlayingPeriod();
updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
- playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id,
- playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs);
+ playbackInfo =
+ playbackInfo.copyWithNewPosition(
+ playingPeriodHolder.info.id,
+ playingPeriodHolder.info.startPositionUs,
+ playingPeriodHolder.info.contentPositionUs,
+ getTotalBufferedDurationUs());
playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);
updatePlaybackPositions();
advancedPlayingPeriod = true;
@@ -1574,7 +1593,7 @@ import java.util.Collections;
return;
}
long bufferedDurationUs =
- nextLoadPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
+ getTotalBufferedDurationUs(/* bufferedPositionInLoadingPeriodUs= */ nextLoadPositionUs);
boolean continueLoading =
loadControl.shouldContinueLoading(
bufferedDurationUs, mediaClock.getPlaybackParameters().speed);
@@ -1671,6 +1690,11 @@ import java.util.Collections;
if (loadingMediaPeriodChanged) {
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(loadingMediaPeriodId);
}
+ playbackInfo.bufferedPositionUs =
+ loadingMediaPeriodHolder == null
+ ? playbackInfo.positionUs
+ : loadingMediaPeriodHolder.getBufferedPositionUs();
+ playbackInfo.totalBufferedDurationUs = getTotalBufferedDurationUs();
if ((loadingMediaPeriodChanged || loadingTrackSelectionChanged)
&& loadingMediaPeriodHolder != null
&& loadingMediaPeriodHolder.prepared) {
@@ -1680,6 +1704,17 @@ import java.util.Collections;
}
}
+ private long getTotalBufferedDurationUs() {
+ return getTotalBufferedDurationUs(playbackInfo.bufferedPositionUs);
+ }
+
+ private long getTotalBufferedDurationUs(long bufferedPositionInLoadingPeriodUs) {
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
+ return loadingPeriodHolder == null
+ ? 0
+ : bufferedPositionInLoadingPeriodUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
+ }
+
private void updateLoadControlTrackSelection(
TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) {
loadControl.onTracksSelected(renderers, trackGroups, trackSelectorResult.selections);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
index 3ba96d2b80..4fc21475d9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
@@ -133,23 +133,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
/**
- * Returns the buffered position in microseconds. If the period is buffered to the end then
- * {@link C#TIME_END_OF_SOURCE} is returned unless {@code convertEosToDuration} is true, in which
- * case the period duration is returned.
+ * Returns the buffered position in microseconds. If the period is buffered to the end, then the
+ * period duration is returned.
*
- * @param convertEosToDuration Whether to return the period duration rather than
- * {@link C#TIME_END_OF_SOURCE} if the period is fully buffered.
* @return The buffered position in microseconds.
*/
- public long getBufferedPositionUs(boolean convertEosToDuration) {
+ public long getBufferedPositionUs() {
if (!prepared) {
return info.startPositionUs;
}
long bufferedPositionUs =
hasEnabledTracks ? mediaPeriod.getBufferedPositionUs() : C.TIME_END_OF_SOURCE;
- return bufferedPositionUs == C.TIME_END_OF_SOURCE && convertEosToDuration
- ? info.durationUs
- : bufferedPositionUs;
+ return bufferedPositionUs == C.TIME_END_OF_SOURCE ? info.durationUs : bufferedPositionUs;
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
index 6370299334..40bc658953 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
@@ -533,6 +533,11 @@ import com.google.android.exoplayer2.util.Assertions;
// until the timeline is updated. Store whether the next timeline period is ready when the
// timeline is updated, to avoid repeatedly checking the same timeline.
MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info;
+ // The expected delay until playback transitions to the new period is equal the duration of
+ // media that's currently buffered (assuming no interruptions). This is used to project forward
+ // the start position for transitions to new windows.
+ long bufferedDurationUs =
+ mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs;
if (mediaPeriodInfo.isLastInTimelinePeriod) {
int currentPeriodIndex = timeline.getIndexOfPeriod(mediaPeriodInfo.id.periodUid);
int nextPeriodIndex =
@@ -550,19 +555,15 @@ import com.google.android.exoplayer2.util.Assertions;
long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber;
if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) {
// We're starting to buffer a new window. When playback transitions to this window we'll
- // want it to be from its default start position. The expected delay until playback
- // transitions is equal the duration of media that's currently buffered (assuming no
- // interruptions). Hence we project the default start position forward by the duration of
- // the buffer, and start buffering from this point.
- long defaultPositionProjectionUs =
- mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs;
+ // want it to be from its default start position, so project the default start position
+ // forward by the duration of the buffer, and start buffering from this point.
Pair