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 defaultPosition = timeline.getPeriodPosition( window, period, nextWindowIndex, - C.TIME_UNSET, - Math.max(0, defaultPositionProjectionUs)); + /* windowPositionUs= */ C.TIME_UNSET, + /* defaultPositionProjectionUs= */ Math.max(0, bufferedDurationUs)); if (defaultPosition == null) { return null; } @@ -603,11 +604,27 @@ import com.google.android.exoplayer2.util.Assertions; mediaPeriodInfo.contentPositionUs, currentPeriodId.windowSequenceNumber); } else { - // Play content from the ad group position. + // Play content from the ad group position. As a special case, if we're transitioning from a + // preroll ad group to content and there are no other ad groups, project the start position + // forward as if this were a transition to a new window. No attempt is made to handle + // midrolls in live streams, as it's unclear what content position should play after an ad + // (server-side dynamic ad insertion is more appropriate for this use case). + long startPositionUs = mediaPeriodInfo.contentPositionUs; + if (period.getAdGroupCount() == 1 && period.getAdGroupTimeUs(0) == 0) { + Pair defaultPosition = + timeline.getPeriodPosition( + window, + period, + period.windowIndex, + /* windowPositionUs= */ C.TIME_UNSET, + /* defaultPositionProjectionUs= */ Math.max(0, bufferedDurationUs)); + if (defaultPosition == null) { + return null; + } + startPositionUs = defaultPosition.second; + } return getMediaPeriodInfoForContent( - currentPeriodId.periodUid, - mediaPeriodInfo.contentPositionUs, - currentPeriodId.windowSequenceNumber); + currentPeriodId.periodUid, startPositionUs, currentPeriodId.windowSequenceNumber); } } else if (mediaPeriodInfo.id.endPositionUs != C.TIME_END_OF_SOURCE) { // Play the next ad group if it's available. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 45d6537b84..6645850d3b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -158,6 +158,12 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities onDisabled(); } + @Override + public final void reset() { + Assertions.checkState(state == STATE_DISABLED); + onReset(); + } + @Override public boolean isReady() { return true; @@ -260,6 +266,15 @@ public abstract class NoSampleRenderer 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. /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index 02058c0484..8c73fde3be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.CheckResult; import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -40,7 +41,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; public final MediaPeriodId periodId; /** * The start position at which playback started in {@link #periodId} relative to the start of the - * associated period in the {@link #timeline}, in microseconds. + * associated period in the {@link #timeline}, in microseconds. Note that this value changes for + * each position discontinuity. */ public final long startPositionUs; /** @@ -103,6 +105,23 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; startPositionUs); } + /** + * Create playback info. + * + * @param timeline See {@link #timeline}. + * @param manifest See {@link #manifest}. + * @param periodId See {@link #periodId}. + * @param startPositionUs See {@link #startPositionUs}. + * @param contentPositionUs See {@link #contentPositionUs}. + * @param playbackState See {@link #playbackState}. + * @param isLoading See {@link #isLoading}. + * @param trackGroups See {@link #trackGroups}. + * @param trackSelectorResult See {@link #trackSelectorResult}. + * @param loadingMediaPeriodId See {@link #loadingMediaPeriodId}. + * @param bufferedPositionUs See {@link #bufferedPositionUs}. + * @param totalBufferedDurationUs See {@link #totalBufferedDurationUs}. + * @param positionUs See {@link #positionUs}. + */ public PlaybackInfo( Timeline timeline, @Nullable Object manifest, @@ -132,7 +151,17 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; this.positionUs = positionUs; } - public PlaybackInfo fromNewPosition( + /** + * Copies playback info and resets playing and loading position. + * + * @param periodId New playing and loading {@link MediaPeriodId}. + * @param startPositionUs New start position. See {@link #startPositionUs}. + * @param contentPositionUs New content position. See {@link #contentPositionUs}. Value is ignored + * if {@code periodId.isAd()} is true. + * @return Copied playback info with reset position. + */ + @CheckResult + public PlaybackInfo resetToNewPosition( MediaPeriodId periodId, long startPositionUs, long contentPositionUs) { return new PlaybackInfo( timeline, @@ -150,6 +179,46 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; startPositionUs); } + /** + * Copied playback info with new playing position. + * + * @param periodId New playing media period. See {@link #periodId}. + * @param positionUs New position. See {@link #positionUs}. + * @param contentPositionUs New content position. See {@link #contentPositionUs}. Value is ignored + * if {@code periodId.isAd()} is true. + * @param totalBufferedDurationUs New buffered duration. See {@link #totalBufferedDurationUs}. + * @return Copied playback info with new playing position. + */ + @CheckResult + public PlaybackInfo copyWithNewPosition( + MediaPeriodId periodId, + long positionUs, + long contentPositionUs, + long totalBufferedDurationUs) { + return new PlaybackInfo( + timeline, + manifest, + periodId, + positionUs, + periodId.isAd() ? contentPositionUs : C.TIME_UNSET, + playbackState, + isLoading, + trackGroups, + trackSelectorResult, + loadingMediaPeriodId, + bufferedPositionUs, + totalBufferedDurationUs, + positionUs); + } + + /** + * Copies playback info with new timeline and manifest. + * + * @param timeline New timeline. See {@link #timeline}. + * @param manifest New manifest. See {@link #manifest}. + * @return Copied playback info with new timeline and manifest. + */ + @CheckResult public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) { return new PlaybackInfo( timeline, @@ -167,6 +236,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; positionUs); } + /** + * Copies playback info with new playback state. + * + * @param playbackState New playback state. See {@link #playbackState}. + * @return Copied playback info with new playback state. + */ + @CheckResult public PlaybackInfo copyWithPlaybackState(int playbackState) { return new PlaybackInfo( timeline, @@ -184,6 +260,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; positionUs); } + /** + * Copies playback info with new loading state. + * + * @param isLoading New loading state. See {@link #isLoading}. + * @return Copied playback info with new loading state. + */ + @CheckResult public PlaybackInfo copyWithIsLoading(boolean isLoading) { return new PlaybackInfo( timeline, @@ -201,6 +284,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; positionUs); } + /** + * Copies playback info with new track information. + * + * @param trackGroups New track groups. See {@link #trackGroups}. + * @param trackSelectorResult New track selector result. See {@link #trackSelectorResult}. + * @return Copied playback info with new track information. + */ + @CheckResult public PlaybackInfo copyWithTrackInfo( TrackGroupArray trackGroups, TrackSelectorResult trackSelectorResult) { return new PlaybackInfo( @@ -219,6 +310,13 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; positionUs); } + /** + * Copies playback info with new loading media period. + * + * @param loadingMediaPeriodId New loading media period id. See {@link #loadingMediaPeriodId}. + * @return Copied playback info with new loading media period. + */ + @CheckResult public PlaybackInfo copyWithLoadingMediaPeriodId(MediaPeriodId loadingMediaPeriodId) { return new PlaybackInfo( timeline, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index c6456e5f7f..1d4d587aeb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -44,12 +44,16 @@ public interface Renderer extends PlayerMessage.Target { @IntDef({STATE_DISABLED, STATE_ENABLED, STATE_STARTED}) @interface State {} /** - * The renderer is disabled. + * The renderer is disabled. A renderer in this state may hold resources that it requires for + * rendering (e.g. media decoders), for use if it's subsequently enabled. {@link #reset()} can be + * called to force the renderer to release these resources. */ int STATE_DISABLED = 0; /** - * The renderer is enabled but not started. A renderer in this state is not actively rendering - * media, but will typically hold resources that it requires for rendering (e.g. media decoders). + * The renderer is enabled but not started. A renderer in this state may render media at the + * current position (e.g. an initial video frame), but the position will not advance. A renderer + * in this state will typically hold resources that it requires for rendering (e.g. media + * decoders). */ int STATE_ENABLED = 1; /** @@ -279,4 +283,12 @@ public interface Renderer extends PlayerMessage.Target { */ void disable(); + /** + * Forces the renderer to give up any resources (e.g. media decoders) that it may be holding. If + * the renderer is not holding any resources, the call is a no-op. + * + *

This method may be called when the renderer is in the following states: {@link + * #STATE_DISABLED}. + */ + void reset(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 4ce34ad41a..6ad56a78d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -351,8 +351,7 @@ public final class DefaultAudioSink implements AudioSink { channelMappingAudioProcessor, trimmingAudioProcessor); Collections.addAll(toIntPcmAudioProcessors, audioProcessorChain.getAudioProcessors()); - toIntPcmAvailableAudioProcessors = - toIntPcmAudioProcessors.toArray(new AudioProcessor[toIntPcmAudioProcessors.size()]); + toIntPcmAvailableAudioProcessors = toIntPcmAudioProcessors.toArray(new AudioProcessor[0]); toFloatPcmAvailableAudioProcessors = new AudioProcessor[] {new FloatResamplingAudioProcessor()}; volume = 1.0f; startMediaTimeState = START_NOT_SET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index ef96c7ae75..ff01cbc2b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -97,7 +97,7 @@ public final class DrmInitData implements Comparator, Parcelable { * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(List schemeDatas) { - this(null, false, schemeDatas.toArray(new SchemeData[schemeDatas.size()])); + this(null, false, schemeDatas.toArray(new SchemeData[0])); } /** @@ -105,7 +105,7 @@ public final class DrmInitData implements Comparator, Parcelable { * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(@Nullable String schemeType, List schemeDatas) { - this(schemeType, false, schemeDatas.toArray(new SchemeData[schemeDatas.size()])); + this(schemeType, false, schemeDatas.toArray(new SchemeData[0])); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java index 3b0b834427..022b5b4c75 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/BinarySearchSeeker.java @@ -81,8 +81,9 @@ public abstract class BinarySearchSeeker { */ public static final class OutputFrameHolder { + public final ByteBuffer byteBuffer; + public long timeUs; - public ByteBuffer byteBuffer; /** Constructs an instance, wrapping the given byte buffer. */ public OutputFrameHolder(ByteBuffer outputByteBuffer) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java index 63fee48771..86b750e821 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java @@ -157,6 +157,7 @@ public final class MatroskaExtractor implements Extractor { private static final int ID_FLAG_DEFAULT = 0x88; private static final int ID_FLAG_FORCED = 0x55AA; private static final int ID_DEFAULT_DURATION = 0x23E383; + private static final int ID_NAME = 0x536E; private static final int ID_CODEC_ID = 0x86; private static final int ID_CODEC_PRIVATE = 0x63A2; private static final int ID_CODEC_DELAY = 0x56AA; @@ -815,6 +816,9 @@ public final class MatroskaExtractor implements Extractor { throw new ParserException("DocType " + value + " not supported"); } break; + case ID_NAME: + currentTrack.name = value; + break; case ID_CODEC_ID: currentTrack.codecId = value; break; @@ -1463,6 +1467,7 @@ public final class MatroskaExtractor implements Extractor { case ID_MAX_FALL: return TYPE_UNSIGNED_INT; case ID_DOC_TYPE: + case ID_NAME: case ID_CODEC_ID: case ID_LANGUAGE: return TYPE_STRING; @@ -1609,6 +1614,7 @@ public final class MatroskaExtractor implements Extractor { private static final int DEFAULT_MAX_FALL = 200; // nits. // Common elements. + public String name; public String codecId; public int number; public int type; @@ -1833,10 +1839,34 @@ public final class MatroskaExtractor implements Extractor { byte[] hdrStaticInfo = getHdrStaticInfo(); colorInfo = new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo); } - format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null, - Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData, - Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, colorInfo, - drmInitData); + int rotationDegrees = Format.NO_VALUE; + // Some HTC devices signal rotation in track names. + if ("htc_video_rotA-000".equals(name)) { + rotationDegrees = 0; + } else if ("htc_video_rotA-090".equals(name)) { + rotationDegrees = 90; + } else if ("htc_video_rotA-180".equals(name)) { + rotationDegrees = 180; + } else if ("htc_video_rotA-270".equals(name)) { + rotationDegrees = 270; + } + format = + Format.createVideoSampleFormat( + Integer.toString(trackId), + mimeType, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + maxInputSize, + width, + height, + /* frameRate= */ Format.NO_VALUE, + initializationData, + rotationDegrees, + pixelWidthHeightRatio, + projectionData, + stereoMode, + colorInfo, + drmInitData); } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { type = C.TRACK_TYPE_TEXT; format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, selectionFlags, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java index 17c82c2c5b..ec24bed964 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java @@ -423,7 +423,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { } this.firstVideoTrackIndex = firstVideoTrackIndex; this.durationUs = durationUs; - this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); + this.tracks = tracks.toArray(new Mp4Track[0]); accumulatedSampleSizes = calculateAccumulatedSampleSizes(this.tracks); extractorOutput.endTracks(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 87d56fdf3d..27b0d89a3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -546,9 +546,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { inputStreamEnded = false; outputStreamEnded = false; - if (codec != null) { - flushCodec(); - } + flushOrReinitCodec(); formatQueue.clear(); } @@ -560,6 +558,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override protected void onDisabled() { + if (drmSession != null || pendingDrmSession != null) { + // TODO: Do something better with this case. + onReset(); + } else { + flushOrReleaseCodec(); + } + } + + @Override + protected void onReset() { format = null; availableCodecInfos = null; try { @@ -687,10 +695,36 @@ public abstract class MediaCodecRenderer extends BaseRenderer { decoderCounters.ensureUpdated(); } - protected void flushCodec() throws ExoPlaybackException { + /** + * Flushes the codec. If flushing is not possible, the codec will be released and re-instantiated. + * This method is a no-op if the codec is {@code null}. + * + *

The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link + * #maybeInitCodec()} if the codec needs to be re-instantiated. + * + * @throws ExoPlaybackException If an error occurs re-instantiating the codec. + */ + protected final void flushOrReinitCodec() throws ExoPlaybackException { + if (flushOrReleaseCodec()) { + maybeInitCodec(); + } + } + + /** + * Flushes the codec. If flushing is not possible, the codec will be released. This method is a + * no-op if the codec is {@code null}. + * + * @return Whether the codec was released. + */ + protected boolean flushOrReleaseCodec() { + if (codec == null) { + // Nothing to do. + return false; + } codecHotswapDeadlineMs = C.TIME_UNSET; resetInputBuffer(); resetOutputBuffer(); + codecReceivedBuffers = false; waitingForFirstSyncFrame = true; waitingForKeys = false; shouldSkipOutputBuffer = false; @@ -699,28 +733,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer { shouldSkipAdaptationWorkaroundOutputBuffer = false; if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { releaseCodec(); - maybeInitCodec(); + return true; } else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) { // We're already waiting to re-initialize the codec. Since we're now flushing, there's no need // to wait any longer. if (codecReinitializationIsRelease) { releaseCodec(); - maybeInitCodec(); + return true; } else { codec.flush(); - codecReceivedBuffers = false; codecReinitializationState = REINITIALIZATION_STATE_NONE; } } else { // We can flush and re-use the existing decoder. codec.flush(); - codecReceivedBuffers = false; } if (codecReconfigured && format != null) { // Any reconfiguration data that we send shortly before the flush may be discarded. We // avoid this issue by sending reconfiguration data following every flush. codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; } + return false; } private boolean initCodecWithFallback(MediaCrypto crypto, boolean drmSessionRequiresSecureDecoder) @@ -1496,7 +1529,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { releaseCodec(); maybeInitCodec(); } else { - flushCodec(); + flushOrReinitCodec(); } } else { outputStreamEnded = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 7418e84449..fb99eef6e7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -801,6 +801,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource activeMediaPeriods; public DeferredTimeline timeline; public int childIndex; @@ -809,7 +810,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource activeMediaPeriods; public MediaSourceHolder(MediaSource mediaSource) { this.mediaSource = mediaSource; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index a993c79b4a..9fac69b281 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -308,7 +308,7 @@ public class ChunkSampleStream implements SampleStream, S // chunk even if the sample timestamps are slightly offset from the chunk start times. seekInsideBuffer = primarySampleQueue.setReadPosition(seekToMediaChunk.getFirstSampleIndex(0)); - decodeOnlyUntilPositionUs = Long.MIN_VALUE; + decodeOnlyUntilPositionUs = 0; } else { seekInsideBuffer = primarySampleQueue.advanceTo( @@ -583,7 +583,7 @@ public class ChunkSampleStream implements SampleStream, S if (pendingReset) { boolean resetToMediaChunk = mediaChunk.startTimeUs == pendingResetPositionUs; // Only enable setting of the decode only flag if we're not resetting to a chunk boundary. - decodeOnlyUntilPositionUs = resetToMediaChunk ? Long.MIN_VALUE : pendingResetPositionUs; + decodeOnlyUntilPositionUs = resetToMediaChunk ? 0 : pendingResetPositionUs; pendingResetPositionUs = C.TIME_UNSET; } mediaChunk.init(mediaChunkOutput); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java index 06ca83dd93..71e2d8d19f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultAllocator.java @@ -117,19 +117,6 @@ public final class DefaultAllocator implements Allocator { Math.max(availableAllocations.length * 2, availableCount + allocations.length)); } for (Allocation allocation : allocations) { - // Weak sanity check that the allocation probably originated from this pool. - if (allocation.data != initialAllocationBlock - && allocation.data.length != individualAllocationSize) { - throw new IllegalArgumentException( - "Unexpected allocation: " - + System.identityHashCode(allocation.data) - + ", " - + System.identityHashCode(initialAllocationBlock) - + ", " - + allocation.data.length - + ", " - + individualAllocationSize); - } availableAllocations[availableCount++] = allocation; } allocatedCount -= allocations.length; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java index 6d090d073e..e8315385e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.support.annotation.NonNull; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.util.Log; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.Iterator; import java.util.NavigableSet; @@ -195,8 +196,7 @@ public final class CachedRegionTracker implements Cache.Listener { @Override public int compareTo(@NonNull Region another) { - return startOffset < another.startOffset ? -1 - : startOffset == another.startOffset ? 0 : 1; + return Util.compareLong(startOffset, another.startOffset); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index d217b47785..2b2e628017 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -520,9 +520,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @CallSuper @Override - protected void flushCodec() throws ExoPlaybackException { - super.flushCodec(); - buffersInCodecCount = 0; + protected boolean flushOrReleaseCodec() { + try { + return super.flushOrReleaseCodec(); + } finally { + buffersInCodecCount = 0; + } } @Override @@ -859,7 +862,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // We dropped some buffers to catch up, so update the decoder counters and flush the codec, // which releases all pending buffers buffers including the current output buffer. updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); - flushCodec(); + flushOrReinitCodec(); return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java index 4a8354d17f..663c9fe284 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/spherical/CameraMotionRenderer.java @@ -72,12 +72,12 @@ public class CameraMotionRenderer extends BaseRenderer { @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - reset(); + resetListener(); } @Override protected void onDisabled() { - reset(); + resetListener(); } @Override @@ -124,7 +124,7 @@ public class CameraMotionRenderer extends BaseRenderer { return result; } - private void reset() { + private void resetListener() { lastTimestampUs = 0; if (listener != null) { listener.onCameraMotionReset(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 407e9a3827..d131ed0f51 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -2534,6 +2534,59 @@ public final class ExoPlayerTest { assertThat(positionWhenReady.get()).isEqualTo(periodDurationMs + 10); } + @Test + public void periodTransitionReportsCorrectBufferedPosition() throws Exception { + int periodCount = 3; + long periodDurationUs = 5 * C.MICROS_PER_SECOND; + long windowDurationUs = periodCount * periodDurationUs; + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition( + periodCount, + /* id= */ new Object(), + /* isSeekable= */ true, + /* isDynamic= */ false, + windowDurationUs)); + AtomicReference playerReference = new AtomicReference<>(); + AtomicLong bufferedPositionAtFirstDiscontinuityMs = new AtomicLong(C.TIME_UNSET); + EventListener eventListener = + new EventListener() { + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION) { + if (bufferedPositionAtFirstDiscontinuityMs.get() == C.TIME_UNSET) { + bufferedPositionAtFirstDiscontinuityMs.set( + playerReference.get().getBufferedPosition()); + } + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("periodTransitionReportsCorrectBufferedPosition") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + .pause() + // Wait until all periods are fully buffered. + .waitForIsLoading(/* targetIsLoading= */ true) + .waitForIsLoading(/* targetIsLoading= */ false) + .play() + .build(); + new Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build(context) + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(bufferedPositionAtFirstDiscontinuityMs.get()).isEqualTo(C.usToMs(windowDurationUs)); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java index 8dc60a15a4..48e71c619c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java @@ -103,6 +103,7 @@ public class SimpleDecoderAudioRendererTest { } verify(mockAudioSink, times(1)).playToEndOfStream(); audioRenderer.disable(); + audioRenderer.reset(); verify(mockAudioSink, times(1)).release(); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 37c9e313ae..1ea25ecc36 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -544,7 +544,7 @@ public class DefaultDashChunkSource implements DashChunkSource { long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum + segmentCount - 1); long periodDurationUs = representationHolder.periodDurationUs; long clippedEndTimeUs = - periodDurationUs != C.TIME_UNSET && periodDurationUs < endTimeUs + periodDurationUs != C.TIME_UNSET && periodDurationUs <= endTimeUs ? periodDurationUs : C.TIME_UNSET; DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index 9938f8564c..aacafc5a40 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -158,6 +158,8 @@ public class DashManifestParser extends DefaultHandler : (period.startMs + periodDurationMs); periods.add(period); } + } else { + maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "MPD")); @@ -224,6 +226,8 @@ public class DashManifestParser extends DefaultHandler segmentBase = parseSegmentList(xpp, null); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, null); + } else { + maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "Period")); @@ -412,22 +416,26 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); - } else if (data == null) { - if (XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh") - && xpp.next() == XmlPullParser.TEXT) { - // The cenc:pssh element is defined in 23001-7:2015. - data = Base64.decode(xpp.getText(), Base64.DEFAULT); - uuid = PsshAtomUtil.parseUuid(data); - if (uuid == null) { - Log.w(TAG, "Skipping malformed cenc:pssh data"); - data = null; - } - } else if (C.PLAYREADY_UUID.equals(uuid) && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") - && xpp.next() == XmlPullParser.TEXT) { - // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. - data = PsshAtomUtil.buildPsshAtom(C.PLAYREADY_UUID, - Base64.decode(xpp.getText(), Base64.DEFAULT)); + } else if (data == null + && XmlPullParserUtil.isStartTagIgnorePrefix(xpp, "pssh") + && xpp.next() == XmlPullParser.TEXT) { + // The cenc:pssh element is defined in 23001-7:2015. + data = Base64.decode(xpp.getText(), Base64.DEFAULT); + uuid = PsshAtomUtil.parseUuid(data); + if (uuid == null) { + Log.w(TAG, "Skipping malformed cenc:pssh data"); + data = null; } + } else if (data == null + && C.PLAYREADY_UUID.equals(uuid) + && XmlPullParserUtil.isStartTag(xpp, "mspr:pro") + && xpp.next() == XmlPullParser.TEXT) { + // The mspr:pro element is defined in DASH Content Protection using Microsoft PlayReady. + data = + PsshAtomUtil.buildPsshAtom( + C.PLAYREADY_UUID, Base64.decode(xpp.getText(), Base64.DEFAULT)); + } else { + maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); SchemeData schemeData = @@ -465,7 +473,7 @@ public class DashManifestParser extends DefaultHandler */ protected void parseAdaptationSetChild(XmlPullParser xpp) throws XmlPullParserException, IOException { - // pass + maybeSkipTag(xpp); } // Representation parsing. @@ -529,6 +537,8 @@ public class DashManifestParser extends DefaultHandler inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); + } else { + maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); @@ -667,6 +677,8 @@ public class DashManifestParser extends DefaultHandler xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) { initialization = parseInitialization(xpp); + } else { + maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentBase")); @@ -704,6 +716,8 @@ public class DashManifestParser extends DefaultHandler segments = new ArrayList<>(); } segments.add(parseSegmentUrl(xpp)); + } else { + maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentList")); @@ -750,6 +764,8 @@ public class DashManifestParser extends DefaultHandler initialization = parseInitialization(xpp); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) { timeline = parseSegmentTimeline(xpp); + } else { + maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTemplate")); @@ -797,6 +813,8 @@ public class DashManifestParser extends DefaultHandler EventMessage event = parseEvent(xpp, schemeIdUri, value, timescale, scratchOutputStream); eventMessages.add(event); + } else { + maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "EventStream")); @@ -935,6 +953,8 @@ public class DashManifestParser extends DefaultHandler segmentTimeline.add(buildSegmentTimelineElement(elapsedTime, duration)); elapsedTime += duration; } + } else { + maybeSkipTag(xpp); } } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTimeline")); return segmentTimeline; @@ -1017,6 +1037,29 @@ public class DashManifestParser extends DefaultHandler // Utility methods. + /** + * If the provided {@link XmlPullParser} is currently positioned at the start of a tag, skips + * forward to the end of that tag. + * + * @param xpp The {@link XmlPullParser}. + * @throws XmlPullParserException If an error occurs parsing the stream. + * @throws IOException If an error occurs reading the stream. + */ + public static void maybeSkipTag(XmlPullParser xpp) throws IOException, XmlPullParserException { + if (!XmlPullParserUtil.isStartTag(xpp)) { + return; + } + int depth = 1; + while (depth != 0) { + xpp.next(); + if (XmlPullParserUtil.isStartTag(xpp)) { + depth++; + } else if (XmlPullParserUtil.isEndTag(xpp)) { + depth--; + } + } + } + /** * Removes unnecessary {@link SchemeData}s with null {@link SchemeData#data}. */ diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index e429f5bfa0..7e693111ab 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -1032,6 +1032,12 @@ public class PlayerView extends FrameLayout { if (ev.getActionMasked() != MotionEvent.ACTION_DOWN) { return false; } + return performClick(); + } + + @Override + public boolean performClick() { + super.performClick(); return toggleControllerVisibility(); } diff --git a/library/ui/src/main/res/values-mr/strings.xml b/library/ui/src/main/res/values-mr/strings.xml index 753e6ba8d8..45bac7c61f 100644 --- a/library/ui/src/main/res/values-mr/strings.xml +++ b/library/ui/src/main/res/values-mr/strings.xml @@ -11,7 +11,7 @@ एक रीपीट करा सर्व रीपीट करा शफल करा - पूर्ण स्क्रीन मोड + फुल स्क्रीन मोड डाउनलोड करा डाउनलोड डाउनलोड होत आहे diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 14d62e85c8..c988c0c172 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -686,6 +686,56 @@ public abstract class Action { } } + /** + * Waits for a specified loading state, returning either immediately or after a call to {@link + * Player.EventListener#onLoadingChanged(boolean)}. + */ + public static final class WaitForIsLoading extends Action { + + private final boolean targetIsLoading; + + /** + * @param tag A tag to use for logging. + * @param targetIsLoading The loading state to wait for. + */ + public WaitForIsLoading(String tag, boolean targetIsLoading) { + super(tag, "WaitForIsLoading"); + this.targetIsLoading = targetIsLoading; + } + + @Override + protected void doActionAndScheduleNextImpl( + final SimpleExoPlayer player, + final DefaultTrackSelector trackSelector, + final Surface surface, + final HandlerWrapper handler, + final ActionNode nextAction) { + if (nextAction == null) { + return; + } + if (targetIsLoading == player.isLoading()) { + nextAction.schedule(player, trackSelector, surface, handler); + } else { + player.addListener( + new Player.EventListener() { + @Override + public void onLoadingChanged(boolean isLoading) { + if (targetIsLoading == isLoading) { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + } + }); + } + } + + @Override + protected void doActionImpl( + SimpleExoPlayer player, DefaultTrackSelector trackSelector, Surface surface) { + // Not triggered. + } + } + /** * Waits for {@link Player.EventListener#onSeekProcessed()}. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 54d97fb905..71f5fdeae1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.ThrowPlaybackException; +import com.google.android.exoplayer2.testutil.Action.WaitForIsLoading; import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed; @@ -414,6 +415,16 @@ public final class ActionSchedule { return apply(new WaitForPlaybackState(tag, targetPlaybackState)); } + /** + * Schedules a delay until {@code player.isLoading()} changes to the specified value. + * + * @param targetIsLoading The target value of {@code player.isLoading()}. + * @return The builder, for convenience. + */ + public Builder waitForIsLoading(boolean targetIsLoading) { + return apply(new WaitForIsLoading(tag, targetIsLoading)); + } + /** * Schedules a {@link Runnable} to be executed. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 627b5b72f3..39194d48fe 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -105,9 +105,12 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { } @Override - protected void flushCodec() throws ExoPlaybackException { - super.flushCodec(); - clearTimestamps(); + protected boolean flushOrReleaseCodec() { + try { + return super.flushOrReleaseCodec(); + } finally { + clearTimestamps(); + } } @Override