From e3aa8a06175e752496a04b3dc21f0ee29f105d8b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 7 May 2018 15:15:01 -0700 Subject: [PATCH 001/106] Remove non-release notes from release branch --- RELEASENOTES.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 979543f7be..5efb382bba 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,9 +1,5 @@ # Release notes # -### dev-v2 (not yet released) ### - -* Coming soon... - ### 2.8.0 ### * Downloading: From 646cdbc6d2766cfb541edc54c13f818b1b854872 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 May 2018 17:59:26 -0700 Subject: [PATCH 002/106] Revert retention of audio decoders ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=195752931 --- .../audio/MediaCodecAudioRenderer.java | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 79b2311c88..9ab066ee7d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -327,10 +327,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected @KeepCodecResult int canKeepCodec( MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { - return getCodecMaxInputSize(codecInfo, newFormat) <= codecMaxInputSize - && areAdaptationCompatible(oldFormat, newFormat) - ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION - : KEEP_CODEC_RESULT_NO; + return KEEP_CODEC_RESULT_NO; + // TODO: Determine when codecs can be safely kept. When doing so, also uncomment the commented + // out code in getCodecMaxInputSize. + // return getCodecMaxInputSize(codecInfo, newFormat) <= codecMaxInputSize + // && areAdaptationCompatible(oldFormat, newFormat) + // ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION + // : KEEP_CODEC_RESULT_NO; } @Override @@ -571,16 +574,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected int getCodecMaxInputSize( MediaCodecInfo codecInfo, Format format, Format[] streamFormats) { int maxInputSize = getCodecMaxInputSize(codecInfo, format); - if (streamFormats.length == 1) { - // The single entry in streamFormats must correspond to the format for which the codec is - // being configured. - return maxInputSize; - } - for (Format streamFormat : streamFormats) { - if (areAdaptationCompatible(format, streamFormat)) { - maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat)); - } - } + // if (streamFormats.length == 1) { + // // The single entry in streamFormats must correspond to the format for which the codec is + // // being configured. + // return maxInputSize; + // } + // for (Format streamFormat : streamFormats) { + // if (areAdaptationCompatible(format, streamFormat)) { + // maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat)); + // } + // } return maxInputSize; } From 9763a506a0d485d368ec5cf1afbc042351364076 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 May 2018 17:59:52 -0700 Subject: [PATCH 003/106] Fix demo app playlist playbacks ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=195752969 --- .../google/android/exoplayer2/demo/SampleChooserActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 324c5ea4cb..5524f98257 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 @@ -548,10 +548,10 @@ public class SampleChooserActivity extends Activity @Override public Intent buildIntent(Context context) { - Uri[] uris = new Uri[children.length]; + String[] uris = new String[children.length]; String[] extensions = new String[children.length]; for (int i = 0; i < children.length; i++) { - uris[i] = children[i].uri; + uris[i] = children[i].uri.toString(); extensions[i] = children[i].extension; } return super.buildIntent(context) From f6cc43cec7b4fd2933e711a5d33754d804a642b2 Mon Sep 17 00:00:00 2001 From: Andrew Lewis Date: Tue, 22 May 2018 11:58:22 +0100 Subject: [PATCH 004/106] Update release notes --- RELEASENOTES.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0b17cb5251..cb3d654e18 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,9 +1,5 @@ # Release notes # -### dev-v2 (not yet released) ### - -* Coming soon - ### 2.8.1 ### * HLS: From 9de96782fd651491c801728eedcb0b8132ae139f Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 25 May 2018 04:02:53 -0700 Subject: [PATCH 005/106] Serialize recursive listener notifications. When the player state is changed from an event listener callback, we may get recursive listener notifications. These recursions can produce a wrong order, skip or duplicate updates, and send different notifications to different listeners. This change serializes listener notifications by clustering all update data in a helper data class and adding the updates to a queue which can be handled in a loop on the outer layer of the recursion. As playWhenReady updates also reference the current playbackInfo, we need to redirect the listener notifcations for setPlayWhenReady to the same queue. Issue:#4276 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198031431 --- RELEASENOTES.md | 5 + .../android/exoplayer2/ExoPlayerImpl.java | 169 +++++++++++++----- .../android/exoplayer2/ExoPlayerTest.java | 99 ++++++++++ 3 files changed, 225 insertions(+), 48 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cb3d654e18..7a0a56d713 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,10 @@ # Release notes # +### 2.8.2 ### + +* Fix inconsistent `Player.EventListener` invocations for recursive player state + changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)). + ### 2.8.1 ### * HLS: 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 4125a203a6..9a9577c50a 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 @@ -33,8 +33,10 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -53,6 +55,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; + private final ArrayDeque pendingPlaybackInfoUpdates; private boolean playWhenReady; private @RepeatMode int repeatMode; @@ -112,6 +115,7 @@ import java.util.concurrent.CopyOnWriteArraySet; /* startPositionUs= */ 0, TrackGroupArray.EMPTY, emptyTrackSelectorResult); + pendingPlaybackInfoUpdates = new ArrayDeque<>(); internalPlayer = new ExoPlayerImplInternal( renderers, @@ -185,7 +189,8 @@ import java.util.concurrent.CopyOnWriteArraySet; /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, TIMELINE_CHANGE_REASON_RESET, - /* seekProcessed= */ false); + /* seekProcessed= */ false, + /* playWhenReadyChanged= */ false); } @Override @@ -193,10 +198,13 @@ import java.util.concurrent.CopyOnWriteArraySet; if (this.playWhenReady != playWhenReady) { this.playWhenReady = playWhenReady; internalPlayer.setPlayWhenReady(playWhenReady); - PlaybackInfo playbackInfo = this.playbackInfo; - for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); - } + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + /* ignored */ TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false, + /* playWhenReadyChanged= */ true); } } @@ -352,7 +360,8 @@ import java.util.concurrent.CopyOnWriteArraySet; /* positionDiscontinuity= */ false, /* ignored */ DISCONTINUITY_REASON_INTERNAL, TIMELINE_CHANGE_REASON_RESET, - /* seekProcessed= */ false); + /* seekProcessed= */ false, + /* playWhenReadyChanged= */ false); } @Override @@ -615,7 +624,8 @@ import java.util.concurrent.CopyOnWriteArraySet; positionDiscontinuity, positionDiscontinuityReason, timelineChangeReason, - seekProcessed); + seekProcessed, + /* playWhenReadyChanged= */ false); } } @@ -643,51 +653,33 @@ import java.util.concurrent.CopyOnWriteArraySet; } private void updatePlaybackInfo( - PlaybackInfo newPlaybackInfo, + PlaybackInfo playbackInfo, boolean positionDiscontinuity, @Player.DiscontinuityReason int positionDiscontinuityReason, @Player.TimelineChangeReason int timelineChangeReason, - boolean seekProcessed) { - boolean timelineOrManifestChanged = - playbackInfo.timeline != newPlaybackInfo.timeline - || playbackInfo.manifest != newPlaybackInfo.manifest; - boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState; - boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading; - boolean trackSelectorResultChanged = - playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; - playbackInfo = newPlaybackInfo; - if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged( - playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason); - } + boolean seekProcessed, + boolean playWhenReadyChanged) { + boolean isRunningRecursiveListenerNotification = !pendingPlaybackInfoUpdates.isEmpty(); + pendingPlaybackInfoUpdates.addLast( + new PlaybackInfoUpdate( + playbackInfo, + /* previousPlaybackInfo= */ this.playbackInfo, + listeners, + trackSelector, + positionDiscontinuity, + positionDiscontinuityReason, + timelineChangeReason, + seekProcessed, + playWhenReady, + playWhenReadyChanged)); + // Assign playback info immediately such that all getters return the right values. + this.playbackInfo = playbackInfo; + if (isRunningRecursiveListenerNotification) { + return; } - if (positionDiscontinuity) { - for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(positionDiscontinuityReason); - } - } - if (trackSelectorResultChanged) { - trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged( - playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections); - } - } - if (isLoadingChanged) { - for (Player.EventListener listener : listeners) { - listener.onLoadingChanged(playbackInfo.isLoading); - } - } - if (playbackStateChanged) { - for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); - } - } - if (seekProcessed) { - for (Player.EventListener listener : listeners) { - listener.onSeekProcessed(); - } + while (!pendingPlaybackInfoUpdates.isEmpty()) { + pendingPlaybackInfoUpdates.peekFirst().notifyListeners(); + pendingPlaybackInfoUpdates.removeFirst(); } } @@ -703,4 +695,85 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean shouldMaskPosition() { return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0; } + + private static final class PlaybackInfoUpdate { + + private final PlaybackInfo playbackInfo; + private final Set listeners; + private final TrackSelector trackSelector; + private final boolean positionDiscontinuity; + private final @Player.DiscontinuityReason int positionDiscontinuityReason; + private final @Player.TimelineChangeReason int timelineChangeReason; + private final boolean seekProcessed; + private final boolean playWhenReady; + private final boolean playbackStateOrPlayWhenReadyChanged; + private final boolean timelineOrManifestChanged; + private final boolean isLoadingChanged; + private final boolean trackSelectorResultChanged; + + public PlaybackInfoUpdate( + PlaybackInfo playbackInfo, + PlaybackInfo previousPlaybackInfo, + Set listeners, + TrackSelector trackSelector, + boolean positionDiscontinuity, + @Player.DiscontinuityReason int positionDiscontinuityReason, + @Player.TimelineChangeReason int timelineChangeReason, + boolean seekProcessed, + boolean playWhenReady, + boolean playWhenReadyChanged) { + this.playbackInfo = playbackInfo; + this.listeners = listeners; + this.trackSelector = trackSelector; + this.positionDiscontinuity = positionDiscontinuity; + this.positionDiscontinuityReason = positionDiscontinuityReason; + this.timelineChangeReason = timelineChangeReason; + this.seekProcessed = seekProcessed; + this.playWhenReady = playWhenReady; + playbackStateOrPlayWhenReadyChanged = + playWhenReadyChanged || previousPlaybackInfo.playbackState != playbackInfo.playbackState; + timelineOrManifestChanged = + previousPlaybackInfo.timeline != playbackInfo.timeline + || previousPlaybackInfo.manifest != playbackInfo.manifest; + isLoadingChanged = previousPlaybackInfo.isLoading != playbackInfo.isLoading; + trackSelectorResultChanged = + previousPlaybackInfo.trackSelectorResult != playbackInfo.trackSelectorResult; + } + + public void notifyListeners() { + if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { + for (Player.EventListener listener : listeners) { + listener.onTimelineChanged( + playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason); + } + } + if (positionDiscontinuity) { + for (Player.EventListener listener : listeners) { + listener.onPositionDiscontinuity(positionDiscontinuityReason); + } + } + if (trackSelectorResultChanged) { + trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); + for (Player.EventListener listener : listeners) { + listener.onTracksChanged( + playbackInfo.trackGroups, playbackInfo.trackSelectorResult.selections); + } + } + if (isLoadingChanged) { + for (Player.EventListener listener : listeners) { + listener.onLoadingChanged(playbackInfo.isLoading); + } + } + if (playbackStateOrPlayWhenReadyChanged) { + for (Player.EventListener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); + } + } + if (seekProcessed) { + for (Player.EventListener listener : listeners) { + listener.onSeekProcessed(); + } + } + } + } } 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 0df854cddb..c05f8914f5 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 @@ -1980,6 +1980,105 @@ public final class ExoPlayerTest { .inOrder(); } + @Test + public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception { + // We add two listeners to the player. The first stops the player as soon as it's ready and both + // record the state change events they receive. + final AtomicReference playerReference = new AtomicReference<>(); + final List eventListener1States = new ArrayList<>(); + final List eventListener2States = new ArrayList<>(); + final EventListener eventListener1 = + new DefaultEventListener() { + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + eventListener1States.add(playbackState); + if (playbackState == Player.STATE_READY) { + playerReference.get().stop(/* reset= */ true); + } + } + }; + final EventListener eventListener2 = + new DefaultEventListener() { + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + eventListener2States.add(playbackState); + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testRecursivePlayerChanges") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener1); + player.addListener(eventListener2); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(eventListener1States) + .containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE) + .inOrder(); + assertThat(eventListener2States) + .containsExactly(Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE) + .inOrder(); + } + + @Test + public void testRecursivePlayerChangesAreReportedInCorrectOrder() throws Exception { + // The listener stops the player as soon as it's ready (which should report a timeline and state + // change) and sets playWhenReady to false when the timeline callback is received. + final AtomicReference playerReference = new AtomicReference<>(); + final List eventListenerPlayWhenReady = new ArrayList<>(); + final List eventListenerStates = new ArrayList<>(); + final EventListener eventListener = + new DefaultEventListener() { + @Override + public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { + if (timeline.isEmpty()) { + playerReference.get().setPlayWhenReady(/* playWhenReady= */ false); + } + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + eventListenerPlayWhenReady.add(playWhenReady); + eventListenerStates.add(playbackState); + if (playbackState == Player.STATE_READY) { + playerReference.get().stop(/* reset= */ true); + } + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testRecursivePlayerChanges") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + playerReference.set(player); + player.addListener(eventListener); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + assertThat(eventListenerStates) + .containsExactly( + Player.STATE_BUFFERING, Player.STATE_READY, Player.STATE_IDLE, Player.STATE_IDLE) + .inOrder(); + assertThat(eventListenerPlayWhenReady).containsExactly(true, true, true, false).inOrder(); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { From 3974a8464a39bbbab7a59ee1dac9584b70c6653e Mon Sep 17 00:00:00 2001 From: sammon Date: Fri, 25 May 2018 09:52:06 -0700 Subject: [PATCH 006/106] Exposing BaseMediaChunkOutput as public so that BaseMediaChunk.init() is unit testable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198062017 --- .../exoplayer2/source/chunk/BaseMediaChunkOutput.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java index 9531aaf32e..2154400c9e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -15,16 +15,16 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.support.annotation.VisibleForTesting; import android.util.Log; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; -/** - * An output for {@link BaseMediaChunk}s. - */ -/* package */ final class BaseMediaChunkOutput implements TrackOutputProvider { +/** An output for {@link BaseMediaChunk}s. */ +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +public final class BaseMediaChunkOutput implements TrackOutputProvider { private static final String TAG = "BaseMediaChunkOutput"; From 1d7ecd73b7c40e8cc5a7def2757cc210b589ebb4 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 29 May 2018 03:02:20 -0700 Subject: [PATCH 007/106] Roll forward of [] Set content length and redirect uri in a single transaction. New: Fixed the code where DataSpec.uri is set to null in [] Automated g4 rollback of changelist 196765970. *** Reason for rollback *** Fixed the code where DataSpec.uri is set to null in [] *** Original change description *** Automated g4 rollback of changelist 194932235. *** Reason for rollback *** This CL breaks the playability of Mango's offlined progressive videos. *** Original change description *** Set content length and redirect uri in a single transaction NORELNOTES=true NO_BUG *** *** ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198370211 --- .../upstream/cache/CacheDataSource.java | 70 +++++++------------ 1 file changed, 26 insertions(+), 44 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 023567e7df..045fc25338 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; import android.support.annotation.IntDef; import android.support.annotation.Nullable; -import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSink; import com.google.android.exoplayer2.upstream.DataSource; @@ -52,8 +51,6 @@ public final class CacheDataSource implements DataSource { */ public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024; - private static final String TAG = "CacheDataSource"; - /** * Flags controlling the cache's behavior. */ @@ -221,7 +218,7 @@ public final class CacheDataSource implements DataSource { try { key = CacheUtil.getKey(dataSpec); uri = dataSpec.uri; - actualUri = loadRedirectedUriOrReturnGivenUri(cache, key, uri); + actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri); flags = dataSpec.flags; readPosition = dataSpec.position; @@ -272,7 +269,7 @@ public final class CacheDataSource implements DataSource { bytesRemaining -= bytesRead; } } else if (currentDataSpecLengthUnset) { - setBytesRemainingAndMaybeStoreLength(0); + setNoBytesRemainingAndMaybeStoreLength(); } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { closeCurrentSource(); openNextSource(false); @@ -281,7 +278,7 @@ public final class CacheDataSource implements DataSource { return bytesRead; } catch (IOException e) { if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { - setBytesRemainingAndMaybeStoreLength(0); + setNoBytesRemainingAndMaybeStoreLength(); return C.RESULT_END_OF_INPUT; } handleBeforeThrow(e); @@ -402,46 +399,38 @@ public final class CacheDataSource implements DataSource { currentDataSource = nextDataSource; currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET; long resolvedLength = nextDataSource.open(nextDataSpec); - if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { - setBytesRemainingAndMaybeStoreLength(resolvedLength); - } - // TODO find a way to store length and redirected uri in one metadata mutation. - maybeUpdateActualUriFieldAndRedirectedUriMetadata(); - } - private void maybeUpdateActualUriFieldAndRedirectedUriMetadata() { - if (!isReadingFromUpstream()) { - return; - } - actualUri = currentDataSource.getUri(); - maybeUpdateRedirectedUriMetadata(); - } - - private void maybeUpdateRedirectedUriMetadata() { - if (!isWritingToCache()) { - return; - } + // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata. ContentMetadataMutations mutations = new ContentMetadataMutations(); - boolean isRedirected = !uri.equals(actualUri); - if (isRedirected) { - ContentMetadataInternal.setRedirectedUri(mutations, actualUri); - } else { - ContentMetadataInternal.removeRedirectedUri(mutations); + if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { + bytesRemaining = resolvedLength; + ContentMetadataInternal.setContentLength(mutations, readPosition + bytesRemaining); } - try { + if (isReadingFromUpstream()) { + actualUri = currentDataSource.getUri(); + boolean isRedirected = !uri.equals(actualUri); + if (isRedirected) { + ContentMetadataInternal.setRedirectedUri(mutations, actualUri); + } else { + ContentMetadataInternal.removeRedirectedUri(mutations); + } + } + if (isWritingToCache()) { cache.applyContentMetadataMutations(key, mutations); - } catch (CacheException e) { - String message = - "Couldn't update redirected URI. " - + "This might cause relative URIs get resolved incorrectly."; - Log.w(TAG, message, e); } } - private static Uri loadRedirectedUriOrReturnGivenUri(Cache cache, String key, Uri uri) { + private void setNoBytesRemainingAndMaybeStoreLength() throws IOException { + bytesRemaining = 0; + if (isWritingToCache()) { + cache.setContentLength(key, readPosition); + } + } + + private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) { ContentMetadata contentMetadata = cache.getContentMetadata(key); Uri redirectedUri = ContentMetadataInternal.getRedirectedUri(contentMetadata); - return redirectedUri == null ? uri : redirectedUri; + return redirectedUri == null ? defaultUri : redirectedUri; } private static boolean isCausedByPositionOutOfRange(IOException e) { @@ -458,13 +447,6 @@ public final class CacheDataSource implements DataSource { return false; } - private void setBytesRemainingAndMaybeStoreLength(long bytesRemaining) throws IOException { - this.bytesRemaining = bytesRemaining; - if (isWritingToCache()) { - cache.setContentLength(key, readPosition + bytesRemaining); - } - } - private boolean isReadingFromUpstream() { return !isReadingFromCache(); } From 059f8107740f62107ae0d594cbb2cae220e24e0c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 May 2018 08:04:57 -0700 Subject: [PATCH 008/106] Make BaseMediaChunkOutput properly public I think it was just wrong that it was package private before, since it resulted in our public API referencing something that's not part of the public API: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.html#init-com.google.android.exoplayer2.source.chunk.BaseMediaChunkOutput- ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198396555 --- .../android/exoplayer2/source/chunk/BaseMediaChunkOutput.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java index 2154400c9e..e0129e5c64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunkOutput.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.source.chunk; -import android.support.annotation.VisibleForTesting; import android.util.Log; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -23,7 +22,6 @@ import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOutputProvider; /** An output for {@link BaseMediaChunk}s. */ -@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public final class BaseMediaChunkOutput implements TrackOutputProvider { private static final String TAG = "BaseMediaChunkOutput"; From cd7f1b9558e18467b80ea2e204270fa64d08b44e Mon Sep 17 00:00:00 2001 From: sammon Date: Tue, 29 May 2018 13:09:42 -0700 Subject: [PATCH 009/106] Explicitly support MediaChunk.chunkIndex = C.INDEX_UNSET. This is common in Manifestless streams. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198445216 --- .../android/exoplayer2/source/chunk/BaseMediaChunk.java | 2 +- .../exoplayer2/source/chunk/ContainerMediaChunk.java | 2 +- .../android/exoplayer2/source/chunk/MediaChunk.java | 8 ++++---- .../exoplayer2/source/chunk/SingleSampleMediaChunk.java | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java index e3eae2b4d8..e872f730de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -44,7 +44,7 @@ public abstract class BaseMediaChunk extends MediaChunk { * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the * whole chunk should be output. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. */ public BaseMediaChunk( DataSource dataSource, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index ed73cf2588..6aa90e58e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -49,7 +49,7 @@ public class ContainerMediaChunk extends BaseMediaChunk { * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the * whole chunk should be output. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. * @param chunkCount The number of chunks in the underlying media that are spanned by this * instance. Normally equal to one, but may be larger if multiple chunks as defined by the * underlying media are being merged into a single load. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java index d313a8cb81..9626f4b03f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.util.Assertions; */ public abstract class MediaChunk extends Chunk { - /** The chunk index. */ + /** The chunk index, or {@link C#INDEX_UNSET} if it is not known. */ public final long chunkIndex; /** @@ -37,7 +37,7 @@ public abstract class MediaChunk extends Chunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. */ public MediaChunk( DataSource dataSource, @@ -54,9 +54,9 @@ public abstract class MediaChunk extends Chunk { this.chunkIndex = chunkIndex; } - /** Returns the next chunk index. */ + /** Returns the next chunk index or {@link C#INDEX_UNSET} if it is not known. */ public long getNextChunkIndex() { - return chunkIndex + 1; + return chunkIndex != C.INDEX_UNSET ? chunkIndex + 1 : C.INDEX_UNSET; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index bd2363ede1..5247f9f973 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -45,7 +45,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. - * @param chunkIndex The index of the chunk. + * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. * @param trackType The type of the chunk. Typically one of the {@link C} {@code TRACK_TYPE_*} * constants. * @param sampleFormat The {@link Format} of the sample in the chunk. From f7dcee2e78925eb467f212f88b7b894dc25c468e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 May 2018 03:38:43 -0700 Subject: [PATCH 010/106] Update IMA and Play Services ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198536438 --- constants.gradle | 2 +- extensions/cast/build.gradle | 7 ++++--- extensions/ima/build.gradle | 12 ++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/constants.gradle b/constants.gradle index 9068fb8b56..9b9e79c99e 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,7 +25,7 @@ project.ext { buildToolsVersion = '27.0.3' testSupportLibraryVersion = '0.5' supportLibraryVersion = '27.0.0' - playServicesLibraryVersion = '12.0.0' + playServicesLibraryVersion = '15.0.1' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' junitVersion = '4.12' diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index ded92000d3..8374910879 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -30,9 +30,10 @@ dependencies { // com.android.support:support-v4, com.android.support:appcompat-v7 and // com.android.support:mediarouter-v7 to be used. Else older versions are // used, for example: - // com.google.android.gms:play-services-cast-framework:12.0.0 - // |-- com.google.android.gms:play-services-basement:12.0.0 - // |-- com.android.support:support-v4:26.1.0 + // com.google.android.gms:play-services-cast-framework:15.0.1 + // |-- com.google.android.gms:play-services-base:15.0.1 + // |-- com.google.android.gms:play-services-basement:15.0.1 + // |-- com.android.support:support-v4:26.1.0 api 'com.android.support:support-v4:' + supportLibraryVersion api 'com.android.support:appcompat-v7:' + supportLibraryVersion api 'com.android.support:mediarouter-v7:' + supportLibraryVersion diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 3529e05380..4403095658 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -27,14 +27,14 @@ android { dependencies { // This dependency is necessary to force the supportLibraryVersion of - // com.android.support:support-v4 to be used. Else an older version (25.2.0) - // is included via: - // com.google.android.gms:play-services-ads:12.0.0 - // |-- com.google.android.gms:play-services-ads-lite:12.0.0 - // |-- com.google.android.gms:play-services-basement:12.0.0 + // com.android.support:support-v4 to be used. Else an older version is + // included via: + // com.google.android.gms:play-services-ads:15.0.1 + // |-- com.google.android.gms:play-services-ads-identifier:15.0.1 + // |-- com.google.android.gms:play-services-basement:15.0.1 // |-- com.android.support:support-v4:26.1.0 api 'com.android.support:support-v4:' + supportLibraryVersion - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.5' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.7' implementation project(modulePrefix + 'library-core') implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion } From 2bbfc8424259aaebd94e35e6564cf5483be48912 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 May 2018 03:45:29 -0700 Subject: [PATCH 011/106] Don't advertise support for video/mpeg ads Issue: #4297 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198536888 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7a0a56d713..05a23a5077 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,8 @@ * Fix inconsistent `Player.EventListener` invocations for recursive player state changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)). +* IMA: Don't advertise support for video/mpeg ad media, as we don't have an + extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)). ### 2.8.1 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 2d9ddfb288..3256da21dd 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -447,9 +447,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } else if (contentType == C.TYPE_HLS) { supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); } else if (contentType == C.TYPE_OTHER) { - supportedMimeTypes.addAll(Arrays.asList( - MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG, - MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG)); + supportedMimeTypes.addAll( + Arrays.asList( + MimeTypes.VIDEO_MP4, + MimeTypes.VIDEO_WEBM, + MimeTypes.VIDEO_H263, + MimeTypes.AUDIO_MP4, + MimeTypes.AUDIO_MPEG)); } else if (contentType == C.TYPE_SS) { // IMA does not support Smooth Streaming ad media. } From cd65cc85e21a1c890760fab132c4e4d8a8b2f7da Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 31 May 2018 03:11:25 -0700 Subject: [PATCH 012/106] Clarify threading requirements for the player in the doc. This makes the requirement that all calls are made on one thread more explicit and also mentions this in the Getting Started guide. Issue:#4278 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198694579 --- .../com/google/android/exoplayer2/ExoPlayer.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 39a6243933..b97790d5fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -89,12 +89,12 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * model"> * *
    - *
  • It is strongly recommended that ExoPlayer instances are created and accessed from a single - * application thread. The application's main thread is ideal. Accessing an instance from - * multiple threads is discouraged as it may cause synchronization problems. - *
  • Registered listeners are called on the thread that created the ExoPlayer instance, unless - * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that - * case, registered listeners will be called on the application's main thread. + *
  • ExoPlayer instances must be accessed from a single application thread. This must be the + * thread the player is created on if that thread has a {@link Looper}, or the application's + * main thread otherwise. + *
  • Registered listeners are called on the thread the player is created on if that thread has a + * {@link Looper}, or the application's main thread otherwise. Note that this means registered + * listeners are called on the same thread which must be used to access the player. *
  • An internal playback thread is responsible for playback. Injected player components such as * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this * thread. From 4ecce9802bf549fc563ce46fdd8725e67654696f Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 31 May 2018 09:52:18 -0700 Subject: [PATCH 013/106] Explicitly null out LoadTask.callback on release As highlighted by the ref'd issue, we can end up with memory leaks if Loadable.load implementations take a long time to return upon cancelation. This change cuts off one of the two problematic reference chains. This doesn't do much about the ref'd issue, since there's a second reference chain that's much harder to deal with: Thread->LoadTask->loadable. But since it's easy just to cut this one off, I figure it makes sense to do so. Issue: #4249 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198735386 --- .../com/google/android/exoplayer2/upstream/Loader.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 074fc095ea..0f3198d06c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -250,11 +250,12 @@ public final class Loader implements LoaderErrorThrower { private static final int MSG_IO_EXCEPTION = 3; private static final int MSG_FATAL_ERROR = 4; - private final T loadable; - private final Loader.Callback callback; public final int defaultMinRetryCount; + + private final T loadable; private final long startTimeMs; + private @Nullable Loader.Callback callback; private IOException currentError; private int errorCount; @@ -304,6 +305,11 @@ public final class Loader implements LoaderErrorThrower { finish(); long nowMs = SystemClock.elapsedRealtime(); callback.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true); + // If loading, this task will be referenced from a GC root (the loading thread) until + // cancellation completes. The time taken for cancellation to complete depends on the + // implementation of the Loadable that the task is loading. We null the callback reference + // here so that it doesn't prevent garbage collection whilst cancellation is ongoing. + callback = null; } } From 7d0769249f7d88c03c984b37849cea8bbdddff54 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 31 May 2018 11:04:00 -0700 Subject: [PATCH 014/106] Avoid possibility of leaking an activity/service context The bug here was that we'd create a VideoFrameReleaseTimeHelper using whatever context DefaultRenderersFactory has, and it would then hold a reference to that context via DisplayManager. A leak could then occur if the player outlived the life of the context used to create it (which would be strange/unusual, but not impossible). Issue: #4249 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198747599 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 2 +- .../exoplayer2/video/VideoFrameReleaseTimeHelper.java | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) 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 579f7c45f4..d166297054 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 @@ -205,7 +205,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; this.context = context.getApplicationContext(); - frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context); + frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(this.context); eventDispatcher = new EventDispatcher(eventHandler, eventListener); deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround(); pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index 9036b19a75..b4835186ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -72,8 +72,12 @@ public final class VideoFrameReleaseTimeHelper { * @param context A context from which information about the default display can be retrieved. */ public VideoFrameReleaseTimeHelper(@Nullable Context context) { - windowManager = context == null ? null - : (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + if (context != null) { + context = context.getApplicationContext(); + windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + } else { + windowManager = null; + } if (windowManager != null) { displayListener = Util.SDK_INT >= 17 ? maybeBuildDefaultDisplayListenerV17(context) : null; vsyncSampler = VSyncSampler.getInstance(); From 928cbfa7bcb63d7a26f67d8bf094255ccc92fa5a Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 1 Jun 2018 04:31:10 -0700 Subject: [PATCH 015/106] Replace hash by Object reference for uids. There is the small (but unlikely) chance that the uids clash because the Objects have the same hash code. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198855724 --- .../source/ConcatenatingMediaSource.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) 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 3e39139918..b3e0b1404d 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 @@ -19,7 +19,6 @@ import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; @@ -34,6 +33,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -656,7 +656,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource { public final MediaSource mediaSource; - public final int uid; + public final Object uid; public DeferredTimeline timeline; public int childIndex; @@ -671,6 +671,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); + this.uid = new Object(); } public void reset(int childIndex, int firstWindowIndexInChild, int firstPeriodIndexInChild) { @@ -728,8 +729,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource childIndexByUid; public ConcatenatedTimeline( Collection mediaSourceHolders, @@ -744,8 +745,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); int index = 0; for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { timelines[index] = mediaSourceHolder.timeline; @@ -768,11 +769,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource Date: Fri, 1 Jun 2018 05:44:04 -0700 Subject: [PATCH 016/106] Avoid starting RequirementsWatcher if there is no download task ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198860680 --- .../exoplayer2/offline/DownloadManager.java | 13 ++++- .../exoplayer2/offline/DownloadService.java | 48 +++++++++++-------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 0e2c5874b1..b3f1d3da6a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -262,12 +262,23 @@ public final class DownloadManager { return task.id; } - /** Returns the current number of tasks. */ + /** Returns the number of tasks. */ public int getTaskCount() { Assertions.checkState(!released); return tasks.size(); } + /** Returns the number of download tasks. */ + public int getDownloadCount() { + int count = 0; + for (int i = 0; i < tasks.size(); i++) { + if (!tasks.get(i).action.isRemoveAction) { + count++; + } + } + return count; + } + /** Returns the state of a task, or null if no such task exists */ public @Nullable TaskState getTaskState(int taskId) { Assertions.checkState(!released); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 908aae481a..21fd541b52 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -187,17 +187,6 @@ public abstract class DownloadService extends Service { downloadManager = getDownloadManager(); downloadManagerListener = new DownloadManagerListener(); downloadManager.addListener(downloadManagerListener); - - RequirementsHelper requirementsHelper; - synchronized (requirementsHelpers) { - Class clazz = getClass(); - requirementsHelper = requirementsHelpers.get(clazz); - if (requirementsHelper == null) { - requirementsHelper = new RequirementsHelper(this, getRequirements(), getScheduler(), clazz); - requirementsHelpers.put(clazz, requirementsHelper); - } - } - requirementsHelper.start(); } @Override @@ -237,6 +226,7 @@ public abstract class DownloadService extends Service { Log.e(TAG, "Ignoring unrecognized action: " + intentAction); break; } + maybeStartWatchingRequirements(); if (downloadManager.isIdle()) { stop(); } @@ -248,14 +238,7 @@ public abstract class DownloadService extends Service { logd("onDestroy"); foregroundNotificationUpdater.stopPeriodicUpdates(); downloadManager.removeListener(downloadManagerListener); - if (downloadManager.getTaskCount() == 0) { - synchronized (requirementsHelpers) { - RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass()); - if (requirementsHelper != null) { - requirementsHelper.stop(); - } - } - } + maybeStopWatchingRequirements(); } @Nullable @@ -312,6 +295,31 @@ public abstract class DownloadService extends Service { // Do nothing. } + private void maybeStartWatchingRequirements() { + if (downloadManager.getDownloadCount() == 0) { + return; + } + Class clazz = getClass(); + RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz); + if (requirementsHelper == null) { + requirementsHelper = new RequirementsHelper(this, getRequirements(), getScheduler(), clazz); + requirementsHelpers.put(clazz, requirementsHelper); + requirementsHelper.start(); + logd("started watching requirements"); + } + } + + private void maybeStopWatchingRequirements() { + if (downloadManager.getDownloadCount() > 0) { + return; + } + RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass()); + if (requirementsHelper != null) { + requirementsHelper.stop(); + logd("stopped watching requirements"); + } + } + private void stop() { foregroundNotificationUpdater.stopPeriodicUpdates(); // Make sure startForeground is called before stopping. Workaround for [Internal: b/69424260]. @@ -331,7 +339,7 @@ public abstract class DownloadService extends Service { private final class DownloadManagerListener implements DownloadManager.Listener { @Override public void onInitialized(DownloadManager downloadManager) { - // Do nothing. + maybeStartWatchingRequirements(); } @Override From 7621a71bc390164cffba9ea8533c10933bef9ea6 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 1 Jun 2018 07:46:29 -0700 Subject: [PATCH 017/106] Remove Loadable.isLoadCanceled This simplifies Loadable implementations, and also removes the possibility of an incorrect Loadable implementation causing the wrong Loader.Callback method being called (perviously, for the correct method to be called, we relied on isLoadCanceled being implemented correctly). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198871133 --- .../exoplayer2/source/ExtractorMediaPeriod.java | 5 ----- .../exoplayer2/source/SingleSampleMediaPeriod.java | 5 ----- .../source/chunk/ContainerMediaChunk.java | 5 ----- .../android/exoplayer2/source/chunk/DataChunk.java | 5 ----- .../source/chunk/InitializationChunk.java | 5 ----- .../source/chunk/SingleSampleMediaChunk.java | 8 +------- .../google/android/exoplayer2/upstream/Loader.java | 13 +++++-------- .../exoplayer2/upstream/ParsingLoadable.java | 10 +--------- .../trackselection/AdaptiveTrackSelectionTest.java | 5 ----- .../exoplayer2/source/hls/HlsMediaChunk.java | 5 ----- 10 files changed, 7 insertions(+), 59 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index ed27a24350..d4ea6191aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -832,11 +832,6 @@ import java.util.Arrays; loadCanceled = true; } - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - @Override public void load() throws IOException, InterruptedException { int result = Extractor.RESULT_CONTINUE; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 0a089e5b7c..41814c4b40 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -348,11 +348,6 @@ import java.util.Arrays; // Never happens. } - @Override - public boolean isLoadCanceled() { - return false; - } - @Override public void load() throws IOException, InterruptedException { // We always load from the beginning, so reset the sampleSize to 0. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index 6aa90e58e1..1159f336a7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -106,11 +106,6 @@ public class ContainerMediaChunk extends BaseMediaChunk { loadCanceled = true; } - @Override - public final boolean isLoadCanceled() { - return loadCanceled; - } - @SuppressWarnings("NonAtomicVolatileUpdate") @Override public final void load() throws IOException, InterruptedException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java index 0846e7679d..1d3bdb57da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/DataChunk.java @@ -75,11 +75,6 @@ public abstract class DataChunk extends Chunk { loadCanceled = true; } - @Override - public final boolean isLoadCanceled() { - return loadCanceled; - } - @Override public final void load() throws IOException, InterruptedException { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java index 6dd90b8735..387a90297a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -69,11 +69,6 @@ public final class InitializationChunk extends Chunk { loadCanceled = true; } - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - @SuppressWarnings("NonAtomicVolatileUpdate") @Override public void load() throws IOException, InterruptedException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index 5247f9f973..17154ebc62 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -34,7 +34,6 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { private final Format sampleFormat; private volatile int bytesLoaded; - private volatile boolean loadCanceled; private volatile boolean loadCompleted; /** @@ -90,12 +89,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { @Override public void cancelLoad() { - loadCanceled = true; - } - - @Override - public boolean isLoadCanceled() { - return loadCanceled; + // Do nothing. } @SuppressWarnings("NonAtomicVolatileUpdate") diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 0f3198d06c..430948c875 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -57,11 +57,6 @@ public final class Loader implements LoaderErrorThrower { */ void cancelLoad(); - /** - * Returns whether the load has been canceled. - */ - boolean isLoadCanceled(); - /** * Performs the load, returning on completion or cancellation. * @@ -260,6 +255,7 @@ public final class Loader implements LoaderErrorThrower { private int errorCount; private volatile Thread executorThread; + private volatile boolean canceled; private volatile boolean released; public LoadTask(Looper looper, T loadable, Loader.Callback callback, @@ -296,6 +292,7 @@ public final class Loader implements LoaderErrorThrower { sendEmptyMessage(MSG_CANCEL); } } else { + canceled = true; loadable.cancelLoad(); if (executorThread != null) { executorThread.interrupt(); @@ -317,7 +314,7 @@ public final class Loader implements LoaderErrorThrower { public void run() { try { executorThread = Thread.currentThread(); - if (!loadable.isLoadCanceled()) { + if (!canceled) { TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName()); try { loadable.load(); @@ -334,7 +331,7 @@ public final class Loader implements LoaderErrorThrower { } } catch (InterruptedException e) { // The load was canceled. - Assertions.checkState(loadable.isLoadCanceled()); + Assertions.checkState(canceled); if (!released) { sendEmptyMessage(MSG_END_OF_SOURCE); } @@ -379,7 +376,7 @@ public final class Loader implements LoaderErrorThrower { finish(); long nowMs = SystemClock.elapsedRealtime(); long durationMs = nowMs - startTimeMs; - if (loadable.isLoadCanceled()) { + if (canceled) { callback.onLoadCanceled(loadable, nowMs, durationMs, false); return; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java index 7ef79b8963..987effcf43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java @@ -78,7 +78,6 @@ public final class ParsingLoadable implements Loadable { private final Parser parser; private volatile T result; - private volatile boolean isCanceled; private volatile long bytesLoaded; /** @@ -128,14 +127,7 @@ public final class ParsingLoadable implements Loadable { @Override public final void cancelLoad() { - // We don't actually cancel anything, but we need to record the cancellation so that - // isLoadCanceled can return the correct value. - isCanceled = true; - } - - @Override - public final boolean isLoadCanceled() { - return isCanceled; + // Do nothing. } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java index 4026bc0c37..f9ebee78d6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -391,11 +391,6 @@ public final class AdaptiveTrackSelectionTest { // Do nothing. } - @Override - public boolean isLoadCanceled() { - return false; - } - @Override public void load() throws IOException, InterruptedException { // Do nothing. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 9e993aa27b..99a5b44574 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -206,11 +206,6 @@ import java.util.concurrent.atomic.AtomicInteger; loadCanceled = true; } - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - @Override public void load() throws IOException, InterruptedException { maybeLoadInitData(); From a535da81288782d52782e57b966f1d4c8f5b0dc4 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 1 Jun 2018 08:25:29 -0700 Subject: [PATCH 018/106] Fix starting the download service in the background throw exception This happens when the device screen is locked. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198875192 --- .../exoplayer2/demo/SampleChooserActivity.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 5524f98257..7c6dbfc88a 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 @@ -95,8 +95,16 @@ public class SampleChooserActivity extends Activity loaderTask.execute(uris); // Ping the download service in case it's not running (but should be). - startService( - new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT)); + Intent serviceIntent = + new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT); + // Starting the service in the foreground causes notification flicker if there is no scheduled + // action. Starting it in the background throws an exception if the app is in the background too + // (e.g. if device screen is locked). + try { + startService(serviceIntent); + } catch (IllegalStateException e) { + startForegroundService(serviceIntent); + } } @Override From b68dcb0b5b49650c8e42e152fee16472a995f1df Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 4 Jun 2018 03:00:35 -0700 Subject: [PATCH 019/106] Allow setting player lazily in AnalyticsCollector. This helps to use the AnalyticsCollector without SimpleExoPlayer. Currently, that may be problematic, if the contructor needs the player, but in order to create the player, one already needs the AnalyticsCollector as a listener for the renderers. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199105012 --- .../analytics/AnalyticsCollector.java | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 43ef308f27..8f4267efce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -46,6 +46,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** * Data collector which is able to forward analytics events to {@link AnalyticsListener}s by @@ -66,29 +67,34 @@ public class AnalyticsCollector /** * Creates an analytics collector for the specified player. * - * @param player The {@link Player} for which data will be collected. + * @param player The {@link Player} for which data will be collected. Can be null, if the player + * is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics + * collector. * @param clock A {@link Clock} used to generate timestamps. * @return An analytics collector. */ - public AnalyticsCollector createAnalyticsCollector(Player player, Clock clock) { + public AnalyticsCollector createAnalyticsCollector(@Nullable Player player, Clock clock) { return new AnalyticsCollector(player, clock); } } private final CopyOnWriteArraySet listeners; - private final Player player; private final Clock clock; private final Window window; private final MediaPeriodQueueTracker mediaPeriodQueueTracker; + private @MonotonicNonNull Player player; + /** * Creates an analytics collector for the specified player. * - * @param player The {@link Player} for which data will be collected. + * @param player The {@link Player} for which data will be collected. Can be null, if the player + * is set by calling {@link AnalyticsCollector#setPlayer(Player)} before using the analytics + * collector. * @param clock A {@link Clock} used to generate timestamps. */ - protected AnalyticsCollector(Player player, Clock clock) { - this.player = Assertions.checkNotNull(player); + protected AnalyticsCollector(@Nullable Player player, Clock clock) { + this.player = player; this.clock = Assertions.checkNotNull(clock); listeners = new CopyOnWriteArraySet<>(); mediaPeriodQueueTracker = new MediaPeriodQueueTracker(); @@ -113,6 +119,17 @@ public class AnalyticsCollector listeners.remove(listener); } + /** + * Sets the player for which data will be collected. Must only be called if no player has been set + * yet. + * + * @param player The {@link Player} for which data will be collected. + */ + public void setPlayer(Player player) { + Assertions.checkState(this.player == null); + this.player = Assertions.checkNotNull(player); + } + // External events. /** @@ -541,6 +558,7 @@ public class AnalyticsCollector /** Returns a new {@link EventTime} for the specified window index and media period id. */ protected EventTime generateEventTime(int windowIndex, @Nullable MediaPeriodId mediaPeriodId) { + Assertions.checkNotNull(player); long realtimeMs = clock.elapsedRealtime(); Timeline timeline = player.getCurrentTimeline(); long eventPositionMs; @@ -579,7 +597,7 @@ public class AnalyticsCollector private EventTime generateEventTime(@Nullable WindowAndMediaPeriodId mediaPeriod) { if (mediaPeriod == null) { - int windowIndex = player.getCurrentWindowIndex(); + int windowIndex = Assertions.checkNotNull(player).getCurrentWindowIndex(); MediaPeriodId mediaPeriodId = mediaPeriodQueueTracker.tryResolveWindowIndex(windowIndex); return generateEventTime(windowIndex, mediaPeriodId); } From 0d59dc4c436d78d809a3ca4d4d2a36c1711fae84 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Jun 2018 05:47:51 -0700 Subject: [PATCH 020/106] Fix variable name ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199120421 --- .../android/exoplayer2/video/DummySurface.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 2f41831a5e..996e6f30ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -156,7 +156,7 @@ public final class DummySurface extends Surface { private static final int MSG_INIT = 1; private static final int MSG_RELEASE = 2; - private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexure; + private @MonotonicNonNull EGLSurfaceTexture eglSurfaceTexture; private @MonotonicNonNull Handler handler; private @Nullable Error initError; private @Nullable RuntimeException initException; @@ -169,7 +169,7 @@ public final class DummySurface extends Surface { public DummySurface init(@SecureMode int secureMode) { start(); handler = new Handler(getLooper(), /* callback= */ this); - eglSurfaceTexure = new EGLSurfaceTexture(handler); + eglSurfaceTexture = new EGLSurfaceTexture(handler); boolean wasInterrupted = false; synchronized (this) { handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget(); @@ -232,16 +232,16 @@ public final class DummySurface extends Surface { } private void initInternal(@SecureMode int secureMode) { - Assertions.checkNotNull(eglSurfaceTexure); - eglSurfaceTexure.init(secureMode); + Assertions.checkNotNull(eglSurfaceTexture); + eglSurfaceTexture.init(secureMode); this.surface = new DummySurface( - this, eglSurfaceTexure.getSurfaceTexture(), secureMode != SECURE_MODE_NONE); + this, eglSurfaceTexture.getSurfaceTexture(), secureMode != SECURE_MODE_NONE); } private void releaseInternal() { - Assertions.checkNotNull(eglSurfaceTexure); - eglSurfaceTexure.release(); + Assertions.checkNotNull(eglSurfaceTexture); + eglSurfaceTexture.release(); } } From 5a6507b72da90d8589af5f557a368ae065a36835 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 4 Jun 2018 07:30:27 -0700 Subject: [PATCH 021/106] Fix starting the download service in the background throw exception This happens when the device screen is locked. This fixes a previous attempt to fix the problem. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199130325 --- .../demo/SampleChooserActivity.java | 8 ++--- .../exoplayer2/offline/DownloadService.java | 31 +++++++++++++++++-- 2 files changed, 32 insertions(+), 7 deletions(-) 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 7c6dbfc88a..d87fca8e58 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 @@ -94,16 +94,14 @@ public class SampleChooserActivity extends Activity SampleListLoader loaderTask = new SampleListLoader(); loaderTask.execute(uris); - // Ping the download service in case it's not running (but should be). - Intent serviceIntent = - new Intent(this, DemoDownloadService.class).setAction(DownloadService.ACTION_INIT); + // Start the download service if it should be running but it's not currently. // Starting the service in the foreground causes notification flicker if there is no scheduled // action. Starting it in the background throws an exception if the app is in the background too // (e.g. if device screen is locked). try { - startService(serviceIntent); + DownloadService.start(this, DemoDownloadService.class); } catch (IllegalStateException e) { - startForegroundService(serviceIntent); + DownloadService.startForeground(this, DemoDownloadService.class); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 21fd541b52..6dae3f70b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -160,9 +160,9 @@ public abstract class DownloadService extends Service { * Starts the service, adding an action to be executed. * * @param context A {@link Context}. - * @param clazz The concrete download service being targeted by the intent. + * @param clazz The concrete download service to be started. * @param downloadAction The action to be executed. - * @param foreground Whether this intent will be used to start the service in the foreground. + * @param foreground Whether the service is started in the foreground. */ public static void startWithAction( Context context, @@ -177,6 +177,33 @@ public abstract class DownloadService extends Service { } } + /** + * Starts the service without adding a new action. If there are any not finished actions and the + * requirements are met, the service resumes executing actions. Otherwise it stops immediately. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @see #startForeground(Context, Class) + */ + public static void start(Context context, Class clazz) { + context.startService(new Intent(context, clazz).setAction(ACTION_INIT)); + } + + /** + * Starts the service in the foreground without adding a new action. If there are any not finished + * actions and the requirements are met, the service resumes executing actions. Otherwise it stops + * immediately. + * + * @param context A {@link Context}. + * @param clazz The concrete download service to be started. + * @see #start(Context, Class) + */ + public static void startForeground(Context context, Class clazz) { + Intent intent = + new Intent(context, clazz).setAction(ACTION_INIT).putExtra(KEY_FOREGROUND, true); + Util.startForegroundService(context, intent); + } + @Override public void onCreate() { logd("onCreate"); From 6a82f99ca15c09ca95742cdc9d96bebca832a29e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Jun 2018 09:24:05 -0700 Subject: [PATCH 022/106] Explicitly null MediaPeriod callbacks on release If a MediaPeriod uses a Loadable, then there are typically reference chains of the form: LoadingThread[GCroot]->Loadable->MediaPeriod->Player Where the player is the MediaPeriod callback. When the player is released, this reference chain prevents the player from being GC'd until Loadable cancellation completes, which may not always be fast. This in turn will typically prevent the application's activity from being GC'd, since it'll normally be registered as a listener on the player (directly or indirectly via something like a view). This change mitigates the issue by removing references that the MediaPeriod holds back to the player. The MediaPeriod will still not be eligible for GC, but the player and application activity will be, which in most cases will be most of the leak (in terms of size). Issue: #4249 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199143646 --- .../android/exoplayer2/source/ExtractorMediaPeriod.java | 3 ++- .../android/exoplayer2/source/dash/DashMediaPeriod.java | 4 +++- .../google/android/exoplayer2/source/hls/HlsMediaPeriod.java | 4 +++- .../exoplayer2/source/smoothstreaming/SsMediaPeriod.java | 4 +++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index d4ea6191aa..63c86c2c96 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -90,7 +90,7 @@ import java.util.Arrays; private final Runnable onContinueLoadingRequestedRunnable; private final Handler handler; - private Callback callback; + private @Nullable Callback callback; private SeekMap seekMap; private SampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; @@ -190,6 +190,7 @@ import java.util.Arrays; } loader.release(this); handler.removeCallbacksAndMessages(null); + callback = null; released = true; eventDispatcher.mediaPeriodReleased(); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index d2982481e0..f80ff89fc1 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.dash; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Pair; import android.util.SparseArray; import android.util.SparseIntArray; @@ -72,7 +73,7 @@ import java.util.List; private final IdentityHashMap, PlayerTrackEmsgHandler> trackEmsgHandlerBySampleStream; - private Callback callback; + private @Nullable Callback callback; private ChunkSampleStream[] sampleStreams; private EventSampleStream[] eventSampleStreams; private SequenceableLoader compositeSequenceableLoader; @@ -150,6 +151,7 @@ import java.util.List; for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.release(this); } + callback = null; eventDispatcher.mediaPeriodReleased(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 1a3f41fffc..b142d38df9 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; @@ -57,7 +58,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final boolean allowChunklessPreparation; - private Callback callback; + private @Nullable Callback callback; private int pendingPrepareCount; private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; @@ -96,6 +97,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { sampleStreamWrapper.release(); } + callback = null; eventDispatcher.mediaPeriodReleased(); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 9a0d57ff31..8e7c3e38c9 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.smoothstreaming; +import android.support.annotation.Nullable; import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; @@ -52,7 +53,7 @@ import java.util.ArrayList; private final TrackEncryptionBox[] trackEncryptionBoxes; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private Callback callback; + private @Nullable Callback callback; private SsManifest manifest; private ChunkSampleStream[] sampleStreams; private SequenceableLoader compositeSequenceableLoader; @@ -98,6 +99,7 @@ import java.util.ArrayList; for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.release(); } + callback = null; eventDispatcher.mediaPeriodReleased(); } From 3c6ca19c8567231d9050d76d5152f9f87b9cf7c7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Jun 2018 02:30:25 -0700 Subject: [PATCH 023/106] Fix track selection nullability issues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199266768 --- library/core/build.gradle | 1 + .../AdaptiveTrackSelection.java | 7 +- .../trackselection/DefaultTrackSelector.java | 128 +++++++++++------- .../trackselection/FixedTrackSelection.java | 12 +- .../trackselection/MappingTrackSelector.java | 29 ++-- .../trackselection/RandomTrackSelection.java | 4 +- .../trackselection/TrackSelection.java | 7 +- .../trackselection/TrackSelectionArray.java | 17 +-- .../trackselection/TrackSelector.java | 3 +- .../trackselection/TrackSelectorResult.java | 7 +- .../google/android/exoplayer2/util/Util.java | 6 +- .../exoplayer2/ui/TrackSelectionView.java | 5 +- 12 files changed, 130 insertions(+), 96 deletions(-) diff --git a/library/core/build.gradle b/library/core/build.gradle index bb331b615c..aa2e06fb3d 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -54,6 +54,7 @@ android { dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index b28dc6ca6f..0aa6dcffaa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; @@ -242,9 +243,11 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; this.clock = clock; playbackSpeed = 1f; - selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); reason = C.SELECTION_REASON_INITIAL; lastBufferEvaluationMs = C.TIME_UNSET; + @SuppressWarnings("nullness:method.invocation.invalid") + int selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); + this.selectedIndex = selectedIndex; } @Override @@ -301,7 +304,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } @Override - public Object getSelectionData() { + public @Nullable Object getSelectionData() { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 71d2544784..10b11044d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -19,7 +19,6 @@ import android.content.Context; import android.graphics.Point; import android.os.Parcel; import android.os.Parcelable; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Pair; @@ -44,6 +43,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A default {@link TrackSelector} suitable for most use cases. Track selections are made according @@ -161,8 +161,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final SparseArray> selectionOverrides; private final SparseBooleanArray rendererDisabledFlags; - private String preferredAudioLanguage; - private String preferredTextLanguage; + private @Nullable String preferredAudioLanguage; + private @Nullable String preferredTextLanguage; private boolean selectUndeterminedTextLanguage; private int disabledTextTrackSelectionFlags; private boolean forceLowestBitrate; @@ -572,14 +572,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. * {@code null} selects the default track, or the first track if there's no default. */ - public final String preferredAudioLanguage; + public final @Nullable String preferredAudioLanguage; // Text /** * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the * default track if there is one, or no track otherwise. */ - public final String preferredTextLanguage; + public final @Nullable String preferredTextLanguage; /** * Whether a text track with undetermined language should be selected if no track with * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. @@ -673,8 +673,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { /* package */ Parameters( SparseArray> selectionOverrides, SparseBooleanArray rendererDisabledFlags, - String preferredAudioLanguage, - String preferredTextLanguage, + @Nullable String preferredAudioLanguage, + @Nullable String preferredTextLanguage, boolean selectUndeterminedTextLanguage, int disabledTextTrackSelectionFlags, boolean forceLowestBitrate, @@ -759,7 +759,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param groups The {@link TrackGroupArray}. * @return The override, or null if no override exists. */ - public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { + public final @Nullable SelectionOverride getSelectionOverride( + int rendererIndex, TrackGroupArray groups) { Map overrides = selectionOverrides.get(rendererIndex); return overrides != null ? overrides.get(groups) : null; } @@ -816,8 +817,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + viewportHeight; result = 31 * result + maxVideoBitrate; result = 31 * result + tunnelingAudioSessionId; - result = 31 * result + preferredAudioLanguage.hashCode(); - result = 31 * result + preferredTextLanguage.hashCode(); + result = + 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode()); + result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode()); return result; } @@ -1042,7 +1044,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static final int[] NO_TRACKS = new int[0]; private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000; - private final TrackSelection.Factory adaptiveTrackSelectionFactory; + private final @Nullable TrackSelection.Factory adaptiveTrackSelectionFactory; private final AtomicReference parametersReference; /** @@ -1069,7 +1071,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null if * the selector should not support adaptive tracks. */ - public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { + public DefaultTrackSelector(@Nullable TrackSelection.Factory adaptiveTrackSelectionFactory) { this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; parametersReference = new AtomicReference<>(Parameters.DEFAULT); } @@ -1139,7 +1141,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** @deprecated Use {@link Parameters#getSelectionOverride(int, TrackGroupArray)}. */ @Deprecated - public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) { + public final @Nullable SelectionOverride getSelectionOverride( + int rendererIndex, TrackGroupArray groups) { return getParameters().getSelectionOverride(rendererIndex, groups); } @@ -1170,14 +1173,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { // MappingTrackSelector implementation. @Override - protected final Pair selectTracks( - MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupports) - throws ExoPlaybackException { + protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> + selectTracks( + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupports) + throws ExoPlaybackException { Parameters params = parametersReference.get(); int rendererCount = mappedTrackInfo.getRendererCount(); - TrackSelection[] rendererTrackSelections = + @NullableType TrackSelection[] rendererTrackSelections = selectAllTracks( mappedTrackInfo, rendererFormatSupports, @@ -1200,8 +1204,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererTrackGroups.get(override.groupIndex), override.tracks[0]); } else { rendererTrackSelections[i] = - adaptiveTrackSelectionFactory.createTrackSelection( - rendererTrackGroups.get(override.groupIndex), override.tracks); + Assertions.checkNotNull(adaptiveTrackSelectionFactory) + .createTrackSelection( + rendererTrackGroups.get(override.groupIndex), override.tracks); } } } @@ -1209,7 +1214,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Initialize the renderer configurations to the default configuration for all renderers with // selections, and null otherwise. - RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCount]; + @NullableType RendererConfiguration[] rendererConfigurations = + new RendererConfiguration[rendererCount]; for (int i = 0; i < rendererCount; i++) { boolean forceRendererDisabled = params.getRendererDisabled(i); boolean rendererEnabled = @@ -1248,14 +1254,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { * disabled, unless RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection[] selectAllTracks( + protected @NullableType TrackSelection[] selectAllTracks( MappedTrackInfo mappedTrackInfo, int[][][] rendererFormatSupports, int[] rendererMixedMimeTypeAdaptationSupports, Parameters params) throws ExoPlaybackException { int rendererCount = mappedTrackInfo.getRendererCount(); - TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; + @NullableType TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; boolean seenVideoRendererWithMappedTracks = false; boolean selectedVideoTracks = false; @@ -1331,12 +1337,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectVideoTrack( + protected @Nullable TrackSelection selectVideoTrack( TrackGroupArray groups, int[][] formatSupports, int mixedMimeTypeAdaptationSupports, Parameters params, - TrackSelection.Factory adaptiveTrackSelectionFactory) + @Nullable TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { TrackSelection selection = null; if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) { @@ -1354,7 +1360,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { return selection; } - private static TrackSelection selectAdaptiveVideoTrack( + private static @Nullable TrackSelection selectAdaptiveVideoTrack( TrackGroupArray groups, int[][] formatSupport, int mixedMimeTypeAdaptationSupports, @@ -1374,7 +1380,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { params.maxVideoBitrate, params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); if (adaptiveTracks.length > 0) { - return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); + return Assertions.checkNotNull(adaptiveTrackSelectionFactory) + .createTrackSelection(group, adaptiveTracks); } } return null; @@ -1397,7 +1404,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { String selectedMimeType = null; if (!allowMixedMimeTypes) { // Select the mime type for which we have the most adaptive tracks. - HashSet seenMimeTypes = new HashSet<>(); + HashSet<@NullableType String> seenMimeTypes = new HashSet<>(); int selectedMimeTypeTrackCount = 0; for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); @@ -1421,9 +1428,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices); } - private static int getAdaptiveVideoTrackCountForMimeType(TrackGroup group, int[] formatSupport, - int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, List selectedTrackIndices) { + private static int getAdaptiveVideoTrackCountForMimeType( + TrackGroup group, + int[] formatSupport, + int requiredAdaptiveSupport, + @Nullable String mimeType, + int maxVideoWidth, + int maxVideoHeight, + int maxVideoBitrate, + List selectedTrackIndices) { int adaptiveTrackCount = 0; for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); @@ -1436,9 +1449,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { return adaptiveTrackCount; } - private static void filterAdaptiveVideoTrackCountForMimeType(TrackGroup group, - int[] formatSupport, int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, - int maxVideoHeight, int maxVideoBitrate, List selectedTrackIndices) { + private static void filterAdaptiveVideoTrackCountForMimeType( + TrackGroup group, + int[] formatSupport, + int requiredAdaptiveSupport, + @Nullable String mimeType, + int maxVideoWidth, + int maxVideoHeight, + int maxVideoBitrate, + List selectedTrackIndices) { for (int i = selectedTrackIndices.size() - 1; i >= 0; i--) { int trackIndex = selectedTrackIndices.get(i); if (!isSupportedAdaptiveVideoTrack(group.getFormat(trackIndex), mimeType, @@ -1449,8 +1468,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType, - int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, + private static boolean isSupportedAdaptiveVideoTrack( + Format format, + @Nullable String mimeType, + int formatSupport, + int requiredAdaptiveSupport, + int maxVideoWidth, + int maxVideoHeight, int maxVideoBitrate) { return isSupported(formatSupport, false) && ((formatSupport & requiredAdaptiveSupport) != 0) && (mimeType == null || Util.areEqual(format.sampleMimeType, mimeType)) @@ -1459,7 +1483,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); } - private static TrackSelection selectFixedVideoTrack( + private static @Nullable TrackSelection selectFixedVideoTrack( TrackGroupArray groups, int[][] formatSupports, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; @@ -1537,12 +1561,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectAudioTrack( + protected @Nullable TrackSelection selectAudioTrack( TrackGroupArray groups, int[][] formatSupports, int mixedMimeTypeAdaptationSupports, Parameters params, - TrackSelection.Factory adaptiveTrackSelectionFactory) + @Nullable TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { int selectedTrackIndex = C.INDEX_UNSET; int selectedGroupIndex = C.INDEX_UNSET; @@ -1606,8 +1630,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { int[] adaptiveIndices = new int[selectedConfigurationTrackCount]; int index = 0; for (int i = 0; i < group.length; i++) { - if (isSupportedAdaptiveAudioTrack(group.getFormat(i), formatSupport[i], - selectedConfiguration)) { + if (isSupportedAdaptiveAudioTrack( + group.getFormat(i), formatSupport[i], Assertions.checkNotNull(selectedConfiguration))) { adaptiveIndices[index++] = i; } } @@ -1648,7 +1672,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectTextTrack( + protected @Nullable TrackSelection selectTextTrack( TrackGroupArray groups, int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; @@ -1721,7 +1745,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @return The {@link TrackSelection} for the renderer, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected TrackSelection selectOtherTrack( + protected @Nullable TrackSelection selectOtherTrack( int trackType, TrackGroupArray groups, int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; @@ -1768,8 +1792,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void maybeConfigureRenderersForTunneling( MappedTrackInfo mappedTrackInfo, int[][][] renderererFormatSupports, - RendererConfiguration[] rendererConfigurations, - TrackSelection[] trackSelections, + @NullableType RendererConfiguration[] rendererConfigurations, + @NullableType TrackSelection[] trackSelections, int tunnelingAudioSessionId) { if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) { return; @@ -1883,15 +1907,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns whether a {@link Format} specifies a particular language, or {@code false} if - * {@code language} is null. + * Returns whether a {@link Format} specifies a particular language, or {@code false} if {@code + * language} is null. * * @param format The {@link Format}. * @param language The language. * @return Whether the format specifies the language, or {@code false} if {@code language} is * null. */ - protected static boolean formatHasLanguage(Format format, String language) { + protected static boolean formatHasLanguage(Format format, @Nullable String language) { return language != null && TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); } @@ -1997,7 +2021,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * negative integer if this score is worse than the other. */ @Override - public int compareTo(@NonNull AudioTrackScore other) { + public int compareTo(AudioTrackScore other) { if (this.withinRendererCapabilitiesScore != other.withinRendererCapabilitiesScore) { return compareInts(this.withinRendererCapabilitiesScore, other.withinRendererCapabilitiesScore); @@ -2066,9 +2090,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final int channelCount; public final int sampleRate; - public final String mimeType; + public final @Nullable String mimeType; - public AudioConfigurationTuple(int channelCount, int sampleRate, String mimeType) { + public AudioConfigurationTuple(int channelCount, int sampleRate, @Nullable String mimeType) { this.channelCount = channelCount; this.sampleRate = sampleRate; this.mimeType = mimeType; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java index 50873d372d..2aecf624da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.util.Assertions; @@ -30,7 +31,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { public static final class Factory implements TrackSelection.Factory { private final int reason; - private final Object data; + private final @Nullable Object data; public Factory() { this.reason = C.SELECTION_REASON_UNKNOWN; @@ -41,7 +42,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { * @param reason A reason for the track selection. * @param data Optional data associated with the track selection. */ - public Factory(int reason, Object data) { + public Factory(int reason, @Nullable Object data) { this.reason = reason; this.data = data; } @@ -51,11 +52,10 @@ public final class FixedTrackSelection extends BaseTrackSelection { Assertions.checkArgument(tracks.length == 1); return new FixedTrackSelection(group, tracks[0], reason, data); } - } private final int reason; - private final Object data; + private final @Nullable Object data; /** * @param group The {@link TrackGroup}. Must not be null. @@ -71,7 +71,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { * @param reason A reason for the track selection. * @param data Optional data associated with the track selection. */ - public FixedTrackSelection(TrackGroup group, int track, int reason, Object data) { + public FixedTrackSelection(TrackGroup group, int track, int reason, @Nullable Object data) { super(group, track); this.reason = reason; this.data = data; @@ -94,7 +94,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { } @Override - public Object getSelectionData() { + public @Nullable Object getSelectionData() { return data; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 4af969369e..99e4e58c4a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.trackselection; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s @@ -301,13 +303,13 @@ public abstract class MappingTrackSelector extends TrackSelector { } - private MappedTrackInfo currentMappedTrackInfo; + private @Nullable MappedTrackInfo currentMappedTrackInfo; /** * Returns the mapping information for the currently active track selection, or null if no * selection is currently active. */ - public final MappedTrackInfo getCurrentMappedTrackInfo() { + public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() { return currentMappedTrackInfo; } @@ -357,9 +359,11 @@ public abstract class MappingTrackSelector extends TrackSelector { int[] rendererTrackTypes = new int[rendererCapabilities.length]; for (int i = 0; i < rendererCapabilities.length; i++) { int rendererTrackGroupCount = rendererTrackGroupCounts[i]; - rendererTrackGroupArrays[i] = new TrackGroupArray( - Arrays.copyOf(rendererTrackGroups[i], rendererTrackGroupCount)); - rendererFormatSupports[i] = Arrays.copyOf(rendererFormatSupports[i], rendererTrackGroupCount); + rendererTrackGroupArrays[i] = + new TrackGroupArray( + Util.nullSafeArrayCopy(rendererTrackGroups[i], rendererTrackGroupCount)); + rendererFormatSupports[i] = + Util.nullSafeArrayCopy(rendererFormatSupports[i], rendererTrackGroupCount); rendererTrackTypes[i] = rendererCapabilities[i].getTrackType(); } @@ -367,7 +371,7 @@ public abstract class MappingTrackSelector extends TrackSelector { int unmappedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length]; TrackGroupArray unmappedTrackGroupArray = new TrackGroupArray( - Arrays.copyOf( + Util.nullSafeArrayCopy( rendererTrackGroups[rendererCapabilities.length], unmappedTrackGroupCount)); // Package up the track information and selections. @@ -379,7 +383,7 @@ public abstract class MappingTrackSelector extends TrackSelector { rendererFormatSupports, unmappedTrackGroupArray); - Pair result = + Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> result = selectTracks( mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports); return new TrackSelectorResult(result.first, result.second, mappedTrackInfo); @@ -399,11 +403,12 @@ public abstract class MappingTrackSelector extends TrackSelector { * RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected abstract Pair selectTracks( - MappedTrackInfo mappedTrackInfo, - int[][][] rendererFormatSupports, - int[] rendererMixedMimeTypeAdaptationSupport) - throws ExoPlaybackException; + protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> + selectTracks( + MappedTrackInfo mappedTrackInfo, + int[][][] rendererFormatSupports, + int[] rendererMixedMimeTypeAdaptationSupport) + throws ExoPlaybackException; /** * Finds the renderer to which the provided {@link TrackGroup} should be mapped. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java index d11344a6f6..e1bdc73986 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.trackselection; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.TrackGroup; import java.util.Random; @@ -47,7 +48,6 @@ public final class RandomTrackSelection extends BaseTrackSelection { public RandomTrackSelection createTrackSelection(TrackGroup group, int... tracks) { return new RandomTrackSelection(group, tracks, random); } - } private final Random random; @@ -123,7 +123,7 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public Object getSelectionData() { + public @Nullable Object getSelectionData() { return null; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index 55e6050622..58616996ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; @@ -129,10 +130,8 @@ public interface TrackSelection { */ int getSelectionReason(); - /** - * Returns optional data associated with the current track selection. - */ - Object getSelectionData(); + /** Returns optional data associated with the current track selection. */ + @Nullable Object getSelectionData(); // Adaptation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java index b37c8cad67..48151002be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection; import android.support.annotation.Nullable; import java.util.Arrays; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** An array of {@link TrackSelection}s. */ public final class TrackSelectionArray { @@ -24,15 +25,13 @@ public final class TrackSelectionArray { /** The length of this array. */ public final int length; - private final TrackSelection[] trackSelections; + private final @NullableType TrackSelection[] trackSelections; // Lazily initialized hashcode. private int hashCode; - /** - * @param trackSelections The selections. Must not be null, but may contain null elements. - */ - public TrackSelectionArray(TrackSelection... trackSelections) { + /** @param trackSelections The selections. Must not be null, but may contain null elements. */ + public TrackSelectionArray(@NullableType TrackSelection... trackSelections) { this.trackSelections = trackSelections; this.length = trackSelections.length; } @@ -43,14 +42,12 @@ public final class TrackSelectionArray { * @param index The index of the selection. * @return The selection. */ - public TrackSelection get(int index) { + public @Nullable TrackSelection get(int index) { return trackSelections[index]; } - /** - * Returns the selections in a newly allocated array. - */ - public TrackSelection[] getAll() { + /** Returns the selections in a newly allocated array. */ + public @NullableType TrackSelection[] getAll() { return trackSelections.clone(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index a26fee6f78..0c229527a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.trackselection; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Renderer; @@ -89,7 +90,7 @@ public abstract class TrackSelector { } - private InvalidationListener listener; + private @Nullable InvalidationListener listener; /** * Called by the player to initialize the selector. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 882d98764e..f1136f0be5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.util.Util; +import org.checkerframework.checker.nullness.compatqual.NullableType; /** * The result of a {@link TrackSelector} operation. @@ -29,7 +30,7 @@ public final class TrackSelectorResult { * A {@link RendererConfiguration} for each renderer. A null entry indicates the corresponding * renderer should be disabled. */ - public final RendererConfiguration[] rendererConfigurations; + public final @NullableType RendererConfiguration[] rendererConfigurations; /** * A {@link TrackSelectionArray} containing the track selection for each renderer. */ @@ -48,7 +49,9 @@ public final class TrackSelectorResult { * TrackSelector#onSelectionActivated(Object)} should the selection be activated. */ public TrackSelectorResult( - RendererConfiguration[] rendererConfigurations, TrackSelection[] selections, Object info) { + @NullableType RendererConfiguration[] rendererConfigurations, + @NullableType TrackSelection[] selections, + Object info) { this.rendererConfigurations = rendererConfigurations; this.selections = new TrackSelectionArray(selections); this.info = info; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index fe83ce13e6..37e3e119bf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -311,10 +311,10 @@ public final class Util { * Returns a normalized RFC 639-2/T code for {@code language}. * * @param language A case-insensitive ISO 639 alpha-2 or alpha-3 language code. - * @return The all-lowercase normalized code, or null if the input was null, or - * {@code language.toLowerCase()} if the language could not be normalized. + * @return The all-lowercase normalized code, or null if the input was null, or {@code + * language.toLowerCase()} if the language could not be normalized. */ - public static String normalizeLanguageCode(String language) { + public static @Nullable String normalizeLanguageCode(@Nullable String language) { try { return language == null ? null : new Locale(language).getISO3Language(); } catch (MissingResourceException e) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java index be0babf5a8..fe5d5cbbc5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -203,7 +203,9 @@ public class TrackSelectionView extends LinearLayout { removeViewAt(i); } - if (trackSelector == null) { + MappingTrackSelector.MappedTrackInfo trackInfo = + trackSelector == null ? null : trackSelector.getCurrentMappedTrackInfo(); + if (trackSelector == null || trackInfo == null) { // The view is not initialized. disableView.setEnabled(false); defaultView.setEnabled(false); @@ -212,7 +214,6 @@ public class TrackSelectionView extends LinearLayout { disableView.setEnabled(true); defaultView.setEnabled(true); - MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo(); trackGroups = trackInfo.getTrackGroups(rendererIndex); DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); From 4998354cb1e807c669465cd0d39a785b7404c226 Mon Sep 17 00:00:00 2001 From: Anton Potekhin Date: Tue, 29 May 2018 10:28:17 +0300 Subject: [PATCH 024/106] Blacklist Moto C from setOutputSurface Issue: #4315 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 d166297054..236a6aaadb 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 @@ -1179,6 +1179,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/4084, // https://github.com/google/ExoPlayer/issues/4104. // https://github.com/google/ExoPlayer/issues/4134. + // https://github.com/google/ExoPlayer/issues/4315 return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) || "flo".equals(Util.DEVICE) // Nexus 7 (2013) || "mido".equals(Util.DEVICE) // Redmi Note 4 @@ -1192,7 +1193,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { || "M5c".equals(Util.DEVICE) // Meizu M5C || "QM16XE_U".equals(Util.DEVICE) // Philips QM163E || "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note - || "woods_f".equals(Util.MODEL)) // Moto E (4) + || "woods_f".equals(Util.MODEL) // Moto E (4) + || "watson".equals(Util.DEVICE)) // Moto C && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) || (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite || "CAM-L21".equals(Util.MODEL)) // Huawei Y6II From 993eb8eda078845b3fbbc4524d53ed04d27425de Mon Sep 17 00:00:00 2001 From: takusemba Date: Tue, 29 May 2018 15:46:42 +0900 Subject: [PATCH 025/106] close initDataSource after reading --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 99a5b44574..3cd8556580 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -237,7 +237,7 @@ import java.util.concurrent.atomic.AtomicInteger; initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition); } } finally { - Util.closeQuietly(dataSource); + Util.closeQuietly(initDataSource); } initLoadCompleted = true; } From 615b2b103980403187341a1843c215f1605abcc4 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 5 Jun 2018 19:34:54 +0100 Subject: [PATCH 026/106] Fix punctuation --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 236a6aaadb..fe50f26717 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 @@ -1177,9 +1177,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/3835, // https://github.com/google/ExoPlayer/issues/4006, // https://github.com/google/ExoPlayer/issues/4084, - // https://github.com/google/ExoPlayer/issues/4104. - // https://github.com/google/ExoPlayer/issues/4134. - // https://github.com/google/ExoPlayer/issues/4315 + // https://github.com/google/ExoPlayer/issues/4104, + // https://github.com/google/ExoPlayer/issues/4134, + // https://github.com/google/ExoPlayer/issues/4315. return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) || "flo".equals(Util.DEVICE) // Nexus 7 (2013) || "mido".equals(Util.DEVICE) // Redmi Note 4 From 97e68ecc3120036c6fbeaae28b60ec0eb7549dfb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 5 Jun 2018 22:30:58 +0100 Subject: [PATCH 027/106] Fix release branch --- .../android/exoplayer2/source/ConcatenatingMediaSource.java | 1 - .../exoplayer2/trackselection/DefaultTrackSelector.java | 6 +++--- .../exoplayer2/trackselection/MappingTrackSelector.java | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) 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 b3e0b1404d..6f7ffa262d 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 @@ -668,7 +668,6 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); this.uid = new Object(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 10b11044d3..ab9534adb7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -1173,7 +1173,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // MappingTrackSelector implementation. @Override - protected final Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> + protected final Pair selectTracks( MappedTrackInfo mappedTrackInfo, int[][][] rendererFormatSupports, @@ -1181,7 +1181,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { throws ExoPlaybackException { Parameters params = parametersReference.get(); int rendererCount = mappedTrackInfo.getRendererCount(); - @NullableType TrackSelection[] rendererTrackSelections = + TrackSelection[] rendererTrackSelections = selectAllTracks( mappedTrackInfo, rendererFormatSupports, @@ -1404,7 +1404,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { String selectedMimeType = null; if (!allowMixedMimeTypes) { // Select the mime type for which we have the most adaptive tracks. - HashSet<@NullableType String> seenMimeTypes = new HashSet<>(); + HashSet seenMimeTypes = new HashSet<>(); int selectedMimeTypeTrackCount = 0; for (int i = 0; i < selectedTrackIndices.size(); i++) { int trackIndex = selectedTrackIndices.get(i); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 99e4e58c4a..2eb0440669 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -383,7 +383,7 @@ public abstract class MappingTrackSelector extends TrackSelector { rendererFormatSupports, unmappedTrackGroupArray); - Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> result = + Pair result = selectTracks( mappedTrackInfo, rendererFormatSupports, rendererMixedMimeTypeAdaptationSupports); return new TrackSelectorResult(result.first, result.second, mappedTrackInfo); @@ -403,7 +403,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected abstract Pair<@NullableType RendererConfiguration[], @NullableType TrackSelection[]> + protected abstract Pair selectTracks( MappedTrackInfo mappedTrackInfo, int[][][] rendererFormatSupports, From a877bbaf7ba51e8adc672919663b6ed50c38378a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 5 Jun 2018 22:37:44 +0100 Subject: [PATCH 028/106] Remove NullableType usage from release branch --- library/core/build.gradle | 1 - .../trackselection/DefaultTrackSelector.java | 11 +++++------ .../trackselection/MappingTrackSelector.java | 1 - .../trackselection/TrackSelectionArray.java | 7 +++---- .../trackselection/TrackSelectorResult.java | 7 +++---- 5 files changed, 11 insertions(+), 16 deletions(-) diff --git a/library/core/build.gradle b/library/core/build.gradle index aa2e06fb3d..bb331b615c 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -54,7 +54,6 @@ android { dependencies { implementation 'com.android.support:support-annotations:' + supportLibraryVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkVersion androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation 'com.google.truth:truth:' + truthVersion diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index ab9534adb7..3bbb2a7941 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -43,7 +43,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** * A default {@link TrackSelector} suitable for most use cases. Track selections are made according @@ -1214,7 +1213,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Initialize the renderer configurations to the default configuration for all renderers with // selections, and null otherwise. - @NullableType RendererConfiguration[] rendererConfigurations = + RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCount]; for (int i = 0; i < rendererCount; i++) { boolean forceRendererDisabled = params.getRendererDisabled(i); @@ -1254,14 +1253,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { * disabled, unless RendererCapabilities#getTrackType()} is {@link C#TRACK_TYPE_NONE}. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected @NullableType TrackSelection[] selectAllTracks( + protected TrackSelection[] selectAllTracks( MappedTrackInfo mappedTrackInfo, int[][][] rendererFormatSupports, int[] rendererMixedMimeTypeAdaptationSupports, Parameters params) throws ExoPlaybackException { int rendererCount = mappedTrackInfo.getRendererCount(); - @NullableType TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; + TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; boolean seenVideoRendererWithMappedTracks = false; boolean selectedVideoTracks = false; @@ -1792,8 +1791,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static void maybeConfigureRenderersForTunneling( MappedTrackInfo mappedTrackInfo, int[][][] renderererFormatSupports, - @NullableType RendererConfiguration[] rendererConfigurations, - @NullableType TrackSelection[] trackSelections, + RendererConfiguration[] rendererConfigurations, + TrackSelection[] trackSelections, int tunnelingAudioSessionId) { if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) { return; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 2eb0440669..eb855ea0c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java index 48151002be..071293566d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.trackselection; import android.support.annotation.Nullable; import java.util.Arrays; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** An array of {@link TrackSelection}s. */ public final class TrackSelectionArray { @@ -25,13 +24,13 @@ public final class TrackSelectionArray { /** The length of this array. */ public final int length; - private final @NullableType TrackSelection[] trackSelections; + private final TrackSelection[] trackSelections; // Lazily initialized hashcode. private int hashCode; /** @param trackSelections The selections. Must not be null, but may contain null elements. */ - public TrackSelectionArray(@NullableType TrackSelection... trackSelections) { + public TrackSelectionArray(TrackSelection... trackSelections) { this.trackSelections = trackSelections; this.length = trackSelections.length; } @@ -47,7 +46,7 @@ public final class TrackSelectionArray { } /** Returns the selections in a newly allocated array. */ - public @NullableType TrackSelection[] getAll() { + public TrackSelection[] getAll() { return trackSelections.clone(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index f1136f0be5..941b90f8a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.trackselection; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.util.Util; -import org.checkerframework.checker.nullness.compatqual.NullableType; /** * The result of a {@link TrackSelector} operation. @@ -30,7 +29,7 @@ public final class TrackSelectorResult { * A {@link RendererConfiguration} for each renderer. A null entry indicates the corresponding * renderer should be disabled. */ - public final @NullableType RendererConfiguration[] rendererConfigurations; + public final RendererConfiguration[] rendererConfigurations; /** * A {@link TrackSelectionArray} containing the track selection for each renderer. */ @@ -49,8 +48,8 @@ public final class TrackSelectorResult { * TrackSelector#onSelectionActivated(Object)} should the selection be activated. */ public TrackSelectorResult( - @NullableType RendererConfiguration[] rendererConfigurations, - @NullableType TrackSelection[] selections, + RendererConfiguration[] rendererConfigurations, + TrackSelection[] selections, Object info) { this.rendererConfigurations = rendererConfigurations; this.selections = new TrackSelectionArray(selections); From 04565a3158e48ce73a80af7e15237a3543bb4cd2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 6 Jun 2018 05:22:55 -0700 Subject: [PATCH 029/106] Fix leak in the demo app ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199448766 --- .../exoplayer2/demo/PlayerActivity.java | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 091e483155..565f7e300a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -136,6 +136,7 @@ public class PlayerActivity extends Activity private DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; + private FrameworkMediaDrm mediaDrm; private MediaSource mediaSource; private DefaultTrackSelector trackSelector; private DefaultTrackSelector.Parameters trackSelectorParameters; @@ -487,8 +488,9 @@ public class PlayerActivity extends Activity keyRequestPropertiesArray[i + 1]); } } - return new DefaultDrmSessionManager<>( - uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, null, multiSession); + releaseMediaDrm(); + mediaDrm = FrameworkMediaDrm.newInstance(uuid); + return new DefaultDrmSessionManager<>(uuid, mediaDrm, drmCallback, null, multiSession); } private void releasePlayer() { @@ -502,6 +504,23 @@ public class PlayerActivity extends Activity mediaSource = null; trackSelector = null; } + releaseMediaDrm(); + } + + private void releaseMediaDrm() { + if (mediaDrm != null) { + mediaDrm.release(); + mediaDrm = null; + } + } + + private void releaseAdsLoader() { + if (adsLoader != null) { + adsLoader.release(); + adsLoader = null; + loadedAdTagUri = null; + playerView.getOverlayFrameLayout().removeAllViews(); + } } private void updateTrackSelectorParameters() { @@ -576,15 +595,6 @@ public class PlayerActivity extends Activity } } - private void releaseAdsLoader() { - if (adsLoader != null) { - adsLoader.release(); - adsLoader = null; - loadedAdTagUri = null; - playerView.getOverlayFrameLayout().removeAllViews(); - } - } - // User controls private void updateButtonVisibilities() { From 1481801891774038e88e8b67afda2a0757f11bf5 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Jun 2018 06:09:26 -0700 Subject: [PATCH 030/106] Bump version + update release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199453125 --- RELEASENOTES.md | 8 ++++++-- constants.gradle | 4 ++-- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 05a23a5077..28471f56cb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,10 +2,14 @@ ### 2.8.2 ### -* Fix inconsistent `Player.EventListener` invocations for recursive player state - changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)). * IMA: Don't advertise support for video/mpeg ad media, as we don't have an extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)). +* Mitigate memory leaks when `MediaSource` loads are slow to cancel + ([#4249](https://github.com/google/ExoPlayer/issues/4249)). +* Fix inconsistent `Player.EventListener` invocations for recursive player state + changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)). +* Fix `MediaCodec.native_setSurface` crash on Moto C + ([#4315](https://github.com/google/ExoPlayer/issues/4315)). ### 2.8.1 ### diff --git a/constants.gradle b/constants.gradle index 9b9e79c99e..5544173a3c 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.8.1' - releaseVersionCode = 2801 + releaseVersion = '2.8.2' + releaseVersionCode = 2802 // Important: ExoPlayer specifies a minSdkVersion of 14 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index aabb01481b..172eb19da3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.8.1"; + public static final String VERSION = "2.8.2"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.2"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2008001; + public static final int VERSION_INT = 2008002; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 879f6894885e941fd3b74e3abb1e38ac9821393f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 6 Jun 2018 09:42:23 -0700 Subject: [PATCH 031/106] Set METADATA_KEY_TITLE Issue: #4292 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199478946 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 28471f56cb..d8de5f9a90 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -10,6 +10,8 @@ changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)). * Fix `MediaCodec.native_setSurface` crash on Moto C ([#4315](https://github.com/google/ExoPlayer/issues/4315)). +* Set `METADATA_KEY_TITLE` on media descriptions + ((#4292)[https://github.com/google/ExoPlayer/issues/4292]). ### 2.8.1 ### diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 83fb16236d..4bafaa4326 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -600,8 +600,9 @@ public final class MediaSessionConnector { } } if (description.getTitle() != null) { - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, - String.valueOf(description.getTitle())); + String title = String.valueOf(description.getTitle()); + builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title); + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title); } if (description.getSubtitle() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, From ef8a47ba7db79c12fce7477dc40de50f62a9e6bb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 6 Jun 2018 08:55:07 -0700 Subject: [PATCH 032/106] Fix some categories of error prone warnings When switching from Stack to ArrayDeque, calls to add() need to be replaced by calls to push() because ArrayDeque treats the first element in the list as the top of the stack. String.split() has counterintuitive default behavior; see https://github.com/google/error-prone/blob/master/docs/bugpattern/StringSplitter.md. I've switched usages to pass limit = -1 argument, which means empty elements are no longer removed from the end of the returned array. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199472592 --- .../exoplayer2/ExoPlayerImplInternal.java | 1 + .../exoplayer2/decoder/SimpleDecoder.java | 10 ++--- .../exoplayer2/drm/HttpMediaDrmCallback.java | 3 +- .../extractor/mkv/DefaultEbmlReader.java | 16 ++++--- .../exoplayer2/extractor/mkv/Sniffer.java | 5 ++- .../mp4/FixedSampleSizeRechunker.java | 3 ++ .../extractor/mp4/FragmentedMp4Extractor.java | 7 ++- .../extractor/mp4/Mp4Extractor.java | 8 ++-- .../extractor/mp4/PsshAtomUtil.java | 1 + .../exoplayer2/extractor/ogg/OpusReader.java | 2 +- .../exoplayer2/extractor/ogg/VorbisUtil.java | 10 +++-- .../extractor/wav/WavHeaderReader.java | 6 ++- .../offline/ProgressiveDownloader.java | 2 +- .../exoplayer2/offline/SegmentDownloader.java | 2 + .../android/exoplayer2/source/TrackGroup.java | 5 ++- .../exoplayer2/text/cea/Cea708Decoder.java | 8 ++-- .../exoplayer2/text/cea/CeaDecoder.java | 10 ++--- .../exoplayer2/text/ssa/SsaDecoder.java | 2 +- .../exoplayer2/text/ttml/TtmlDecoder.java | 19 ++++---- .../exoplayer2/text/tx3g/Tx3gDecoder.java | 3 +- .../exoplayer2/text/webvtt/CssParser.java | 3 +- .../text/webvtt/Mp4WebvttDecoder.java | 3 +- .../text/webvtt/WebvttCueParser.java | 9 ++-- .../text/webvtt/WebvttParserUtil.java | 5 ++- .../trackselection/BaseTrackSelection.java | 3 ++ .../trackselection/TrackSelection.java | 4 +- .../upstream/DataSchemeDataSource.java | 5 ++- .../cache/ContentMetadataInternal.java | 6 ++- .../upstream/crypto/AesFlushingCipher.java | 5 ++- .../android/exoplayer2/util/ColorParser.java | 5 ++- .../android/exoplayer2/util/MimeTypes.java | 4 +- .../exoplayer2/util/ParsableBitArray.java | 20 +++++---- .../exoplayer2/util/ParsableByteArray.java | 6 +-- .../util/ParsableNalUnitBitArray.java | 2 +- .../google/android/exoplayer2/util/Util.java | 43 ++++++++++++++++++- 35 files changed, 169 insertions(+), 77 deletions(-) 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 fc946804f4..a7d569081e 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 @@ -1543,6 +1543,7 @@ import java.util.Collections; } } + @SuppressWarnings("ParameterNotNullable") private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder) throws ExoPlaybackException { MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index 68089d7b41..441d3899a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.decoder; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; -import java.util.LinkedList; +import java.util.ArrayDeque; /** * Base class for {@link Decoder}s that use their own decode thread. @@ -28,8 +28,8 @@ public abstract class SimpleDecoder queuedInputBuffers; - private final LinkedList queuedOutputBuffers; + private final ArrayDeque queuedInputBuffers; + private final ArrayDeque queuedOutputBuffers; private final I[] availableInputBuffers; private final O[] availableOutputBuffers; @@ -48,8 +48,8 @@ public abstract class SimpleDecoder(); - queuedOutputBuffers = new LinkedList<>(); + queuedInputBuffers = new ArrayDeque<>(); + queuedOutputBuffers = new ArrayDeque<>(); availableInputBuffers = inputBuffers; availableInputBufferCount = inputBuffers.length; for (int i = 0; i < availableInputBufferCount; i++) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index 4a93ac8333..9150a72b53 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -108,7 +108,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { - String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); + String url = + request.getDefaultUrl() + "&signedRequest=" + Util.fromUtf8Bytes(request.getData()); return executePost(dataSourceFactory, url, new byte[0], null); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java index 21cb3775e5..c0494e1ee0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java @@ -24,7 +24,7 @@ import java.io.EOFException; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Stack; +import java.util.ArrayDeque; /** * Default implementation of {@link EbmlReader}. @@ -46,15 +46,21 @@ import java.util.Stack; private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4; private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8; - private final byte[] scratch = new byte[8]; - private final Stack masterElementsStack = new Stack<>(); - private final VarintReader varintReader = new VarintReader(); + private final byte[] scratch; + private final ArrayDeque masterElementsStack; + private final VarintReader varintReader; private EbmlReaderOutput output; private @ElementState int elementState; private int elementId; private long elementContentSize; + public DefaultEbmlReader() { + scratch = new byte[8]; + masterElementsStack = new ArrayDeque<>(); + varintReader = new VarintReader(); + } + @Override public void init(EbmlReaderOutput eventHandler) { this.output = eventHandler; @@ -100,7 +106,7 @@ import java.util.Stack; case EbmlReaderOutput.TYPE_MASTER: long elementContentPosition = input.getPosition(); long elementEndPosition = elementContentPosition + elementContentSize; - masterElementsStack.add(new MasterElement(elementId, elementEndPosition)); + masterElementsStack.push(new MasterElement(elementId, elementEndPosition)); output.startMasterElement(elementId, elementContentPosition, elementContentSize); elementState = ELEMENT_STATE_READ_ID; return true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java index a3fde6d455..62c9404916 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/Sniffer.java @@ -78,8 +78,9 @@ import java.io.IOException; return false; } if (size != 0) { - input.advancePeekPosition((int) size); - peekLength += size; + int sizeInt = (int) size; + input.advancePeekPosition(sizeInt); + peekLength += sizeInt; } } return peekLength == headerStart + headerSize; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java index 8336a280a2..536f70048c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java @@ -108,4 +108,7 @@ import com.google.android.exoplayer2.util.Util; return new Results(offsets, sizes, maximumSize, timestamps, flags, duration); } + private FixedSampleSizeRechunker() { + // Prevent instantiation. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index d1134dc3f6..0bf42f1839 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -50,7 +50,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Stack; import java.util.UUID; /** @@ -141,7 +140,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Parser state. private final ParsableByteArray atomHeader; private final byte[] extendedTypeScratch; - private final Stack containerAtoms; + private final ArrayDeque containerAtoms; private final ArrayDeque pendingMetadataSampleInfos; private final @Nullable TrackOutput additionalEmsgTrackOutput; @@ -257,7 +256,7 @@ public final class FragmentedMp4Extractor implements Extractor { nalPrefix = new ParsableByteArray(5); nalBuffer = new ParsableByteArray(); extendedTypeScratch = new byte[16]; - containerAtoms = new Stack<>(); + containerAtoms = new ArrayDeque<>(); pendingMetadataSampleInfos = new ArrayDeque<>(); trackBundles = new SparseArray<>(); durationUs = C.TIME_UNSET; @@ -390,7 +389,7 @@ public final class FragmentedMp4Extractor implements Extractor { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE; - containerAtoms.add(new ContainerAtom(atomType, endPosition)); + containerAtoms.push(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { 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 75bd2c16ee..e70a49a2d7 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 @@ -37,9 +37,9 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; -import java.util.Stack; /** * Extracts data from the MP4 container format. @@ -101,7 +101,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { private final ParsableByteArray nalLength; private final ParsableByteArray atomHeader; - private final Stack containerAtoms; + private final ArrayDeque containerAtoms; @State private int parserState; private int atomType; @@ -137,7 +137,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { public Mp4Extractor(@Flags int flags) { this.flags = flags; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); - containerAtoms = new Stack<>(); + containerAtoms = new ArrayDeque<>(); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); sampleTrackIndex = C.INDEX_UNSET; @@ -303,7 +303,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { if (shouldParseContainerAtom(atomType)) { long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead; - containerAtoms.add(new ContainerAtom(atomType, endPosition)); + containerAtoms.push(new ContainerAtom(atomType, endPosition)); if (atomSize == atomHeaderBytesRead) { processAtomEnded(endPosition); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java index 84513ef4d3..a033f5c663 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java @@ -49,6 +49,7 @@ public final class PsshAtomUtil { * @param data The scheme specific data. * @return The PSSH atom. */ + @SuppressWarnings("ParameterNotNullable") public static byte[] buildPsshAtom( UUID systemId, @Nullable UUID[] keyIds, @Nullable byte[] data) { boolean buildV1Atom = keyIds != null; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java index 8ed8a4a01d..ce3b9ea6ba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/OpusReader.java @@ -130,6 +130,6 @@ import java.util.List; } else { length = 10000 << length; } - return frames * length; + return (long) frames * length; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java index 79767a00d8..0235fba272 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java @@ -357,12 +357,12 @@ import java.util.Arrays; for (int i = 0; i < lengthMap.length; i++) { if (isSparse) { if (bitArray.readBit()) { - lengthMap[i] = bitArray.readBits(5) + 1; + lengthMap[i] = (long) (bitArray.readBits(5) + 1); } else { // entry unused lengthMap[i] = 0; } } else { // not sparse - lengthMap[i] = bitArray.readBits(5) + 1; + lengthMap[i] = (long) (bitArray.readBits(5) + 1); } } } else { @@ -392,7 +392,7 @@ import java.util.Arrays; lookupValuesCount = 0; } } else { - lookupValuesCount = entries * dimensions; + lookupValuesCount = (long) entries * dimensions; } // discard (no decoding required yet) bitArray.skipBits((int) (lookupValuesCount * valueBits)); @@ -407,6 +407,10 @@ import java.util.Arrays; return (long) Math.floor(Math.pow(entries, 1.d / dimension)); } + private VorbisUtil() { + // Prevent instantiation. + } + public static final class CodeBook { public final int dimensions; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java index d0810a0629..ca745591f5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** Reads a {@code WavHeader} from an input stream; supports resuming from input failures. */ -/*package*/ final class WavHeaderReader { +/* package */ final class WavHeaderReader { private static final String TAG = "WavHeaderReader"; @@ -158,6 +158,10 @@ import java.io.IOException; wavHeader.setDataBounds(input.getPosition(), chunkHeader.size); } + private WavHeaderReader() { + // Prevent instantiation. + } + /** Container for a WAV chunk header. */ private static final class ChunkHeader { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index cf64d26bb5..8c80a23d67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.offline; +package com.google.android.exoplayer2.offline; import android.net.Uri; import com.google.android.exoplayer2.C; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 6ce2121acd..9be694264c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -201,6 +201,8 @@ public abstract class SegmentDownloader, K> throws InterruptedException, IOException; /** Initializes the download, returning a list of {@link Segment}s that need to be downloaded. */ + // Writes to downloadedSegments and downloadedBytes are safe. See the comment on download(). + @SuppressWarnings("NonAtomicVolatileUpdate") private List initDownload() throws IOException, InterruptedException { M manifest = getManifest(dataSource, manifestUri); if (!streamKeys.isEmpty()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java index a9fb261768..56c9989f34 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java @@ -72,11 +72,14 @@ public final class TrackGroup implements Parcelable { } /** - * Returns the index of the track with the given format in the group. + * Returns the index of the track with the given format in the group. The format is located by + * identity so, for example, {@code group.indexOf(group.getFormat(index)) == index} even if + * multiple tracks have formats that contain the same values. * * @param format The format. * @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists. */ + @SuppressWarnings("ReferenceEquality") public int indexOf(Format format) { for (int i = 0; i < formats.length; i++) { if (format == formats[i]) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 6bdbebc73b..731c9032d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -38,7 +38,6 @@ import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; /** @@ -196,7 +195,10 @@ public final class Cea708Decoder extends CeaDecoder { @Override protected void decode(SubtitleInputBuffer inputBuffer) { - ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit()); + // Subtitle input buffers are non-direct and the position is zero, so calling array() is safe. + @SuppressWarnings("ByteBufferBackingArray") + byte[] inputBufferData = inputBuffer.data.array(); + ccData.reset(inputBufferData, inputBuffer.data.limit()); while (ccData.bytesLeft() >= 3) { int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07); @@ -879,7 +881,7 @@ public final class Cea708Decoder extends CeaDecoder { private int row; public CueBuilder() { - rolledUpCaptions = new LinkedList<>(); + rolledUpCaptions = new ArrayList<>(); captionStringBuilder = new SpannableStringBuilder(); reset(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java index 07a55f1a40..3efc16bdd0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.text.SubtitleOutputBuffer; import com.google.android.exoplayer2.util.Assertions; -import java.util.LinkedList; +import java.util.ArrayDeque; import java.util.PriorityQueue; /** @@ -35,8 +35,8 @@ import java.util.PriorityQueue; private static final int NUM_INPUT_BUFFERS = 10; private static final int NUM_OUTPUT_BUFFERS = 2; - private final LinkedList availableInputBuffers; - private final LinkedList availableOutputBuffers; + private final ArrayDeque availableInputBuffers; + private final ArrayDeque availableOutputBuffers; private final PriorityQueue queuedInputBuffers; private CeaInputBuffer dequeuedInputBuffer; @@ -44,11 +44,11 @@ import java.util.PriorityQueue; private long queuedInputBufferCount; public CeaDecoder() { - availableInputBuffers = new LinkedList<>(); + availableInputBuffers = new ArrayDeque<>(); for (int i = 0; i < NUM_INPUT_BUFFERS; i++) { availableInputBuffers.add(new CeaInputBuffer()); } - availableOutputBuffers = new LinkedList<>(); + availableOutputBuffers = new ArrayDeque<>(); for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { availableOutputBuffers.add(new CeaOutputBuffer()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 0cb6f66898..e528a57762 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -62,7 +62,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { super("SsaDecoder"); if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; - String formatLine = new String(initializationData.get(0)); + String formatLine = Util.fromUtf8Bytes(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); parseFormatLine(formatLine); parseHeader(new ParsableByteArray(initializationData.get(1))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java index ad8f849c60..61e0085065 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java @@ -26,8 +26,8 @@ import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.XmlPullParserUtil; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.ArrayDeque; import java.util.HashMap; -import java.util.LinkedList; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -109,13 +109,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length); xmlParser.setInput(inputStream, null); TtmlSubtitle ttmlSubtitle = null; - LinkedList nodeStack = new LinkedList<>(); + ArrayDeque nodeStack = new ArrayDeque<>(); int unsupportedNodeDepth = 0; int eventType = xmlParser.getEventType(); FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE; CellResolution cellResolution = DEFAULT_CELL_RESOLUTION; while (eventType != XmlPullParser.END_DOCUMENT) { - TtmlNode parent = nodeStack.peekLast(); + TtmlNode parent = nodeStack.peek(); if (unsupportedNodeDepth == 0) { String name = xmlParser.getName(); if (eventType == XmlPullParser.START_TAG) { @@ -131,7 +131,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } else { try { TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate); - nodeStack.addLast(node); + nodeStack.push(node); if (parent != null) { parent.addChild(node); } @@ -145,9 +145,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { parent.addChild(TtmlNode.buildTextNode(xmlParser.getText())); } else if (eventType == XmlPullParser.END_TAG) { if (xmlParser.getName().equals(TtmlNode.TAG_TT)) { - ttmlSubtitle = new TtmlSubtitle(nodeStack.getLast(), globalStyles, regionMap); + ttmlSubtitle = new TtmlSubtitle(nodeStack.peek(), globalStyles, regionMap); } - nodeStack.removeLast(); + nodeStack.pop(); } } else { if (eventType == XmlPullParser.START_TAG) { @@ -178,7 +178,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { float frameRateMultiplier = 1; String frameRateMultiplierString = xmlParser.getAttributeValue(TTP, "frameRateMultiplier"); if (frameRateMultiplierString != null) { - String[] parts = frameRateMultiplierString.split(" "); + String[] parts = Util.split(frameRateMultiplierString, " "); if (parts.length != 2) { throw new SubtitleDecoderException("frameRateMultiplier doesn't have 2 parts"); } @@ -354,7 +354,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } private String[] parseStyleIds(String parentStyleIds) { - return parentStyleIds.split("\\s+"); + parentStyleIds = parentStyleIds.trim(); + return parentStyleIds.isEmpty() ? new String[0] : Util.split(parentStyleIds, "\\s+"); } private TtmlStyle parseStyleAttributes(XmlPullParser parser, TtmlStyle style) { @@ -531,7 +532,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { private static void parseFontSize(String expression, TtmlStyle out) throws SubtitleDecoderException { - String[] expressions = expression.split("\\s+"); + String[] expressions = Util.split(expression, "\\s+"); Matcher matcher; if (expressions.length == 1) { matcher = FONT_SIZE.matcher(expression); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java index 2270ccc632..ebc38bcd70 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java @@ -92,7 +92,8 @@ public final class Tx3gDecoder extends SimpleSubtitleDecoder { | ((initializationBytes[27] & 0xFF) << 16) | ((initializationBytes[28] & 0xFF) << 8) | (initializationBytes[29] & 0xFF); - String fontFamily = new String(initializationBytes, 43, initializationBytes.length - 43); + String fontFamily = + Util.fromUtf8Bytes(initializationBytes, 43, initializationBytes.length - 43); defaultFontFamily = TX3G_SERIF.equals(fontFamily) ? C.SERIF_NAME : C.SANS_SERIF_NAME; //font size (initializationBytes[25]) is 5% of video height calculatedVideoTrackHeight = 20 * initializationBytes[25]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java index ea1e6891f0..81c362bda5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/CssParser.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.text.webvtt; import android.text.TextUtils; import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -314,7 +315,7 @@ import java.util.regex.Pattern; } selector = selector.substring(0, voiceStartIndex); } - String[] classDivision = selector.split("\\."); + String[] classDivision = Util.split(selector, "\\."); String tagAndIdDivision = classDivision[0]; int idPrefixIndex = tagAndIdDivision.indexOf('#'); if (idPrefixIndex != -1) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java index 159dd4f2e0..17c2366f07 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java @@ -78,7 +78,8 @@ public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder { int boxType = sampleData.readInt(); remainingCueBoxBytes -= BOX_HEADER_SIZE; int payloadLength = boxSize - BOX_HEADER_SIZE; - String boxPayload = new String(sampleData.data, sampleData.getPosition(), payloadLength); + String boxPayload = + Util.fromUtf8Bytes(sampleData.data, sampleData.getPosition(), payloadLength); sampleData.skipBytes(payloadLength); remainingCueBoxBytes -= payloadLength; if (boxType == TYPE_sttg) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java index 80ebecdc0e..6f2a1328c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java @@ -34,11 +34,12 @@ import android.text.style.UnderlineSpan; import android.util.Log; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -157,7 +158,7 @@ public final class WebvttCueParser { /* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder, List styles) { SpannableStringBuilder spannedText = new SpannableStringBuilder(); - Stack startTagStack = new Stack<>(); + ArrayDeque startTagStack = new ArrayDeque<>(); List scratchStyleMatches = new ArrayList<>(); int pos = 0; while (pos < markup.length()) { @@ -456,7 +457,7 @@ public final class WebvttCueParser { if (tagExpression.isEmpty()) { return null; } - return tagExpression.split("[ \\.]")[0]; + return Util.splitAtFirst(tagExpression, "[ \\.]")[0]; } private static void getApplicableStyles(List declaredStyles, String id, @@ -518,7 +519,7 @@ public final class WebvttCueParser { voice = fullTagExpression.substring(voiceStartIndex).trim(); fullTagExpression = fullTagExpression.substring(0, voiceStartIndex); } - String[] nameAndClasses = fullTagExpression.split("\\."); + String[] nameAndClasses = Util.split(fullTagExpression, "\\."); String name = nameAndClasses[0]; String[] classes; if (nameAndClasses.length > 1) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java index d0c3eda494..b94be19d8f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.text.webvtt; import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -53,8 +54,8 @@ public final class WebvttParserUtil { */ public static long parseTimestampUs(String timestamp) throws NumberFormatException { long value = 0; - String[] parts = timestamp.split("\\.", 2); - String[] subparts = parts[0].split(":"); + String[] parts = Util.splitAtFirst(timestamp, "\\."); + String[] subparts = Util.split(parts[0], ":"); for (String subpart : subparts) { value = (value * 60) + Long.parseLong(subpart); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 81eb5dd888..3f201bccea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -110,6 +110,7 @@ public abstract class BaseTrackSelection implements TrackSelection { } @Override + @SuppressWarnings("ReferenceEquality") public final int indexOf(Format format) { for (int i = 0; i < length; i++) { if (formats[i] == format) { @@ -183,7 +184,9 @@ public abstract class BaseTrackSelection implements TrackSelection { return hashCode; } + // Track groups are compared by identity not value, as distinct groups may have the same value. @Override + @SuppressWarnings("ReferenceEquality") public boolean equals(@Nullable Object obj) { if (this == obj) { return true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index 58616996ff..ee0a397a8d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -91,7 +91,9 @@ public interface TrackSelection { int getIndexInTrackGroup(int index); /** - * Returns the index in the selection of the track with the specified format. + * Returns the index in the selection of the track with the specified format. The format is + * located by identity so, for example, {@code selection.indexOf(selection.getFormat(index)) == + * index} even if multiple selected tracks have formats that contain the same values. * * @param format The format. * @return The index in the selection, or {@link C#INDEX_UNSET} if the track with the specified diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java index c547625819..33d67f3f46 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.net.URLDecoder; @@ -41,8 +42,8 @@ public final class DataSchemeDataSource implements DataSource { if (!SCHEME_DATA.equals(scheme)) { throw new ParserException("Unsupported scheme: " + scheme); } - String[] uriParts = uri.getSchemeSpecificPart().split(","); - if (uriParts.length > 2) { + String[] uriParts = Util.split(uri.getSchemeSpecificPart(), ","); + if (uriParts.length != 2) { throw new ParserException("Unexpected URI format: " + uri); } String dataString = uriParts[1]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java index 3376dd6944..0065018260 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/ContentMetadataInternal.java @@ -20,7 +20,7 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; /** Helper classes to easily access and modify internal metadata values. */ -/*package*/ final class ContentMetadataInternal { +/* package */ final class ContentMetadataInternal { private static final String PREFIX = ContentMetadata.INTERNAL_METADATA_NAME_PREFIX; private static final String METADATA_NAME_REDIRECTED_URI = PREFIX + "redir"; @@ -59,4 +59,8 @@ import com.google.android.exoplayer2.C; public static void removeRedirectedUri(ContentMetadataMutations mutations) { mutations.remove(METADATA_NAME_REDIRECTED_URI); } + + private ContentMetadataInternal() { + // Prevent instantiation. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java index e093eb3064..1721b1d8b7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.crypto; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -49,7 +50,9 @@ public final class AesFlushingCipher { flushedBlock = new byte[blockSize]; long counter = offset / blockSize; int startPadding = (int) (offset % blockSize); - cipher.init(mode, new SecretKeySpec(secretKey, cipher.getAlgorithm().split("/")[0]), + cipher.init( + mode, + new SecretKeySpec(secretKey, Util.splitAtFirst(cipher.getAlgorithm(), "/")[0]), new IvParameterSpec(getInitializationVector(nonce, counter))); if (startPadding != 0) { updateInPlace(new byte[startPadding], 0, startPadding); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java index a9df80e9fe..54f52e0a14 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ColorParser.java @@ -26,7 +26,7 @@ import java.util.regex.Pattern; * * @see WebVTT CSS Styling * @see Timed Text Markup Language 2 (TTML2) - 10.3.5 - **/ + */ public final class ColorParser { private static final String RGB = "rgb"; @@ -271,4 +271,7 @@ public final class ColorParser { COLOR_MAP.put("yellowgreen", 0xFF9ACD32); } + private ColorParser() { + // Prevent instantiation. + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index d13aa877e0..9e9ff5fd77 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -144,7 +144,7 @@ public final class MimeTypes { if (codecs == null) { return null; } - String[] codecList = codecs.split(","); + String[] codecList = Util.split(codecs, ","); for (String codec : codecList) { String mimeType = getMediaMimeType(codec); if (mimeType != null && isVideo(mimeType)) { @@ -164,7 +164,7 @@ public final class MimeTypes { if (codecs == null) { return null; } - String[] codecList = codecs.split(","); + String[] codecList = Util.split(codecs, ","); for (String codec : codecList) { String mimeType = getMediaMimeType(codec); if (mimeType != null && isAudio(mimeType)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index fb5f9525e9..c60caf9ba8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -175,7 +175,7 @@ public final class ParsableBitArray { bitOffset -= 8; returnValue |= (data[byteOffset++] & 0xFF) << bitOffset; } - returnValue |= (data[byteOffset] & 0xFF) >> 8 - bitOffset; + returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset); returnValue &= 0xFFFFFFFF >>> (32 - numBits); if (bitOffset == 8) { bitOffset = 0; @@ -199,17 +199,18 @@ public final class ParsableBitArray { int to = offset + (numBits >> 3) /* numBits / 8 */; for (int i = offset; i < to; i++) { buffer[i] = (byte) (data[byteOffset++] << bitOffset); - buffer[i] |= (data[byteOffset] & 0xFF) >> (8 - bitOffset); + buffer[i] = (byte) (buffer[i] | ((data[byteOffset] & 0xFF) >> (8 - bitOffset))); } // Trailing bits. int bitsLeft = numBits & 7 /* numBits % 8 */; if (bitsLeft == 0) { return; } - buffer[to] &= 0xFF >> bitsLeft; // Set to 0 the bits that are going to be overwritten. + // Set bits that are going to be overwritten to 0. + buffer[to] = (byte) (buffer[to] & (0xFF >> bitsLeft)); if (bitOffset + bitsLeft > 8) { // We read the rest of data[byteOffset] and increase byteOffset. - buffer[to] |= (byte) ((data[byteOffset++] & 0xFF) << bitOffset); + buffer[to] = (byte) (buffer[to] | ((data[byteOffset++] & 0xFF) << bitOffset)); bitOffset -= 8; } bitOffset += bitsLeft; @@ -280,9 +281,10 @@ public final class ParsableBitArray { int firstByteReadSize = Math.min(8 - bitOffset, numBits); int firstByteRightPaddingSize = 8 - bitOffset - firstByteReadSize; int firstByteBitmask = (0xFF00 >> bitOffset) | ((1 << firstByteRightPaddingSize) - 1); - data[byteOffset] &= firstByteBitmask; + data[byteOffset] = (byte) (data[byteOffset] & firstByteBitmask); int firstByteInputBits = value >>> (numBits - firstByteReadSize); - data[byteOffset] |= firstByteInputBits << firstByteRightPaddingSize; + data[byteOffset] = + (byte) (data[byteOffset] | (firstByteInputBits << firstByteRightPaddingSize)); remainingBitsToRead -= firstByteReadSize; int currentByteIndex = byteOffset + 1; while (remainingBitsToRead > 8) { @@ -290,9 +292,11 @@ public final class ParsableBitArray { remainingBitsToRead -= 8; } int lastByteRightPaddingSize = 8 - remainingBitsToRead; - data[currentByteIndex] &= (1 << lastByteRightPaddingSize) - 1; + data[currentByteIndex] = + (byte) (data[currentByteIndex] & ((1 << lastByteRightPaddingSize) - 1)); int lastByteInput = value & ((1 << remainingBitsToRead) - 1); - data[currentByteIndex] |= lastByteInput << lastByteRightPaddingSize; + data[currentByteIndex] = + (byte) (data[currentByteIndex] | (lastByteInput << lastByteRightPaddingSize)); skipBits(numBits); assertValidOffset(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 57313ea895..5190896d9f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -470,7 +470,7 @@ public final class ParsableByteArray { if (lastIndex < limit && data[lastIndex] == 0) { stringLength--; } - String result = new String(data, position, stringLength); + String result = Util.fromUtf8Bytes(data, position, stringLength); position += length; return result; } @@ -489,7 +489,7 @@ public final class ParsableByteArray { while (stringLimit < limit && data[stringLimit] != 0) { stringLimit++; } - String string = new String(data, position, stringLimit - position); + String string = Util.fromUtf8Bytes(data, position, stringLimit - position); position = stringLimit; if (position < limit) { position++; @@ -520,7 +520,7 @@ public final class ParsableByteArray { // There's a byte order mark at the start of the line. Discard it. position += 3; } - String line = new String(data, position, lineLimit - position); + String line = Util.fromUtf8Bytes(data, position, lineLimit - position); position = lineLimit; if (position == limit) { return line; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java index 443c69909c..3a7202c674 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java @@ -140,7 +140,7 @@ public final class ParsableNalUnitBitArray { returnValue |= (data[byteOffset] & 0xFF) << bitOffset; byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1; } - returnValue |= (data[byteOffset] & 0xFF) >> 8 - bitOffset; + returnValue |= (data[byteOffset] & 0xFF) >> (8 - bitOffset); returnValue &= 0xFFFFFFFF >>> (32 - numBits); if (bitOffset == 8) { bitOffset = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 37e3e119bf..90c5d17b6d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -332,6 +332,18 @@ public final class Util { return new String(bytes, Charset.forName(C.UTF8_NAME)); } + /** + * Returns a new {@link String} constructed by decoding UTF-8 encoded bytes in a subarray. + * + * @param bytes The UTF-8 encoded bytes to decode. + * @param offset The index of the first byte to decode. + * @param length The number of bytes to decode. + * @return The string. + */ + public static String fromUtf8Bytes(byte[] bytes, int offset, int length) { + return new String(bytes, offset, length, Charset.forName(C.UTF8_NAME)); + } + /** * Returns a new byte array containing the code points of a {@link String} encoded using UTF-8. * @@ -342,6 +354,33 @@ public final class Util { return value.getBytes(Charset.forName(C.UTF8_NAME)); } + /** + * Splits a string using {@code value.split(regex, -1}). Note: this is is similar to {@link + * String#split(String)} but empty matches at the end of the string will not be omitted from the + * returned array. + * + * @param value The string to split. + * @param regex A delimiting regular expression. + * @return The array of strings resulting from splitting the string. + */ + public static String[] split(String value, String regex) { + return value.split(regex, /* limit= */ -1); + } + + /** + * Splits the string at the first occurrence of the delimiter {@code regex}. If the delimiter does + * not match, returns an array with one element which is the input string. If the delimiter does + * match, returns an array with the portion of the string before the delimiter and the rest of the + * string. + * + * @param value The string. + * @param regex A delimiting regular expression. + * @return The string split by the first occurrence of the delimiter. + */ + public static String[] splitAtFirst(String value, String regex) { + return value.split(regex, /* limit= */ 2); + } + /** * Returns whether the given character is a carriage return ('\r') or a line feed ('\n'). * @@ -978,7 +1017,7 @@ public final class Util { if (TextUtils.isEmpty(codecs)) { return null; } - String[] codecArray = codecs.trim().split("(\\s*,\\s*)"); + String[] codecArray = split(codecs.trim(), "(\\s*,\\s*)"); StringBuilder builder = new StringBuilder(); for (String codec : codecArray) { if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) { @@ -1454,7 +1493,7 @@ public final class Util { // If we managed to read sys.display-size, attempt to parse it. if (!TextUtils.isEmpty(sysDisplaySize)) { try { - String[] sysDisplaySizeParts = sysDisplaySize.trim().split("x"); + String[] sysDisplaySizeParts = split(sysDisplaySize.trim(), "x"); if (sysDisplaySizeParts.length == 2) { int width = Integer.parseInt(sysDisplaySizeParts[0]); int height = Integer.parseInt(sysDisplaySizeParts[1]); From 3f2d20140f0006748b9b044746ff772e1e1d5fef Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Jun 2018 09:56:07 -0700 Subject: [PATCH 033/106] Fix some javadoc errors ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199650794 --- .../source/DynamicConcatenatingMediaSource.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 37313fd1ab..1f77cae20d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -19,19 +19,25 @@ package com.google.android.exoplayer2.source; @Deprecated public final class DynamicConcatenatingMediaSource extends ConcatenatingMediaSource { - /** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource()} instead. */ + /** + * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(MediaSource...)} + * instead. + */ @Deprecated public DynamicConcatenatingMediaSource() {} - /** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean)} instead. */ + /** + * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, + * MediaSource...)} instead. + */ @Deprecated public DynamicConcatenatingMediaSource(boolean isAtomic) { super(isAtomic); } /** - * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, - * ShuffleOrder)} instead. + * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, ShuffleOrder, + * MediaSource...)} instead. */ @Deprecated public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) { From cae427ad7e8dcb5b942794a826592320c8f24d8c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 7 Jun 2018 13:08:24 -0700 Subject: [PATCH 034/106] Fix incorrect padding application ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199683216 --- .../java/com/google/android/exoplayer2/ui/SubtitleView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 4dbd4d5fec..bb9c38d886 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -251,7 +251,7 @@ public final class SubtitleView extends View implements TextOutput { // Calculate the bounds after padding is taken into account. int left = getLeft() + getPaddingLeft(); int top = rawTop + getPaddingTop(); - int right = getRight() + getPaddingRight(); + int right = getRight() - getPaddingRight(); int bottom = rawBottom - getPaddingBottom(); if (bottom <= top || right <= left) { // No space to draw subtitles. From 7bf98b9fc2b6b1d5fac46aec8c8b961487e02ec9 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Jun 2018 00:38:16 -0700 Subject: [PATCH 035/106] Update forced dependencies to fix release ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199758072 --- extensions/cast/build.gradle | 20 +++++++++----------- extensions/ima/build.gradle | 15 +++++++-------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 8374910879..fa348aaf17 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -26,17 +26,6 @@ android { } dependencies { - // These dependencies are necessary to force the supportLibraryVersion of - // com.android.support:support-v4, com.android.support:appcompat-v7 and - // com.android.support:mediarouter-v7 to be used. Else older versions are - // used, for example: - // com.google.android.gms:play-services-cast-framework:15.0.1 - // |-- com.google.android.gms:play-services-base:15.0.1 - // |-- com.google.android.gms:play-services-basement:15.0.1 - // |-- com.android.support:support-v4:26.1.0 - api 'com.android.support:support-v4:' + supportLibraryVersion - api 'com.android.support:appcompat-v7:' + supportLibraryVersion - api 'com.android.support:mediarouter-v7:' + supportLibraryVersion api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') @@ -45,6 +34,15 @@ dependencies { testImplementation 'org.mockito:mockito-core:' + mockitoVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation project(modulePrefix + 'testutils-robolectric') + // These dependencies are necessary to force the supportLibraryVersion of + // com.android.support:support-v4, com.android.support:appcompat-v7 and + // com.android.support:mediarouter-v7 to be used. Else older versions are + // used, for example via: + // com.google.android.gms:play-services-cast-framework:15.0.1 + // |-- com.android.support:mediarouter-v7:26.1.0 + api 'com.android.support:support-v4:' + supportLibraryVersion + api 'com.android.support:mediarouter-v7:' + supportLibraryVersion + api 'com.android.support:recyclerview-v7:' + supportLibraryVersion } ext { diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 4403095658..cec6b8a495 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -26,17 +26,16 @@ android { } dependencies { - // This dependency is necessary to force the supportLibraryVersion of - // com.android.support:support-v4 to be used. Else an older version is - // included via: - // com.google.android.gms:play-services-ads:15.0.1 - // |-- com.google.android.gms:play-services-ads-identifier:15.0.1 - // |-- com.google.android.gms:play-services-basement:15.0.1 - // |-- com.android.support:support-v4:26.1.0 - api 'com.android.support:support-v4:' + supportLibraryVersion api 'com.google.ads.interactivemedia.v3:interactivemedia:3.8.7' implementation project(modulePrefix + 'library-core') implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion + // These dependencies are necessary to force the supportLibraryVersion of + // com.android.support:support-v4 and com.android.support:customtabs to be + // used. Else older versions are used, for example via: + // com.google.android.gms:play-services-ads:15.0.1 + // |-- com.android.support:customtabs:26.1.0 + implementation 'com.android.support:support-v4:' + supportLibraryVersion + implementation 'com.android.support:customtabs:' + supportLibraryVersion } ext { From 799d281e58c521bd8dafaa2a7f4c65ecabe41cc3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Jun 2018 04:45:57 -0700 Subject: [PATCH 036/106] Add support for registering custom MIME types Also add a few missing MP4 object types. Issue: #4264 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199778373 --- RELEASENOTES.md | 2 + .../android/exoplayer2/util/MimeTypes.java | 83 +++++++++++++++++-- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d8de5f9a90..990ee2c7bf 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,6 +12,8 @@ ([#4315](https://github.com/google/ExoPlayer/issues/4315)). * Set `METADATA_KEY_TITLE` on media descriptions ((#4292)[https://github.com/google/ExoPlayer/issues/4292]). +* Allow apps to register custom MIME types + ([#4264](https://github.com/google/ExoPlayer/issues/4264)). ### 2.8.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 9e9ff5fd77..d0c08f9bd7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util; import android.support.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; +import java.util.ArrayList; /** * Defines common MIME types and helper methods. @@ -92,7 +93,29 @@ public final class MimeTypes { public static final String APPLICATION_DVBSUBS = BASE_TYPE_APPLICATION + "/dvbsubs"; public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; - private MimeTypes() {} + private static final ArrayList customMimeTypes = new ArrayList<>(); + + /** + * Registers a custom MIME type. Most applications do not need to call this method, as handling of + * standard MIME types is built in. These built-in MIME types take precedence over any registered + * via this method. If this method is used, it must be called before creating any player(s). + * + * @param mimeType The custom MIME type to register. + * @param codecPrefix The RFC 6381-style codec string prefix associated with the MIME type. + * @param trackType The {@link C}{@code .TRACK_TYPE_*} constant associated with the MIME type. + * This value is ignored if the top-level type of {@code mimeType} is audio, video or text. + */ + public static void registerCustomMimeType(String mimeType, String codecPrefix, int trackType) { + CustomMimeType customMimeType = new CustomMimeType(mimeType, codecPrefix, trackType); + int customMimeTypeCount = customMimeTypes.size(); + for (int i = 0; i < customMimeTypeCount; i++) { + if (mimeType.equals(customMimeTypes.get(i).mimeType)) { + customMimeTypes.remove(i); + break; + } + } + customMimeTypes.add(customMimeType); + } /** * Whether the top-level type of {@code mimeType} is audio. @@ -222,8 +245,9 @@ public final class MimeTypes { return MimeTypes.AUDIO_OPUS; } else if (codec.startsWith("vorbis")) { return MimeTypes.AUDIO_VORBIS; + } else { + return getCustomMimeTypeForCodec(codec); } - return null; } /** @@ -236,18 +260,28 @@ public final class MimeTypes { @Nullable public static String getMimeTypeFromMp4ObjectType(int objectType) { switch (objectType) { - case 0x60: - case 0x61: - return MimeTypes.VIDEO_MPEG2; case 0x20: return MimeTypes.VIDEO_MP4V; case 0x21: return MimeTypes.VIDEO_H264; case 0x23: return MimeTypes.VIDEO_H265; + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + return MimeTypes.VIDEO_MPEG2; + case 0x6A: + return MimeTypes.VIDEO_MPEG; case 0x69: case 0x6B: return MimeTypes.AUDIO_MPEG; + case 0xA3: + return MimeTypes.VIDEO_VC1; + case 0xB1: + return MimeTypes.VIDEO_VP9; case 0x40: case 0x66: case 0x67: @@ -298,7 +332,7 @@ public final class MimeTypes { || APPLICATION_CAMERA_MOTION.equals(mimeType)) { return C.TRACK_TYPE_METADATA; } else { - return C.TRACK_TYPE_UNKNOWN; + return getTrackTypeForCustomMimeType(mimeType); } } @@ -355,4 +389,41 @@ public final class MimeTypes { return mimeType.substring(0, indexOfSlash); } + private static @Nullable String getCustomMimeTypeForCodec(String codec) { + int customMimeTypeCount = customMimeTypes.size(); + for (int i = 0; i < customMimeTypeCount; i++) { + CustomMimeType customMimeType = customMimeTypes.get(i); + if (codec.startsWith(customMimeType.codecPrefix)) { + return customMimeType.mimeType; + } + } + return null; + } + + private static int getTrackTypeForCustomMimeType(String mimeType) { + int customMimeTypeCount = customMimeTypes.size(); + for (int i = 0; i < customMimeTypeCount; i++) { + CustomMimeType customMimeType = customMimeTypes.get(i); + if (mimeType.equals(customMimeType.mimeType)) { + return customMimeType.trackType; + } + } + return C.TRACK_TYPE_UNKNOWN; + } + + private MimeTypes() { + // Prevent instantiation. + } + + private static final class CustomMimeType { + public final String mimeType; + public final String codecPrefix; + public final int trackType; + + public CustomMimeType(String mimeType, String codecPrefix, int trackType) { + this.mimeType = mimeType; + this.codecPrefix = codecPrefix; + this.trackType = trackType; + } + } } From 9ecf959613b6d1d9f76dd37fdeaf495f77fdf691 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Jun 2018 15:03:39 -0700 Subject: [PATCH 037/106] Offset SIDX timestamps by presentationTimeOffset ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199856613 --- RELEASENOTES.md | 2 ++ .../exoplayer2/source/dash/DashWrappingSegmentIndex.java | 9 ++++++--- .../exoplayer2/source/dash/DefaultDashChunkSource.java | 5 ++++- .../exoplayer2/source/dash/offline/DashDownloader.java | 4 +++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 990ee2c7bf..44ba911eac 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,8 @@ * IMA: Don't advertise support for video/mpeg ad media, as we don't have an extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)). +* DASH: Fix playback getting stuck when playing representations that have both + sidx atoms and non-zero presentationTimeOffset values. * Mitigate memory leaks when `MediaSource` loads are slow to cancel ([#4249](https://github.com/google/ExoPlayer/issues/4249)). * Fix inconsistent `Player.EventListener` invocations for recursive player state diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java index 078305a687..3eca7892c4 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java @@ -25,12 +25,15 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri; public final class DashWrappingSegmentIndex implements DashSegmentIndex { private final ChunkIndex chunkIndex; + private final long timeOffsetUs; /** * @param chunkIndex The {@link ChunkIndex} to wrap. + * @param timeOffsetUs An offset to subtract from the times in the wrapped index, in microseconds. */ - public DashWrappingSegmentIndex(ChunkIndex chunkIndex) { + public DashWrappingSegmentIndex(ChunkIndex chunkIndex, long timeOffsetUs) { this.chunkIndex = chunkIndex; + this.timeOffsetUs = timeOffsetUs; } @Override @@ -45,7 +48,7 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex { @Override public long getTimeUs(long segmentNum) { - return chunkIndex.timesUs[(int) segmentNum]; + return chunkIndex.timesUs[(int) segmentNum] - timeOffsetUs; } @Override @@ -61,7 +64,7 @@ public final class DashWrappingSegmentIndex implements DashSegmentIndex { @Override public long getSegmentNum(long timeUs, long periodDurationUs) { - return chunkIndex.getChunkIndex(timeUs); + return chunkIndex.getChunkIndex(timeUs + timeOffsetUs); } @Override 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 4cb14d6614..c00410f57e 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 @@ -354,7 +354,10 @@ public class DefaultDashChunkSource implements DashChunkSource { if (representationHolder.segmentIndex == null) { SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap(); if (seekMap != null) { - representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap); + representationHolder.segmentIndex = + new DashWrappingSegmentIndex( + (ChunkIndex) seekMap, + representationHolder.representation.presentationTimeOffsetUs); } } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 6922e56b84..8762244558 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -167,7 +167,9 @@ public final class DashDownloader extends SegmentDownloader Date: Thu, 14 Jun 2018 02:23:28 -0700 Subject: [PATCH 038/106] CEA608 - Add space when handling mid-row codes Issue: #3906 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=200526335 --- RELEASENOTES.md | 4 +++- .../com/google/android/exoplayer2/text/cea/Cea608Decoder.java | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 44ba911eac..44aae0ab9e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,8 +12,10 @@ changes ([#4276](https://github.com/google/ExoPlayer/issues/4276)). * Fix `MediaCodec.native_setSurface` crash on Moto C ([#4315](https://github.com/google/ExoPlayer/issues/4315)). +* Fix missing whitespace in CEA-608 + ([#3906](https://github.com/google/ExoPlayer/issues/3906)). * Set `METADATA_KEY_TITLE` on media descriptions - ((#4292)[https://github.com/google/ExoPlayer/issues/4292]). + ([#4292](https://github.com/google/ExoPlayer/issues/4292)). * Allow apps to register custom MIME types ([#4264](https://github.com/google/ExoPlayer/issues/4264)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index f018e055fb..57614ae880 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -374,6 +374,9 @@ public final class Cea608Decoder extends CeaDecoder { private void handleMidrowCtrl(byte cc2) { // TODO: support the extended styles (i.e. backgrounds and transparencies) + // A midrow control code advances the cursor. + currentCueBuilder.append(' '); + // cc2 - 0|0|1|0|ATRBT|U // ATRBT is the 3-byte encoded attribute, and U is the underline toggle boolean isUnderlined = (cc2 & 0x01) == 0x01; From 1776e6e5b0f7660c26b38b77b8fcadbb9e099f1c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 18 Jun 2018 01:36:06 -0700 Subject: [PATCH 039/106] Make no context current when releasing DummySurface This avoids a small native leak. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=200955086 --- .../com/google/android/exoplayer2/util/EGLSurfaceTexture.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java index 6fe76b9b2c..19794365dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java @@ -111,6 +111,10 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL GLES20.glDeleteTextures(1, textureIdHolder, 0); } } finally { + if (display != null && !display.equals(EGL14.EGL_NO_DISPLAY)) { + EGL14.eglMakeCurrent( + display, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); + } if (surface != null && !surface.equals(EGL14.EGL_NO_SURFACE)) { EGL14.eglDestroySurface(display, surface); } From d82f91845e862d11f8941a404b452d31857302ea Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 20 Jun 2018 02:53:26 -0700 Subject: [PATCH 040/106] Fix HlsMediaPlaylist download using HlsDownloadHelper Issue: #4396 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201324467 --- .../android/exoplayer2/source/hls/offline/HlsDownloadHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java index 37aa181970..c043e57588 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -73,6 +73,7 @@ public final class HlsDownloadHelper extends DownloadHelper { public TrackGroupArray getTrackGroups(int periodIndex) { Assertions.checkNotNull(playlist); if (playlist instanceof HlsMediaPlaylist) { + renditionGroups = new int[0]; return TrackGroupArray.EMPTY; } // TODO: Generate track groups as in playback. Reverse the mapping in getDownloadAction. From c14d59041258a39320de6af959959a77a2fc5882 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 20 Jun 2018 10:28:05 -0700 Subject: [PATCH 041/106] Fix release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201374393 --- RELEASENOTES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 44aae0ab9e..d7da1276ae 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,8 @@ ([#4315](https://github.com/google/ExoPlayer/issues/4315)). * Fix missing whitespace in CEA-608 ([#3906](https://github.com/google/ExoPlayer/issues/3906)). +* Fix crash downloading HLS media playlists + ([#4396](https://github.com/google/ExoPlayer/issues/4396)). * Set `METADATA_KEY_TITLE` on media descriptions ([#4292](https://github.com/google/ExoPlayer/issues/4292)). * Allow apps to register custom MIME types @@ -78,7 +80,7 @@ periods are created, released and being read from. * Support live stream clipping with `ClippingMediaSource`. * Allow setting tags for all media sources in their factories. The tag of the - current window can be retrieved with `ExoPlayer.getCurrentTag`. + current window can be retrieved with `Player.getCurrentTag`. * UI components: * Add support for displaying error messages and a buffering spinner in `PlayerView`. From b7d149604e48bb2620f3dd73215e06f3169b273c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 20 Jun 2018 10:38:00 -0700 Subject: [PATCH 042/106] Fully clean up resources in EGLSurfaceTexture ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201376315 --- .../com/google/android/exoplayer2/util/EGLSurfaceTexture.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java index 19794365dd..4bed17fc55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EGLSurfaceTexture.java @@ -121,6 +121,10 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL if (context != null) { EGL14.eglDestroyContext(display, context); } + // EGL14.eglReleaseThread could crash before Android K (see [internal: b/11327779]). + if (Util.SDK_INT >= 19) { + EGL14.eglReleaseThread(); + } display = null; context = null; surface = null; From 680d3fda3edc1da0f6a20e8251b72030aa10b87c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 21 Jun 2018 08:03:30 -0700 Subject: [PATCH 043/106] Fix download cancellation Issue: #4403 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201525284 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/upstream/cache/CacheUtil.java | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d7da1276ae..358e9c7926 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,8 @@ ([#3906](https://github.com/google/ExoPlayer/issues/3906)). * Fix crash downloading HLS media playlists ([#4396](https://github.com/google/ExoPlayer/issues/4396)). +* Fix a bug where download cancellation was ignored + ([#4403](https://github.com/google/ExoPlayer/issues/4403)). * Set `METADATA_KEY_TITLE` on media descriptions ([#4292](https://github.com/google/ExoPlayer/issues/4292)). * Allow apps to register custom MIME types diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index a1f7aa3097..c46fcd87a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -129,11 +129,11 @@ public final class CacheUtil { cache, new CacheDataSource(cache, upstream), new byte[DEFAULT_BUFFER_SIZE_BYTES], - null, - 0, + /* priorityTaskManager= */ null, + /* priority= */ 0, counters, - null, - false); + isCanceled, + /* enableEOFException= */ false); } /** From 798b29e3eff3de07677471139cd21a995b963562 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 19 Jun 2018 08:20:21 -0700 Subject: [PATCH 044/106] Normalize timestamps in HlsChunkSource Issue:#4394 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201178909 --- RELEASENOTES.md | 1 + .../exoplayer2/source/hls/HlsChunkSource.java | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 358e9c7926..4880c262d9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,7 @@ extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)). * DASH: Fix playback getting stuck when playing representations that have both sidx atoms and non-zero presentationTimeOffset values. +* HLS: Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags. * Mitigate memory leaks when `MediaSource` loads are slow to cancel ([#4249](https://github.com/google/ExoPlayer/issues/4249)). * Fix inconsistent `Player.EventListener` invocations for recursive player state diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 0fb1b6a969..37804b81f4 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -104,7 +104,7 @@ import java.util.List; // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods // in TrackSelection to avoid unexpected behavior. private TrackSelection trackSelection; - private long liveEdgeTimeUs; + private long liveEdgeInPeriodTimeUs; private boolean seenExpectedPlaylistError; /** @@ -128,7 +128,7 @@ import java.util.List; this.variants = variants; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; - liveEdgeTimeUs = C.TIME_UNSET; + liveEdgeInPeriodTimeUs = C.TIME_UNSET; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { @@ -254,16 +254,17 @@ import java.util.List; // Select the chunk. long chunkMediaSequence; + long startOfPlaylistInPeriodUs = + mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); if (previous == null || switchingVariant) { - long targetPositionUs = (previous == null || independentSegments) ? loadPositionUs - : previous.startTimeUs; - if (!mediaPlaylist.hasEndTag && targetPositionUs >= mediaPlaylist.getEndTimeUs()) { + long endOfPlaylistInPeriodUs = startOfPlaylistInPeriodUs + mediaPlaylist.durationUs; + long targetPositionInPeriodUs = + (previous == null || independentSegments) ? loadPositionUs : previous.startTimeUs; + if (!mediaPlaylist.hasEndTag && targetPositionInPeriodUs >= endOfPlaylistInPeriodUs) { // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); } else { - long positionOfPlaylistInPeriodUs = - mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); - long targetPositionInPlaylistUs = targetPositionUs - positionOfPlaylistInPeriodUs; + long targetPositionInPlaylistUs = targetPositionInPeriodUs - startOfPlaylistInPeriodUs; chunkMediaSequence = Util.binarySearchFloor( mediaPlaylist.segments, @@ -277,6 +278,8 @@ import java.util.List; selectedVariantIndex = oldVariantIndex; selectedUrl = variants[selectedVariantIndex]; mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); + startOfPlaylistInPeriodUs = + mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); chunkMediaSequence = previous.getNextChunkIndex(); } } @@ -331,9 +334,7 @@ import java.util.List; } // Compute start time of the next chunk. - long positionOfPlaylistInPeriodUs = - mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); - long segmentStartTimeInPeriodUs = positionOfPlaylistInPeriodUs + segment.relativeStartTimeUs; + long segmentStartTimeInPeriodUs = startOfPlaylistInPeriodUs + segment.relativeStartTimeUs; int discontinuitySequence = mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence; TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( @@ -420,12 +421,17 @@ import java.util.List; // Private methods. private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { - final boolean resolveTimeToLiveEdgePossible = liveEdgeTimeUs != C.TIME_UNSET; - return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; + final boolean resolveTimeToLiveEdgePossible = liveEdgeInPeriodTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible + ? liveEdgeInPeriodTimeUs - playbackPositionUs + : C.TIME_UNSET; } private void updateLiveEdgeTimeUs(HlsMediaPlaylist mediaPlaylist) { - liveEdgeTimeUs = mediaPlaylist.hasEndTag ? C.TIME_UNSET : mediaPlaylist.getEndTimeUs(); + liveEdgeInPeriodTimeUs = + mediaPlaylist.hasEndTag + ? C.TIME_UNSET + : (mediaPlaylist.getEndTimeUs() - playlistTracker.getInitialStartTimeUs()); } private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, From f1fe1c40a625e3fd516fec29c53e978e5023ca6e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 1 Jun 2018 02:22:31 -0700 Subject: [PATCH 045/106] Extract HlsPlaylistTracker interface This allows injection of custom implementations and configuration of DefaultHlsPlaylistTracker without modifying the HlsMediaSource interface. Issue:#2844 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=198846607 --- RELEASENOTES.md | 4 +- .../exoplayer2/source/hls/HlsMediaSource.java | 58 +- .../playlist/DefaultHlsPlaylistTracker.java | 566 ++++++++++++++++ .../hls/playlist/HlsPlaylistTracker.java | 618 +++--------------- 4 files changed, 699 insertions(+), 547 deletions(-) create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4880c262d9..2c3e1c78b0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,7 +6,9 @@ extractor for this ([#4297](https://github.com/google/ExoPlayer/issues/4297)). * DASH: Fix playback getting stuck when playing representations that have both sidx atoms and non-zero presentationTimeOffset values. -* HLS: Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags. +* HLS: + * Allow injection of custom playlist trackers. + * Fix adaptation in live playlists with EXT-X-PROGRAM-DATE-TIME tags. * Mitigate memory leaks when `MediaSource` loads are slow to cancel ([#4249](https://github.com/google/ExoPlayer/issues/4249)). * Fix inconsistent `Player.EventListener` invocations for recursive player state diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 01bb36f6ce..e0c805e1af 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.source.hls.playlist.DefaultHlsPlaylistTracker; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; @@ -58,6 +59,7 @@ public final class HlsMediaSource extends BaseMediaSource private HlsExtractorFactory extractorFactory; private @Nullable ParsingLoadable.Parser playlistParser; + private @Nullable HlsPlaylistTracker playlistTracker; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private int minLoadableRetryCount; private boolean allowChunklessPreparation; @@ -136,16 +138,37 @@ public final class HlsMediaSource extends BaseMediaSource * Sets the parser to parse HLS playlists. The default is an instance of {@link * HlsPlaylistParser}. * + *

    Must not be called after calling {@link #setPlaylistTracker} on the same builder. + * * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ public Factory setPlaylistParser(ParsingLoadable.Parser playlistParser) { Assertions.checkState(!isCreateCalled); + Assertions.checkState(playlistTracker == null, "A playlist tracker has already been set."); this.playlistParser = Assertions.checkNotNull(playlistParser); return this; } + /** + * Sets the HLS playlist tracker. The default is an instance of {@link + * DefaultHlsPlaylistTracker}. Playlist trackers must not be shared by {@link HlsMediaSource} + * instances. + * + *

    Must not be called after calling {@link #setPlaylistParser} on the same builder. + * + * @param playlistTracker A tracker for HLS playlists. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setPlaylistTracker(HlsPlaylistTracker playlistTracker) { + Assertions.checkState(!isCreateCalled); + Assertions.checkState(playlistParser == null, "A playlist parser has already been set."); + this.playlistTracker = Assertions.checkNotNull(playlistTracker); + return this; + } + /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source * loads data from multiple streams (video, audio etc...). The default is an instance of {@link @@ -187,8 +210,12 @@ public final class HlsMediaSource extends BaseMediaSource @Override public HlsMediaSource createMediaSource(Uri playlistUri) { isCreateCalled = true; - if (playlistParser == null) { - playlistParser = new HlsPlaylistParser(); + if (playlistTracker == null) { + playlistTracker = + new DefaultHlsPlaylistTracker( + hlsDataSourceFactory, + minLoadableRetryCount, + playlistParser != null ? playlistParser : new HlsPlaylistParser()); } return new HlsMediaSource( playlistUri, @@ -196,7 +223,7 @@ public final class HlsMediaSource extends BaseMediaSource extractorFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, - playlistParser, + playlistTracker, allowChunklessPreparation, tag); } @@ -233,12 +260,10 @@ public final class HlsMediaSource extends BaseMediaSource private final HlsDataSourceFactory dataSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; - private final ParsingLoadable.Parser playlistParser; private final boolean allowChunklessPreparation; + private final HlsPlaylistTracker playlistTracker; private final @Nullable Object tag; - private HlsPlaylistTracker playlistTracker; - /** * @param manifestUri The {@link Uri} of the HLS manifest. * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, @@ -276,8 +301,13 @@ public final class HlsMediaSource extends BaseMediaSource int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { - this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), - HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener, + this( + manifestUri, + new DefaultHlsDataSourceFactory(dataSourceFactory), + HlsExtractorFactory.DEFAULT, + minLoadableRetryCount, + eventHandler, + eventListener, new HlsPlaylistParser()); } @@ -309,7 +339,8 @@ public final class HlsMediaSource extends BaseMediaSource extractorFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, - playlistParser, + new DefaultHlsPlaylistTracker( + dataSourceFactory, minLoadableRetryCount, new HlsPlaylistParser()), /* allowChunklessPreparation= */ false, /* tag= */ null); if (eventHandler != null && eventListener != null) { @@ -323,7 +354,7 @@ public final class HlsMediaSource extends BaseMediaSource HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, int minLoadableRetryCount, - ParsingLoadable.Parser playlistParser, + HlsPlaylistTracker playlistTracker, boolean allowChunklessPreparation, @Nullable Object tag) { this.manifestUri = manifestUri; @@ -331,7 +362,7 @@ public final class HlsMediaSource extends BaseMediaSource this.extractorFactory = extractorFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.minLoadableRetryCount = minLoadableRetryCount; - this.playlistParser = playlistParser; + this.playlistTracker = playlistTracker; this.allowChunklessPreparation = allowChunklessPreparation; this.tag = tag; } @@ -339,9 +370,7 @@ public final class HlsMediaSource extends BaseMediaSource @Override public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); - playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, - minLoadableRetryCount, this, playlistParser); - playlistTracker.start(); + playlistTracker.start(manifestUri, eventDispatcher, /* listener= */ this); } @Override @@ -373,7 +402,6 @@ public final class HlsMediaSource extends BaseMediaSource public void releaseSourceInternal() { if (playlistTracker != null) { playlistTracker.release(); - playlistTracker = null; } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java new file mode 100644 index 0000000000..629c1eb59c --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -0,0 +1,566 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; +import android.os.Handler; +import android.os.SystemClock; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; +import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.UriUtil; +import java.io.IOException; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; + +/** Default implementation for {@link HlsPlaylistTracker}. */ +public final class DefaultHlsPlaylistTracker + implements HlsPlaylistTracker, Loader.Callback> { + + /** + * Coefficient applied on the target duration of a playlist to determine the amount of time after + * which an unchanging playlist is considered stuck. + */ + private static final double PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT = 3.5; + + private final HlsDataSourceFactory dataSourceFactory; + private final ParsingLoadable.Parser playlistParser; + private final int minRetryCount; + private final IdentityHashMap playlistBundles; + private final List listeners; + private final Loader initialPlaylistLoader; + + private Handler playlistRefreshHandler; + private EventDispatcher eventDispatcher; + private PrimaryPlaylistListener primaryPlaylistListener; + private HlsMasterPlaylist masterPlaylist; + private HlsUrl primaryHlsUrl; + private HlsMediaPlaylist primaryUrlSnapshot; + private boolean isLive; + private long initialStartTimeUs; + + /** + * @param dataSourceFactory A factory for {@link DataSource} instances. + * @param minRetryCount The minimum number of times loads must be retried before {@link + * #maybeThrowPlaylistRefreshError(HlsUrl)} and {@link + * #maybeThrowPrimaryPlaylistRefreshError()} propagate any loading errors. + * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + */ + public DefaultHlsPlaylistTracker( + HlsDataSourceFactory dataSourceFactory, + int minRetryCount, + ParsingLoadable.Parser playlistParser) { + this.dataSourceFactory = dataSourceFactory; + this.minRetryCount = minRetryCount; + this.playlistParser = playlistParser; + listeners = new ArrayList<>(); + initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist"); + playlistBundles = new IdentityHashMap<>(); + initialStartTimeUs = C.TIME_UNSET; + } + + // HlsPlaylistTracker implementation. + + @Override + public void start( + Uri initialPlaylistUri, + EventDispatcher eventDispatcher, + PrimaryPlaylistListener primaryPlaylistListener) { + this.playlistRefreshHandler = new Handler(); + this.eventDispatcher = eventDispatcher; + this.primaryPlaylistListener = primaryPlaylistListener; + ParsingLoadable masterPlaylistLoadable = + new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), + initialPlaylistUri, + C.DATA_TYPE_MANIFEST, + playlistParser); + initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); + } + + @Override + public void release() { + initialPlaylistLoader.release(); + for (MediaPlaylistBundle bundle : playlistBundles.values()) { + bundle.release(); + } + playlistRefreshHandler.removeCallbacksAndMessages(null); + playlistBundles.clear(); + } + + @Override + public void addListener(PlaylistEventListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(PlaylistEventListener listener) { + listeners.remove(listener); + } + + @Override + public HlsMasterPlaylist getMasterPlaylist() { + return masterPlaylist; + } + + @Override + public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { + HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); + if (snapshot != null) { + maybeSetPrimaryUrl(url); + } + return snapshot; + } + + @Override + public long getInitialStartTimeUs() { + return initialStartTimeUs; + } + + @Override + public boolean isSnapshotValid(HlsUrl url) { + return playlistBundles.get(url).isSnapshotValid(); + } + + @Override + public void maybeThrowPrimaryPlaylistRefreshError() throws IOException { + initialPlaylistLoader.maybeThrowError(); + if (primaryHlsUrl != null) { + maybeThrowPlaylistRefreshError(primaryHlsUrl); + } + } + + @Override + public void maybeThrowPlaylistRefreshError(HlsUrl url) throws IOException { + playlistBundles.get(url).maybeThrowPlaylistRefreshError(); + } + + @Override + public void refreshPlaylist(HlsUrl url) { + playlistBundles.get(url).loadPlaylist(); + } + + @Override + public boolean isLive() { + return isLive; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + HlsPlaylist result = loadable.getResult(); + HlsMasterPlaylist masterPlaylist; + boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; + if (isMediaPlaylist) { + masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); + } else /* result instanceof HlsMasterPlaylist */ { + masterPlaylist = (HlsMasterPlaylist) result; + } + this.masterPlaylist = masterPlaylist; + primaryHlsUrl = masterPlaylist.variants.get(0); + ArrayList urls = new ArrayList<>(); + urls.addAll(masterPlaylist.variants); + urls.addAll(masterPlaylist.audios); + urls.addAll(masterPlaylist.subtitles); + createBundles(urls); + MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl); + if (isMediaPlaylist) { + // We don't need to load the playlist again. We can use the same result. + primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result); + } else { + primaryBundle.loadPlaylist(); + } + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public @Loader.RetryAction int onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { + boolean isFatal = error instanceof ParserException; + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + isFatal); + return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + + // Internal methods. + + private boolean maybeSelectNewPrimaryUrl() { + List variants = masterPlaylist.variants; + int variantsSize = variants.size(); + long currentTimeMs = SystemClock.elapsedRealtime(); + for (int i = 0; i < variantsSize; i++) { + MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i)); + if (currentTimeMs > bundle.blacklistUntilMs) { + primaryHlsUrl = bundle.playlistUrl; + bundle.loadPlaylist(); + return true; + } + } + return false; + } + + private void maybeSetPrimaryUrl(HlsUrl url) { + if (url == primaryHlsUrl + || !masterPlaylist.variants.contains(url) + || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) { + // Ignore if the primary url is unchanged, if the url is not a variant url, or if the last + // primary snapshot contains an end tag. + return; + } + primaryHlsUrl = url; + playlistBundles.get(primaryHlsUrl).loadPlaylist(); + } + + private void createBundles(List urls) { + int listSize = urls.size(); + for (int i = 0; i < listSize; i++) { + HlsUrl url = urls.get(i); + MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); + playlistBundles.put(url, bundle); + } + } + + /** + * Called by the bundles when a snapshot changes. + * + * @param url The url of the playlist. + * @param newSnapshot The new snapshot. + */ + private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) { + if (url == primaryHlsUrl) { + if (primaryUrlSnapshot == null) { + // This is the first primary url snapshot. + isLive = !newSnapshot.hasEndTag; + initialStartTimeUs = newSnapshot.startTimeUs; + } + primaryUrlSnapshot = newSnapshot; + primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); + } + int listenersSize = listeners.size(); + for (int i = 0; i < listenersSize; i++) { + listeners.get(i).onPlaylistChanged(); + } + } + + private boolean notifyPlaylistError(HlsUrl playlistUrl, boolean shouldBlacklist) { + int listenersSize = listeners.size(); + boolean anyBlacklistingFailed = false; + for (int i = 0; i < listenersSize; i++) { + anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, shouldBlacklist); + } + return anyBlacklistingFailed; + } + + private HlsMediaPlaylist getLatestPlaylistSnapshot( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (!loadedPlaylist.isNewerThan(oldPlaylist)) { + if (loadedPlaylist.hasEndTag) { + // If the loaded playlist has an end tag but is not newer than the old playlist then we have + // an inconsistent state. This is typically caused by the server incorrectly resetting the + // media sequence when appending the end tag. We resolve this case as best we can by + // returning the old playlist with the end tag appended. + return oldPlaylist.copyWithEndTag(); + } else { + return oldPlaylist; + } + } + long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist); + int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist); + return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence); + } + + private long getLoadedPlaylistStartTimeUs( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasProgramDateTime) { + return loadedPlaylist.startTimeUs; + } + long primarySnapshotStartTimeUs = + primaryUrlSnapshot != null ? primaryUrlSnapshot.startTimeUs : 0; + if (oldPlaylist == null) { + return primarySnapshotStartTimeUs; + } + int oldPlaylistSize = oldPlaylist.segments.size(); + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs; + } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) { + return oldPlaylist.getEndTimeUs(); + } else { + // No segments overlap, we assume the new playlist start coincides with the primary playlist. + return primarySnapshotStartTimeUs; + } + } + + private int getLoadedPlaylistDiscontinuitySequence( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + if (loadedPlaylist.hasDiscontinuitySequence) { + return loadedPlaylist.discontinuitySequence; + } + // TODO: Improve cross-playlist discontinuity adjustment. + int primaryUrlDiscontinuitySequence = + primaryUrlSnapshot != null ? primaryUrlSnapshot.discontinuitySequence : 0; + if (oldPlaylist == null) { + return primaryUrlDiscontinuitySequence; + } + Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); + if (firstOldOverlappingSegment != null) { + return oldPlaylist.discontinuitySequence + + firstOldOverlappingSegment.relativeDiscontinuitySequence + - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; + } + return primaryUrlDiscontinuitySequence; + } + + private static Segment getFirstOldOverlappingSegment( + HlsMediaPlaylist oldPlaylist, HlsMediaPlaylist loadedPlaylist) { + int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence); + List oldSegments = oldPlaylist.segments; + return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; + } + + /** Holds all information related to a specific Media Playlist. */ + private final class MediaPlaylistBundle + implements Loader.Callback>, Runnable { + + private final HlsUrl playlistUrl; + private final Loader mediaPlaylistLoader; + private final ParsingLoadable mediaPlaylistLoadable; + + private HlsMediaPlaylist playlistSnapshot; + private long lastSnapshotLoadMs; + private long lastSnapshotChangeMs; + private long earliestNextLoadTimeMs; + private long blacklistUntilMs; + private boolean loadPending; + private IOException playlistError; + + public MediaPlaylistBundle(HlsUrl playlistUrl) { + this.playlistUrl = playlistUrl; + mediaPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MediaPlaylist"); + mediaPlaylistLoadable = + new ParsingLoadable<>( + dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), + UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), + C.DATA_TYPE_MANIFEST, + playlistParser); + } + + public HlsMediaPlaylist getPlaylistSnapshot() { + return playlistSnapshot; + } + + public boolean isSnapshotValid() { + if (playlistSnapshot == null) { + return false; + } + long currentTimeMs = SystemClock.elapsedRealtime(); + long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs)); + return playlistSnapshot.hasEndTag + || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT + || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD + || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs; + } + + public void release() { + mediaPlaylistLoader.release(); + } + + public void loadPlaylist() { + blacklistUntilMs = 0; + if (loadPending || mediaPlaylistLoader.isLoading()) { + // Load already pending or in progress. Do nothing. + return; + } + long currentTimeMs = SystemClock.elapsedRealtime(); + if (currentTimeMs < earliestNextLoadTimeMs) { + loadPending = true; + playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs); + } else { + loadPlaylistImmediately(); + } + } + + public void maybeThrowPlaylistRefreshError() throws IOException { + mediaPlaylistLoader.maybeThrowError(); + if (playlistError != null) { + throw playlistError; + } + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted( + ParsingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + HlsPlaylist result = loadable.getResult(); + if (result instanceof HlsMediaPlaylist) { + processLoadedPlaylist((HlsMediaPlaylist) result); + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } else { + playlistError = new ParserException("Loaded playlist has unexpected type."); + } + } + + @Override + public void onLoadCanceled( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + boolean released) { + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded()); + } + + @Override + public @Loader.RetryAction int onLoadError( + ParsingLoadable loadable, + long elapsedRealtimeMs, + long loadDurationMs, + IOException error) { + boolean isFatal = error instanceof ParserException; + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MANIFEST, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded(), + error, + isFatal); + boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error); + boolean shouldRetryIfNotFatal = + notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist; + if (isFatal) { + return Loader.DONT_RETRY_FATAL; + } + if (shouldBlacklist) { + shouldRetryIfNotFatal |= blacklistPlaylist(); + } + return shouldRetryIfNotFatal ? Loader.RETRY : Loader.DONT_RETRY; + } + + // Runnable implementation. + + @Override + public void run() { + loadPending = false; + loadPlaylistImmediately(); + } + + // Internal methods. + + private void loadPlaylistImmediately() { + mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); + } + + private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) { + HlsMediaPlaylist oldPlaylist = playlistSnapshot; + long currentTimeMs = SystemClock.elapsedRealtime(); + lastSnapshotLoadMs = currentTimeMs; + playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); + if (playlistSnapshot != oldPlaylist) { + playlistError = null; + lastSnapshotChangeMs = currentTimeMs; + onPlaylistUpdated(playlistUrl, playlistSnapshot); + } else if (!playlistSnapshot.hasEndTag) { + if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() + < playlistSnapshot.mediaSequence) { + // The media sequence jumped backwards. The server has probably reset. + playlistError = new PlaylistResetException(playlistUrl.url); + notifyPlaylistError(playlistUrl, false); + } else if (currentTimeMs - lastSnapshotChangeMs + > C.usToMs(playlistSnapshot.targetDurationUs) + * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { + // The playlist seems to be stuck. Blacklist it. + playlistError = new PlaylistStuckException(playlistUrl.url); + notifyPlaylistError(playlistUrl, true); + blacklistPlaylist(); + } + } + // Do not allow the playlist to load again within the target duration if we obtained a new + // snapshot, or half the target duration otherwise. + earliestNextLoadTimeMs = + currentTimeMs + + C.usToMs( + playlistSnapshot != oldPlaylist + ? playlistSnapshot.targetDurationUs + : (playlistSnapshot.targetDurationUs / 2)); + // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the + // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes + // the primary. + if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) { + loadPlaylist(); + } + } + + /** + * Blacklists the playlist. + * + * @return Whether the playlist is the primary, despite being blacklisted. + */ + private boolean blacklistPlaylist() { + blacklistUntilMs = + SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; + return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); + } + } +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index 9986f5b65b..febd1c217d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,66 +16,28 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; -import android.os.Handler; -import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; -import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; -import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.Loader; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.UriUtil; import java.io.IOException; -import java.util.ArrayList; -import java.util.IdentityHashMap; -import java.util.List; /** - * Tracks playlists linked to a provided playlist url. The provided url might reference an HLS - * master playlist or a media playlist. + * Tracks playlists associated to an HLS stream and provides snapshots. + * + *

    The playlist tracker is responsible for exposing the seeking window, which is defined by the + * segments that one of the playlists exposes. This playlist is called primary and needs to be + * periodically refreshed in the case of live streams. Note that the primary playlist is one of the + * media playlists while the master playlist is an optional kind of playlist defined by the HLS + * specification (RFC 8216). + * + *

    Playlist loads might encounter errors. The tracker may choose to blacklist them to ensure a + * primary playlist is always available. */ -public final class HlsPlaylistTracker implements Loader.Callback> { +public interface HlsPlaylistTracker { - /** - * Thrown when a playlist is considered to be stuck due to a server side error. - */ - public static final class PlaylistStuckException extends IOException { - - /** - * The url of the stuck playlist. - */ - public final String url; - - private PlaylistStuckException(String url) { - this.url = url; - } - - } - - /** - * Thrown when the media sequence of a new snapshot indicates the server has reset. - */ - public static final class PlaylistResetException extends IOException { - - /** - * The url of the reset playlist. - */ - public final String url; - - private PlaylistResetException(String url) { - this.url = url; - } - - } - - /** - * Listener for primary playlist changes. - */ - public interface PrimaryPlaylistListener { + /** Listener for primary playlist changes. */ + interface PrimaryPlaylistListener { /** * Called when the primary playlist changes. @@ -85,10 +47,8 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser; - private final int minRetryCount; - private final IdentityHashMap playlistBundles; - private final Handler playlistRefreshHandler; - private final PrimaryPlaylistListener primaryPlaylistListener; - private final List listeners; - private final Loader initialPlaylistLoader; - private final EventDispatcher eventDispatcher; + /** The url of the stuck playlist. */ + public final String url; - private HlsMasterPlaylist masterPlaylist; - private HlsUrl primaryHlsUrl; - private HlsMediaPlaylist primaryUrlSnapshot; - private boolean isLive; - private long initialStartTimeUs; - - /** - * @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media - * playlist or a master playlist. - * @param dataSourceFactory A factory for {@link DataSource} instances. - * @param eventDispatcher A dispatcher to notify of events. - * @param minRetryCount The minimum number of times loads must be retried before - * {@link #maybeThrowPlaylistRefreshError(HlsUrl)} and - * {@link #maybeThrowPrimaryPlaylistRefreshError()} propagate any loading errors. - * @param primaryPlaylistListener A callback for the primary playlist change events. - */ - public HlsPlaylistTracker(Uri initialPlaylistUri, HlsDataSourceFactory dataSourceFactory, - EventDispatcher eventDispatcher, int minRetryCount, - PrimaryPlaylistListener primaryPlaylistListener, - ParsingLoadable.Parser playlistParser) { - this.initialPlaylistUri = initialPlaylistUri; - this.dataSourceFactory = dataSourceFactory; - this.eventDispatcher = eventDispatcher; - this.minRetryCount = minRetryCount; - this.primaryPlaylistListener = primaryPlaylistListener; - this.playlistParser = playlistParser; - listeners = new ArrayList<>(); - initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); - playlistBundles = new IdentityHashMap<>(); - playlistRefreshHandler = new Handler(); - initialStartTimeUs = C.TIME_UNSET; + /** + * Creates an instance. + * + * @param url See {@link #url}. + */ + public PlaylistStuckException(String url) { + this.url = url; + } } + /** Thrown when the media sequence of a new snapshot indicates the server has reset. */ + final class PlaylistResetException extends IOException { + + /** The url of the reset playlist. */ + public final String url; + + /** + * Creates an instance. + * + * @param url See {@link #url}. + */ + public PlaylistResetException(String url) { + this.url = url; + } + } + + /** + * Starts the playlist tracker. + * + *

    Must be called from the playback thread. A tracker may be restarted after a {@link + * #release()} call. + * + * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master + * playlist. + * @param eventDispatcher A dispatcher to notify of events. + * @param listener A callback for the primary playlist change events. + */ + void start( + Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener); + + /** Releases all acquired resources. Must be called once per {@link #start} call. */ + void release(); + /** * Registers a listener to receive events from the playlist tracker. * * @param listener The listener. */ - public void addListener(PlaylistEventListener listener) { - listeners.add(listener); - } + void addListener(PlaylistEventListener listener); /** * Unregisters a listener. * * @param listener The listener to unregister. */ - public void removeListener(PlaylistEventListener listener) { - listeners.remove(listener); - } - - /** - * Starts tracking all the playlists related to the provided Uri. - */ - public void start() { - ParsingLoadable masterPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), initialPlaylistUri, - C.DATA_TYPE_MANIFEST, playlistParser); - initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); - } + void removeListener(PlaylistEventListener listener); /** * Returns the master playlist. * + *

    If the uri passed to {@link #start} points to a media playlist, an {@link HlsMasterPlaylist} + * with a single variant for said media playlist is returned. + * * @return The master playlist. Null if the initial playlist has yet to be loaded. */ - public HlsMasterPlaylist getMasterPlaylist() { - return masterPlaylist; - } + @Nullable + HlsMasterPlaylist getMasterPlaylist(); /** - * Returns the most recent snapshot available of the playlist referenced by the provided - * {@link HlsUrl}. + * Returns the most recent snapshot available of the playlist referenced by the provided {@link + * HlsUrl}. * * @param url The {@link HlsUrl} corresponding to the requested media playlist. * @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May * be null if no snapshot has been loaded yet. */ - public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) { - HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot(); - if (snapshot != null) { - maybeSetPrimaryUrl(url); - } - return snapshot; - } + @Nullable + HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url); /** * Returns the start time of the first loaded primary playlist, or {@link C#TIME_UNSET} if no * media playlist has been loaded. */ - public long getInitialStartTimeUs() { - return initialStartTimeUs; - } + long getInitialStartTimeUs(); /** * Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is * valid, meaning all the segments referenced by the playlist are expected to be available. If the * playlist is not valid then some of the segments may no longer be available. - + * * @param url The {@link HlsUrl}. * @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is * valid. */ - public boolean isSnapshotValid(HlsUrl url) { - return playlistBundles.get(url).isSnapshotValid(); - } - - /** - * Releases the playlist tracker. - */ - public void release() { - initialPlaylistLoader.release(); - for (MediaPlaylistBundle bundle : playlistBundles.values()) { - bundle.release(); - } - playlistRefreshHandler.removeCallbacksAndMessages(null); - playlistBundles.clear(); - } + boolean isSnapshotValid(HlsUrl url); /** * If the tracker is having trouble refreshing the master playlist or the primary playlist, this @@ -247,401 +173,31 @@ public final class HlsPlaylistTracker implements Loader.CallbackThe playlist tracker may choose the delay the playlist refresh. The request is discarded if + * a refresh was already pending. * * @param url The {@link HlsUrl} of the playlist to be refreshed. */ - public void refreshPlaylist(HlsUrl url) { - playlistBundles.get(url).loadPlaylist(); - } + void refreshPlaylist(HlsUrl url); /** - * Returns whether this is live content. + * Returns whether the tracked playlists describe a live stream. * * @return True if the content is live. False otherwise. */ - public boolean isLive() { - return isLive; - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - HlsPlaylist result = loadable.getResult(); - HlsMasterPlaylist masterPlaylist; - boolean isMediaPlaylist = result instanceof HlsMediaPlaylist; - if (isMediaPlaylist) { - masterPlaylist = HlsMasterPlaylist.createSingleVariantMasterPlaylist(result.baseUri); - } else /* result instanceof HlsMasterPlaylist */ { - masterPlaylist = (HlsMasterPlaylist) result; - } - this.masterPlaylist = masterPlaylist; - primaryHlsUrl = masterPlaylist.variants.get(0); - ArrayList urls = new ArrayList<>(); - urls.addAll(masterPlaylist.variants); - urls.addAll(masterPlaylist.audios); - urls.addAll(masterPlaylist.subtitles); - createBundles(urls); - MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl); - if (isMediaPlaylist) { - // We don't need to load the playlist again. We can use the same result. - primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result); - } else { - primaryBundle.loadPlaylist(); - } - eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public @Loader.RetryAction int onLoadError( - ParsingLoadable loadable, - long elapsedRealtimeMs, - long loadDurationMs, - IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded(), error, isFatal); - return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; - } - - // Internal methods. - - private boolean maybeSelectNewPrimaryUrl() { - List variants = masterPlaylist.variants; - int variantsSize = variants.size(); - long currentTimeMs = SystemClock.elapsedRealtime(); - for (int i = 0; i < variantsSize; i++) { - MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i)); - if (currentTimeMs > bundle.blacklistUntilMs) { - primaryHlsUrl = bundle.playlistUrl; - bundle.loadPlaylist(); - return true; - } - } - return false; - } - - private void maybeSetPrimaryUrl(HlsUrl url) { - if (url == primaryHlsUrl - || !masterPlaylist.variants.contains(url) - || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) { - // Ignore if the primary url is unchanged, if the url is not a variant url, or if the last - // primary snapshot contains an end tag. - return; - } - primaryHlsUrl = url; - playlistBundles.get(primaryHlsUrl).loadPlaylist(); - } - - private void createBundles(List urls) { - int listSize = urls.size(); - for (int i = 0; i < listSize; i++) { - HlsUrl url = urls.get(i); - MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); - playlistBundles.put(url, bundle); - } - } - - /** - * Called by the bundles when a snapshot changes. - * - * @param url The url of the playlist. - * @param newSnapshot The new snapshot. - */ - private void onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) { - if (url == primaryHlsUrl) { - if (primaryUrlSnapshot == null) { - // This is the first primary url snapshot. - isLive = !newSnapshot.hasEndTag; - initialStartTimeUs = newSnapshot.startTimeUs; - } - primaryUrlSnapshot = newSnapshot; - primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot); - } - int listenersSize = listeners.size(); - for (int i = 0; i < listenersSize; i++) { - listeners.get(i).onPlaylistChanged(); - } - } - - private boolean notifyPlaylistError(HlsUrl playlistUrl, boolean shouldBlacklist) { - int listenersSize = listeners.size(); - boolean anyBlacklistingFailed = false; - for (int i = 0; i < listenersSize; i++) { - anyBlacklistingFailed |= !listeners.get(i).onPlaylistError(playlistUrl, shouldBlacklist); - } - return anyBlacklistingFailed; - } - - private HlsMediaPlaylist getLatestPlaylistSnapshot(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (!loadedPlaylist.isNewerThan(oldPlaylist)) { - if (loadedPlaylist.hasEndTag) { - // If the loaded playlist has an end tag but is not newer than the old playlist then we have - // an inconsistent state. This is typically caused by the server incorrectly resetting the - // media sequence when appending the end tag. We resolve this case as best we can by - // returning the old playlist with the end tag appended. - return oldPlaylist.copyWithEndTag(); - } else { - return oldPlaylist; - } - } - long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist); - int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist); - return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence); - } - - private long getLoadedPlaylistStartTimeUs(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (loadedPlaylist.hasProgramDateTime) { - return loadedPlaylist.startTimeUs; - } - long primarySnapshotStartTimeUs = primaryUrlSnapshot != null - ? primaryUrlSnapshot.startTimeUs : 0; - if (oldPlaylist == null) { - return primarySnapshotStartTimeUs; - } - int oldPlaylistSize = oldPlaylist.segments.size(); - Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); - if (firstOldOverlappingSegment != null) { - return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs; - } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) { - return oldPlaylist.getEndTimeUs(); - } else { - // No segments overlap, we assume the new playlist start coincides with the primary playlist. - return primarySnapshotStartTimeUs; - } - } - - private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - if (loadedPlaylist.hasDiscontinuitySequence) { - return loadedPlaylist.discontinuitySequence; - } - // TODO: Improve cross-playlist discontinuity adjustment. - int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null - ? primaryUrlSnapshot.discontinuitySequence : 0; - if (oldPlaylist == null) { - return primaryUrlDiscontinuitySequence; - } - Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist); - if (firstOldOverlappingSegment != null) { - return oldPlaylist.discontinuitySequence - + firstOldOverlappingSegment.relativeDiscontinuitySequence - - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence; - } - return primaryUrlDiscontinuitySequence; - } - - private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist, - HlsMediaPlaylist loadedPlaylist) { - int mediaSequenceOffset = (int) (loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence); - List oldSegments = oldPlaylist.segments; - return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null; - } - - /** - * Holds all information related to a specific Media Playlist. - */ - private final class MediaPlaylistBundle implements Loader.Callback>, - Runnable { - - private final HlsUrl playlistUrl; - private final Loader mediaPlaylistLoader; - private final ParsingLoadable mediaPlaylistLoadable; - - private HlsMediaPlaylist playlistSnapshot; - private long lastSnapshotLoadMs; - private long lastSnapshotChangeMs; - private long earliestNextLoadTimeMs; - private long blacklistUntilMs; - private boolean loadPending; - private IOException playlistError; - - public MediaPlaylistBundle(HlsUrl playlistUrl) { - this.playlistUrl = playlistUrl; - mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist"); - mediaPlaylistLoadable = new ParsingLoadable<>( - dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), - UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST, - playlistParser); - } - - public HlsMediaPlaylist getPlaylistSnapshot() { - return playlistSnapshot; - } - - public boolean isSnapshotValid() { - if (playlistSnapshot == null) { - return false; - } - long currentTimeMs = SystemClock.elapsedRealtime(); - long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs)); - return playlistSnapshot.hasEndTag - || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT - || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD - || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs; - } - - public void release() { - mediaPlaylistLoader.release(); - } - - public void loadPlaylist() { - blacklistUntilMs = 0; - if (loadPending || mediaPlaylistLoader.isLoading()) { - // Load already pending or in progress. Do nothing. - return; - } - long currentTimeMs = SystemClock.elapsedRealtime(); - if (currentTimeMs < earliestNextLoadTimeMs) { - loadPending = true; - playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs); - } else { - loadPlaylistImmediately(); - } - } - - public void maybeThrowPlaylistRefreshError() throws IOException { - mediaPlaylistLoader.maybeThrowError(); - if (playlistError != null) { - throw playlistError; - } - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - HlsPlaylist result = loadable.getResult(); - if (result instanceof HlsMediaPlaylist) { - processLoadedPlaylist((HlsMediaPlaylist) result); - eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } else { - playlistError = new ParserException("Loaded playlist has unexpected type."); - } - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public @Loader.RetryAction int onLoadError( - ParsingLoadable loadable, - long elapsedRealtimeMs, - long loadDurationMs, - IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded(), error, isFatal); - boolean shouldBlacklist = ChunkedTrackBlacklistUtil.shouldBlacklist(error); - boolean shouldRetryIfNotFatal = - notifyPlaylistError(playlistUrl, shouldBlacklist) || !shouldBlacklist; - if (isFatal) { - return Loader.DONT_RETRY_FATAL; - } - if (shouldBlacklist) { - shouldRetryIfNotFatal |= blacklistPlaylist(); - } - return shouldRetryIfNotFatal ? Loader.RETRY : Loader.DONT_RETRY; - } - - // Runnable implementation. - - @Override - public void run() { - loadPending = false; - loadPlaylistImmediately(); - } - - // Internal methods. - - private void loadPlaylistImmediately() { - mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); - } - - private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) { - HlsMediaPlaylist oldPlaylist = playlistSnapshot; - long currentTimeMs = SystemClock.elapsedRealtime(); - lastSnapshotLoadMs = currentTimeMs; - playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist); - if (playlistSnapshot != oldPlaylist) { - playlistError = null; - lastSnapshotChangeMs = currentTimeMs; - onPlaylistUpdated(playlistUrl, playlistSnapshot); - } else if (!playlistSnapshot.hasEndTag) { - if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() - < playlistSnapshot.mediaSequence) { - // The media sequence jumped backwards. The server has probably reset. - playlistError = new PlaylistResetException(playlistUrl.url); - notifyPlaylistError(playlistUrl, false); - } else if (currentTimeMs - lastSnapshotChangeMs - > C.usToMs(playlistSnapshot.targetDurationUs) - * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { - // The playlist seems to be stuck. Blacklist it. - playlistError = new PlaylistStuckException(playlistUrl.url); - notifyPlaylistError(playlistUrl, true); - blacklistPlaylist(); - } - } - // Do not allow the playlist to load again within the target duration if we obtained a new - // snapshot, or half the target duration otherwise. - earliestNextLoadTimeMs = currentTimeMs + C.usToMs(playlistSnapshot != oldPlaylist - ? playlistSnapshot.targetDurationUs : (playlistSnapshot.targetDurationUs / 2)); - // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the - // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes - // the primary. - if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) { - loadPlaylist(); - } - } - - /** - * Blacklists the playlist. - * - * @return Whether the playlist is the primary, despite being blacklisted. - */ - private boolean blacklistPlaylist() { - blacklistUntilMs = SystemClock.elapsedRealtime() - + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; - return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); - } - - } - + boolean isLive(); } From 35ac394de339fad2a988a0dce7e005c49da17254 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 12 Jun 2018 07:26:26 -0700 Subject: [PATCH 046/106] Add missing onLoadStarted events to HLS playlist tracker. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=200211755 --- .../source/hls/playlist/DefaultHlsPlaylistTracker.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index 629c1eb59c..1d46811cee 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -96,7 +96,10 @@ public final class DefaultHlsPlaylistTracker initialPlaylistUri, C.DATA_TYPE_MANIFEST, playlistParser); - initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); + long elapsedRealtime = + initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); + eventDispatcher.loadStarted( + masterPlaylistLoadable.dataSpec, masterPlaylistLoadable.type, elapsedRealtime); } @Override @@ -509,7 +512,10 @@ public final class DefaultHlsPlaylistTracker // Internal methods. private void loadPlaylistImmediately() { - mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); + long elapsedRealtime = + mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount); + eventDispatcher.loadStarted( + mediaPlaylistLoadable.dataSpec, mediaPlaylistLoadable.type, elapsedRealtime); } private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) { From 3ede1aaa8ec1633cd0e27f9dd56619a78a28e411 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 21 Jun 2018 08:39:25 -0700 Subject: [PATCH 047/106] Fix re-starting of DefaultHlsPlalyistTracker ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201530049 --- .../playlist/DefaultHlsPlaylistTracker.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index 1d46811cee..014a302de7 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segmen import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.UriUtil; import java.io.IOException; import java.util.ArrayList; @@ -49,10 +50,10 @@ public final class DefaultHlsPlaylistTracker private final int minRetryCount; private final IdentityHashMap playlistBundles; private final List listeners; - private final Loader initialPlaylistLoader; - private Handler playlistRefreshHandler; private EventDispatcher eventDispatcher; + private Loader initialPlaylistLoader; + private Handler playlistRefreshHandler; private PrimaryPlaylistListener primaryPlaylistListener; private HlsMasterPlaylist masterPlaylist; private HlsUrl primaryHlsUrl; @@ -75,7 +76,6 @@ public final class DefaultHlsPlaylistTracker this.minRetryCount = minRetryCount; this.playlistParser = playlistParser; listeners = new ArrayList<>(); - initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist"); playlistBundles = new IdentityHashMap<>(); initialStartTimeUs = C.TIME_UNSET; } @@ -96,6 +96,8 @@ public final class DefaultHlsPlaylistTracker initialPlaylistUri, C.DATA_TYPE_MANIFEST, playlistParser); + Assertions.checkState(initialPlaylistLoader == null); + initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MasterPlaylist"); long elapsedRealtime = initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount); eventDispatcher.loadStarted( @@ -104,11 +106,17 @@ public final class DefaultHlsPlaylistTracker @Override public void release() { + primaryHlsUrl = null; + primaryUrlSnapshot = null; + masterPlaylist = null; + initialStartTimeUs = C.TIME_UNSET; initialPlaylistLoader.release(); + initialPlaylistLoader = null; for (MediaPlaylistBundle bundle : playlistBundles.values()) { bundle.release(); } playlistRefreshHandler.removeCallbacksAndMessages(null); + playlistRefreshHandler = null; playlistBundles.clear(); } @@ -148,7 +156,9 @@ public final class DefaultHlsPlaylistTracker @Override public void maybeThrowPrimaryPlaylistRefreshError() throws IOException { - initialPlaylistLoader.maybeThrowError(); + if (initialPlaylistLoader != null) { + initialPlaylistLoader.maybeThrowError(); + } if (primaryHlsUrl != null) { maybeThrowPlaylistRefreshError(primaryHlsUrl); } From d880fac582ea156ca84388d1ca9f8627c64c70d6 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 25 Jun 2018 23:15:02 +0100 Subject: [PATCH 048/106] Fix release build --- .../exoplayer2/source/hls/offline/HlsDownloadHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java index c043e57588..7fe03f6cb3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -73,7 +73,7 @@ public final class HlsDownloadHelper extends DownloadHelper { public TrackGroupArray getTrackGroups(int periodIndex) { Assertions.checkNotNull(playlist); if (playlist instanceof HlsMediaPlaylist) { - renditionGroups = new int[0]; + renditionTypes = new int[0]; return TrackGroupArray.EMPTY; } // TODO: Generate track groups as in playback. Reverse the mapping in getDownloadAction. From 5f79aa253bdcaba23906ce724a4f7d3de2f5c97d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 7 Jun 2018 09:06:17 -0700 Subject: [PATCH 049/106] Add license server URL to SchemeData Allows DrmInitData to carry a license server URL when the media declares one. Issue:#3393 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199643743 --- RELEASENOTES.md | 6 + .../exoplayer2/drm/DefaultDrmSession.java | 104 +++++++++--------- .../drm/DefaultDrmSessionManager.java | 40 +------ .../android/exoplayer2/drm/DrmInitData.java | 31 +++++- .../exoplayer2/drm/FrameworkMediaDrm.java | 35 +++++- .../exoplayer2/drm/HttpMediaDrmCallback.java | 8 +- .../exoplayer2/drm/LocalMediaDrmCallback.java | 5 +- .../exoplayer2/drm/MediaDrmCallback.java | 10 +- 8 files changed, 141 insertions(+), 98 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2c3e1c78b0..46a1117202 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,11 @@ # Release notes # +### 2.8.3 ### + +* DRM: + * Allow DrmInitData to carry a license server URL + ([#3393](https://github.com/google/ExoPlayer/issues/3393)). + ### 2.8.2 ### * IMA: Don't advertise support for video/mpeg ad media, as we don't have an diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index c57b023139..fbbbcbc9ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -22,11 +22,12 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DefaultDrmSessionEventListener.EventDispatcher; -import com.google.android.exoplayer2.drm.ExoMediaDrm.DefaultKeyRequest; +import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import java.util.Arrays; @@ -77,8 +78,7 @@ import java.util.UUID; private final ExoMediaDrm mediaDrm; private final ProvisioningManager provisioningManager; - private final byte[] initData; - private final String mimeType; + private final SchemeData schemeData; private final @DefaultDrmSessionManager.Mode int mode; private final HashMap optionalKeyRequestParameters; private final EventDispatcher eventDispatcher; @@ -103,9 +103,11 @@ import java.util.UUID; * @param uuid The UUID of the drm scheme. * @param mediaDrm The media DRM. * @param provisioningManager The manager for provisioning. - * @param initData The DRM init data. + * @param schemeData The DRM data for this session, or null if a {@code offlineLicenseKeySetId} is + * provided. * @param mode The DRM mode. - * @param offlineLicenseKeySetId The offlineLicense KeySetId. + * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using + * offline keys. * @param optionalKeyRequestParameters The optional key request parameters. * @param callback The media DRM callback. * @param playbackLooper The playback looper. @@ -117,10 +119,9 @@ import java.util.UUID; UUID uuid, ExoMediaDrm mediaDrm, ProvisioningManager provisioningManager, - byte[] initData, - String mimeType, + @Nullable SchemeData schemeData, @DefaultDrmSessionManager.Mode int mode, - byte[] offlineLicenseKeySetId, + @Nullable byte[] offlineLicenseKeySetId, HashMap optionalKeyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, @@ -131,6 +132,7 @@ import java.util.UUID; this.mediaDrm = mediaDrm; this.mode = mode; this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.schemeData = offlineLicenseKeySetId == null ? schemeData : null; this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.callback = callback; this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; @@ -141,14 +143,6 @@ import java.util.UUID; requestHandlerThread = new HandlerThread("DrmRequestHandler"); requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - - if (offlineLicenseKeySetId == null) { - this.initData = initData; - this.mimeType = mimeType; - } else { - this.initData = null; - this.mimeType = null; - } } // Life cycle. @@ -187,13 +181,37 @@ import java.util.UUID; } public boolean hasInitData(byte[] initData) { - return Arrays.equals(this.initData, initData); + return Arrays.equals(schemeData != null ? schemeData.data : null, initData); } public boolean hasSessionId(byte[] sessionId) { return Arrays.equals(this.sessionId, sessionId); } + @SuppressWarnings("deprecation") + public void onMediaDrmEvent(int what) { + if (!isOpen()) { + return; + } + switch (what) { + case ExoMediaDrm.EVENT_KEY_REQUIRED: + doLicense(false); + break; + case ExoMediaDrm.EVENT_KEY_EXPIRED: + // When an already expired key is loaded MediaDrm sends this event immediately. Ignore + // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still + // waiting for key response. + onKeysExpired(); + break; + case ExoMediaDrm.EVENT_PROVISION_REQUIRED: + state = STATE_OPENED; + provisioningManager.provisionRequired(this); + break; + default: + break; + } + } + // Provisioning implementation. public void provision() { @@ -356,14 +374,19 @@ import java.util.UUID; private void postKeyRequest(int type, boolean allowRetry) { byte[] scope = type == ExoMediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; + byte[] initData = null; + String mimeType = null; + String licenseServerUrl = null; + if (schemeData != null) { + initData = schemeData.data; + mimeType = schemeData.mimeType; + licenseServerUrl = schemeData.licenseServerUrl; + } try { - KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, - optionalKeyRequestParameters); - if (C.CLEARKEY_UUID.equals(uuid)) { - request = new DefaultKeyRequest(ClearKeyUtil.adjustRequestData(request.getData()), - request.getDefaultUrl()); - } - postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget(); + KeyRequest request = + mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); + Pair arguments = Pair.create(request, licenseServerUrl); + postRequestHandler.obtainMessage(MSG_KEYS, arguments, allowRetry).sendToTarget(); } catch (Exception e) { onKeysError(e); } @@ -382,9 +405,6 @@ import java.util.UUID; try { byte[] responseData = (byte[]) response; - if (C.CLEARKEY_UUID.equals(uuid)) { - responseData = ClearKeyUtil.adjustResponseData(responseData); - } if (mode == DefaultDrmSessionManager.MODE_RELEASE) { mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData); eventDispatcher.drmKeysRemoved(); @@ -430,30 +450,7 @@ import java.util.UUID; return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; } - @SuppressWarnings("deprecation") - public void onMediaDrmEvent(int what) { - if (!isOpen()) { - return; - } - switch (what) { - case ExoMediaDrm.EVENT_KEY_REQUIRED: - doLicense(false); - break; - case ExoMediaDrm.EVENT_KEY_EXPIRED: - // When an already expired key is loaded MediaDrm sends this event immediately. Ignore - // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still - // waiting for key response. - onKeysExpired(); - break; - case ExoMediaDrm.EVENT_PROVISION_REQUIRED: - state = STATE_OPENED; - provisioningManager.provisionRequired(this); - break; - default: - break; - } - - } + // Internal classes. @SuppressLint("HandlerLeak") private class PostResponseHandler extends Handler { @@ -492,6 +489,7 @@ import java.util.UUID; } @Override + @SuppressWarnings("unchecked") public void handleMessage(Message msg) { Object response; try { @@ -500,7 +498,8 @@ import java.util.UUID; response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); break; case MSG_KEYS: - response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj); + Pair arguments = (Pair) msg.obj; + response = callback.executeKeyRequest(uuid, arguments.first, arguments.second); break; default: throw new RuntimeException(); @@ -534,5 +533,4 @@ import java.util.UUID; } } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 66c9e5cde7..28fd7e15ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -89,7 +88,6 @@ public class DefaultDrmSessionManager implements DrmSe public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; private static final String TAG = "DefaultDrmSessionMgr"; - private static final String CENC_SCHEME_MIME_TYPE = "cenc"; private final UUID uuid; private final ExoMediaDrm mediaDrm; @@ -509,17 +507,14 @@ public class DefaultDrmSessionManager implements DrmSe } } - byte[] initData = null; - String mimeType = null; + SchemeData schemeData = null; if (offlineLicenseKeySetId == null) { - SchemeData data = getSchemeData(drmInitData, uuid, false); - if (data == null) { + schemeData = getSchemeData(drmInitData, uuid, false); + if (schemeData == null) { final MissingSchemeDataException error = new MissingSchemeDataException(uuid); eventDispatcher.drmSessionManagerError(error); return new ErrorStateDrmSession<>(new DrmSessionException(error)); } - initData = getSchemeInitData(data, uuid); - mimeType = getSchemeMimeType(data, uuid); } DefaultDrmSession session; @@ -528,6 +523,7 @@ public class DefaultDrmSessionManager implements DrmSe } else { // Only use an existing session if it has matching init data. session = null; + byte[] initData = schemeData != null ? schemeData.data : null; for (DefaultDrmSession existingSession : sessions) { if (existingSession.hasInitData(initData)) { session = existingSession; @@ -543,8 +539,7 @@ public class DefaultDrmSessionManager implements DrmSe uuid, mediaDrm, this, - initData, - mimeType, + schemeData, mode, offlineLicenseKeySetId, optionalKeyRequestParameters, @@ -650,31 +645,6 @@ public class DefaultDrmSessionManager implements DrmSe return matchingSchemeDatas.get(0); } - private static byte[] getSchemeInitData(SchemeData data, UUID uuid) { - byte[] schemeInitData = data.data; - if (Util.SDK_INT < 21) { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid); - if (psshData == null) { - // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. - } else { - schemeInitData = psshData; - } - } - return schemeInitData; - } - - private static String getSchemeMimeType(SchemeData data, UUID uuid) { - String schemeMimeType = data.mimeType; - if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) - && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) - || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { - // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. - schemeMimeType = CENC_SCHEME_MIME_TYPE; - } - return schemeMimeType; - } - @SuppressLint("HandlerLeak") private class MediaDrmHandler extends Handler { 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 c2de662010..cd7adea1e2 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 @@ -266,9 +266,9 @@ public final class DrmInitData implements Comparator, Parcelable { * applies to all schemes). */ private final UUID uuid; - /** - * The mimeType of {@link #data}. - */ + /** The URL of the server to which license requests should be made. May be null if unknown. */ + public final @Nullable String licenseServerUrl; + /** The mimeType of {@link #data}. */ public final String mimeType; /** * The initialization data. May be null for scheme support checks only. @@ -297,7 +297,25 @@ public final class DrmInitData implements Comparator, Parcelable { * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. */ public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) { + this(uuid, /* licenseServerUrl= */ null, mimeType, data, requiresSecureDecryption); + } + + /** + * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is + * universal (i.e. applies to all schemes). + * @param licenseServerUrl See {@link #licenseServerUrl}. + * @param mimeType See {@link #mimeType}. + * @param data See {@link #data}. + * @param requiresSecureDecryption See {@link #requiresSecureDecryption}. + */ + public SchemeData( + UUID uuid, + @Nullable String licenseServerUrl, + String mimeType, + byte[] data, + boolean requiresSecureDecryption) { this.uuid = Assertions.checkNotNull(uuid); + this.licenseServerUrl = licenseServerUrl; this.mimeType = Assertions.checkNotNull(mimeType); this.data = data; this.requiresSecureDecryption = requiresSecureDecryption; @@ -305,6 +323,7 @@ public final class DrmInitData implements Comparator, Parcelable { /* package */ SchemeData(Parcel in) { uuid = new UUID(in.readLong(), in.readLong()); + licenseServerUrl = in.readString(); mimeType = in.readString(); data = in.createByteArray(); requiresSecureDecryption = in.readByte() != 0; @@ -346,7 +365,9 @@ public final class DrmInitData implements Comparator, Parcelable { return true; } SchemeData other = (SchemeData) obj; - return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid) + return Util.areEqual(licenseServerUrl, other.licenseServerUrl) + && Util.areEqual(mimeType, other.mimeType) + && Util.areEqual(uuid, other.uuid) && Arrays.equals(data, other.data); } @@ -354,6 +375,7 @@ public final class DrmInitData implements Comparator, Parcelable { public int hashCode() { if (hashCode == 0) { int result = uuid.hashCode(); + result = 31 * result + (licenseServerUrl == null ? 0 : licenseServerUrl.hashCode()); result = 31 * result + mimeType.hashCode(); result = 31 * result + Arrays.hashCode(data); hashCode = result; @@ -372,6 +394,7 @@ public final class DrmInitData implements Comparator, Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(uuid.getMostSignificantBits()); dest.writeLong(uuid.getLeastSignificantBits()); + dest.writeString(licenseServerUrl); dest.writeString(mimeType); dest.writeByteArray(data); dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index f960cd637f..9f30cef0f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -26,7 +26,9 @@ import android.media.UnsupportedSchemeException; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.HashMap; @@ -40,6 +42,8 @@ import java.util.UUID; @TargetApi(23) public final class FrameworkMediaDrm implements ExoMediaDrm { + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + private final UUID uuid; private final MediaDrm mediaDrm; @@ -116,14 +120,43 @@ public final class FrameworkMediaDrm implements ExoMediaDrm optionalParameters) throws NotProvisionedException { + + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. + if (Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid)) { + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(init, uuid); + if (psshData == null) { + // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. + } else { + init = psshData; + } + } + + // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. + if (Util.SDK_INT < 26 + && C.CLEARKEY_UUID.equals(uuid) + && (MimeTypes.VIDEO_MP4.equals(mimeType) || MimeTypes.AUDIO_MP4.equals(mimeType))) { + mimeType = CENC_SCHEME_MIME_TYPE; + } + final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType, optionalParameters); - return new DefaultKeyRequest(request.getData(), request.getDefaultUrl()); + + byte[] requestData = request.getData(); + if (C.CLEARKEY_UUID.equals(uuid)) { + requestData = ClearKeyUtil.adjustRequestData(requestData); + } + + return new DefaultKeyRequest(requestData, request.getDefaultUrl()); } @Override public byte[] provideKeyResponse(byte[] scope, byte[] response) throws NotProvisionedException, DeniedByServerException { + + if (C.CLEARKEY_UUID.equals(uuid)) { + response = ClearKeyUtil.adjustResponseData(response); + } + return mediaDrm.provideKeyResponse(scope, response); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index 9150a72b53..fc1e62a89c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.drm; import android.annotation.TargetApi; import android.net.Uri; +import android.support.annotation.Nullable; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; @@ -114,8 +115,13 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { } @Override - public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + public byte[] executeKeyRequest( + UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl) + throws Exception { String url = request.getDefaultUrl(); + if (TextUtils.isEmpty(url)) { + url = mediaProvidedLicenseServerUrl; + } if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { url = defaultLicenseUrl; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java index 7b9aeca30a..7ed4a61a60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.drm; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.util.Assertions; @@ -44,7 +45,9 @@ public final class LocalMediaDrmCallback implements MediaDrmCallback { } @Override - public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + public byte[] executeKeyRequest( + UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl) + throws Exception { return keyResponse; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java index 617e168f9a..4405d6e538 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/MediaDrmCallback.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.drm; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import java.util.UUID; @@ -38,10 +39,13 @@ public interface MediaDrmCallback { * Executes a key request. * * @param uuid The UUID of the content protection scheme. - * @param request The request. + * @param request The request generated by the content decryption module. + * @param mediaProvidedLicenseServerUrl A license server URL provided by the media, or null if the + * media does not include any license server URL. * @return The response data. * @throws Exception If an error occurred executing the request. */ - byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception; - + byte[] executeKeyRequest( + UUID uuid, KeyRequest request, @Nullable String mediaProvidedLicenseServerUrl) + throws Exception; } From 5992b310ad0e05232e9b584006b2c5bc862866f5 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Jun 2018 06:50:28 -0700 Subject: [PATCH 050/106] Enable EOS workaround for FireTV Gen 2 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201678261 --- .../mediacodec/MediaCodecRenderer.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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 03a0b66661..48002c7a86 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 @@ -405,7 +405,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); - codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); + codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecInfo); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format); @@ -1272,20 +1272,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Returns whether the decoder is known to handle the propagation of the - * {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device. - *

    - * If true is returned, the renderer will work around the issue by approximating end of stream + * Returns whether the decoder is known to handle the propagation of the {@link + * MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device. + * + *

    If true is returned, the renderer will work around the issue by approximating end of stream * behavior without relying on the flag being propagated through to an output buffer by the * underlying decoder. * - * @param name The name of the decoder. + * @param codecInfo Information about the {@link MediaCodec}. * @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} * propagation incorrectly on the host device. False otherwise. */ - private static boolean codecNeedsEosPropagationWorkaround(String name) { - return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name) - || "OMX.allwinner.video.decoder.avc".equals(name)); + private static boolean codecNeedsEosPropagationWorkaround(MediaCodecInfo codecInfo) { + String name = codecInfo.name; + return (Util.SDK_INT <= 17 + && ("OMX.rk.video_decoder.avc".equals(name) + || "OMX.allwinner.video.decoder.avc".equals(name))) + || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure); } /** From 9b4981df8a6d304c766219b66e50ecce4859d584 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Jun 2018 06:55:44 -0700 Subject: [PATCH 051/106] Add some FireOS workarounds for max input buffer size limitations ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201678686 --- .../video/MediaCodecVideoRenderer.java | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) 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 fe50f26717..990025b5af 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 @@ -459,7 +459,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (areAdaptationCompatible(codecInfo.adaptive, oldFormat, newFormat) && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height - && getMaxInputSize(newFormat) <= codecMaxValues.inputSize) { + && getMaxInputSize(codecInfo, newFormat) <= codecMaxValues.inputSize) { return oldFormat.initializationDataEquals(newFormat) ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION : KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION; @@ -981,7 +981,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { throws DecoderQueryException { int maxWidth = format.width; int maxHeight = format.height; - int maxInputSize = getMaxInputSize(format); + int maxInputSize = getMaxInputSize(codecInfo, format); if (streamFormats.length == 1) { // The single entry in streamFormats must correspond to the format for which the codec is // being configured. @@ -994,7 +994,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); maxWidth = Math.max(maxWidth, streamFormat.width); maxHeight = Math.max(maxHeight, streamFormat.height); - maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); + maxInputSize = Math.max(maxInputSize, getMaxInputSize(codecInfo, streamFormat)); } } if (haveUnknownDimensions) { @@ -1004,7 +1004,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maxWidth = Math.max(maxWidth, codecMaxSize.x); maxHeight = Math.max(maxHeight, codecMaxSize.y); maxInputSize = - Math.max(maxInputSize, getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); + Math.max( + maxInputSize, + getMaxInputSize(codecInfo, format.sampleMimeType, maxWidth, maxHeight)); Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); } } @@ -1053,13 +1055,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns a maximum input buffer size for a given format. + * Returns a maximum input buffer size for a given codec and format. * + * @param codecInfo Information about the {@link MediaCodec} being configured. * @param format The format. * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not * be determined. */ - private static int getMaxInputSize(Format format) { + private static int getMaxInputSize(MediaCodecInfo codecInfo, Format format) { if (format.maxInputSize != Format.NO_VALUE) { // The format defines an explicit maximum input size. Add the total size of initialization // data buffers, as they may need to be queued in the same input buffer as the largest sample. @@ -1072,20 +1075,22 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } else { // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of // initialization data. - return getMaxInputSize(format.sampleMimeType, format.width, format.height); + return getMaxInputSize(codecInfo, format.sampleMimeType, format.width, format.height); } } /** - * Returns a maximum input size for a given mime type, width and height. + * Returns a maximum input size for a given codec, mime type, width and height. * + * @param codecInfo Information about the {@link MediaCodec} being configured. * @param sampleMimeType The format mime type. * @param width The width in pixels. * @param height The height in pixels. * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be * determined. */ - private static int getMaxInputSize(String sampleMimeType, int width, int height) { + private static int getMaxInputSize( + MediaCodecInfo codecInfo, String sampleMimeType, int width, int height) { if (width == Format.NO_VALUE || height == Format.NO_VALUE) { // We can't infer a maximum input size without video dimensions. return Format.NO_VALUE; @@ -1101,9 +1106,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { minCompressionRatio = 2; break; case MimeTypes.VIDEO_H264: - if ("BRAVIA 4K 2015".equals(Util.MODEL)) { - // The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video - // maximum input size, so use the default value. + if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K + || ("Amazon".equals(Util.MANUFACTURER) + && ("KFSOWI".equals(Util.MODEL) // Kindle Soho + || ("AFTS".equals(Util.MODEL) && codecInfo.secure)))) { // Fire TV Gen 2 + // Use the default value for cases where platform limitations may prevent buffers of the + // calculated maximum input size from being allocated. return Format.NO_VALUE; } // Round up width/height to an integer number of macroblocks. From 0a80b47edb83503297e1e629e1db86712b6bfb06 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Jun 2018 07:01:57 -0700 Subject: [PATCH 052/106] Extract scheme specific data from PSSH for some FireOS devices ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201679215 --- .../android/exoplayer2/drm/FrameworkMediaDrm.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 9f30cef0f8..a10c4c612e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -121,11 +121,17 @@ public final class FrameworkMediaDrm implements ExoMediaDrm optionalParameters) throws NotProvisionedException { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - if (Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid)) { + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. Some Amazon + // devices also required data to be extracted from the PSSH atom for PlayReady. + if ((Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid)) + || (C.PLAYREADY_UUID.equals(uuid) + && "Amazon".equals(Util.MANUFACTURER) + && ("AFTB".equals(Util.MODEL) // Fire TV Gen 1 + || "AFTS".equals(Util.MODEL) // Fire TV Gen 2 + || "AFTM".equals(Util.MODEL)))) { // Fire TV Stick Gen 1 byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(init, uuid); if (psshData == null) { - // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. + // Extraction failed. schemeData isn't a PSSH atom, so leave it unchanged. } else { init = psshData; } From a37bd0d66fffb287190139166daa4405b9525ae0 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 22 Jun 2018 09:19:23 -0700 Subject: [PATCH 053/106] Parse ms:laurl from ContentProtection in DASH Issue:#3393 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201694813 --- .../source/dash/manifest/DashManifestParser.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 0a4274e674..a443ca861f 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 @@ -355,6 +355,7 @@ public class DashManifestParser extends DefaultHandler protected Pair parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeType = null; + String licenseServerUrl = null; byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; @@ -389,7 +390,9 @@ public class DashManifestParser extends DefaultHandler do { xpp.next(); - if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { + if (XmlPullParserUtil.isStartTag(xpp, "ms:laurl")) { + licenseServerUrl = xpp.getAttributeValue(null, "licenseUrl"); + } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) { String robustnessLevel = xpp.getAttributeValue(null, "robustness_level"); requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } else if (data == null) { @@ -409,8 +412,11 @@ public class DashManifestParser extends DefaultHandler } } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - SchemeData schemeData = uuid != null - ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; + SchemeData schemeData = + uuid != null + ? new SchemeData( + uuid, licenseServerUrl, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) + : null; return Pair.create(schemeType, schemeData); } From 9c76ba06036b56205232cca4f2e97e748334ee80 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Jun 2018 09:27:37 -0700 Subject: [PATCH 054/106] Wait for DRM keys before codec configuration on FireOS devices ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201695876 --- .../mediacodec/MediaCodecRenderer.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 48002c7a86..b966492d8c 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 @@ -369,6 +369,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer { wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto(); drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); } + if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) { + @DrmSession.State int drmSessionState = drmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { + // Wait for keys. + return; + } + } } if (codecInfo == null) { @@ -1209,6 +1218,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } + /** + * Returns whether the device needs keys to have been loaded into the {@link DrmSession} before + * codec configuration. + */ + private boolean deviceNeedsDrmKeysToConfigureCodecWorkaround() { + return "Amazon".equals(Util.MANUFACTURER) + && ("AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1 + || "AFTB".equals(Util.MODEL)); // Fire TV Gen 1 + } + /** * Returns whether the decoder is known to fail when flushed. *

    From cbfa602866fedaef883ecb0118c14ce46645633a Mon Sep 17 00:00:00 2001 From: Arek Karbowy Date: Wed, 27 Jun 2018 11:29:04 +0100 Subject: [PATCH 055/106] opt out of using DummySurface on specific Fire TV device --- .../exoplayer2/video/MediaCodecVideoRenderer.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 990025b5af..5e8a98ea68 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 @@ -825,10 +825,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) { return Util.SDK_INT >= 23 && !tunneling + && !codecNeedsDummySurfaceWorkaround(codecInfo.name) && !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name) && (!codecInfo.secure || DummySurface.isSecureSupported(context)); } - + private void setJoiningDeadlineMs() { joiningDeadlineMs = allowedJoiningTimeMs > 0 ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; @@ -1171,6 +1172,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); } + private static boolean codecNeedsDummySurfaceWorkaround(String name) { + // Work around https://github.com/google/ExoPlayer/issues/4419. + return (("needle".equals(Util.DEVICE)) // FireTV 4K + && "OMX.amlogic.avc.decoder.awesome".equals(name)); + } + /** * Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} * incorrectly. From 4a8bd911cf98f967ec25605f19f6d92290ff5c1d Mon Sep 17 00:00:00 2001 From: Keith Thompson Date: Wed, 27 Jun 2018 16:19:22 +0100 Subject: [PATCH 056/106] Add withSkippedAd method to AdPlaybackState. Currently it is only possible to skip entire ad groups but not individual ads within a given ad group. --- .../exoplayer2/source/ads/AdPlaybackState.java | 8 ++++++++ .../exoplayer2/source/ads/AdPlaybackStateTest.java | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index 8654e94bdb..246d804c89 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -344,6 +344,14 @@ public final class AdPlaybackState { return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); } + /** Returns an instance with the specified ad marked as skipped. */ + @CheckResult + public AdPlaybackState withSkippedAd(int adGroupIndex, int adIndexInAdGroup) { + AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length); + adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_SKIPPED, adIndexInAdGroup); + return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs); + } + /** Returns an instance with the specified ad marked as having a load error. */ @CheckResult public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java index a8cc04473d..da03df9b8a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java @@ -89,6 +89,19 @@ public final class AdPlaybackStateTest { assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE); } + @Test + public void testGetFirstAdIndexToPlaySkipsSkippedAd() { + state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); + state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI); + state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI); + + state = state.withSkippedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); + + assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(1); + assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE); + assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE); + } + @Test public void testGetFirstAdIndexToPlaySkipsErrorAds() { state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); From fd8751dbc6de30194584e623a047c7ba19b1beb1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 26 Jun 2018 03:03:36 -0700 Subject: [PATCH 057/106] Make ImaAdsLoader robust to calls after it's released Issue: #3879 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202100576 --- RELEASENOTES.md | 2 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 51 +++++++++++-------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 46a1117202..42b9449410 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ * DRM: * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). +* IMA: Fix behavior when creating/releasing the player then releasing + `ImaAdsLoader` ((#3879)[https://github.com/google/ExoPlayer/issues/3879]). ### 2.8.2 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 3256da21dd..9045ee777a 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -267,13 +267,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /** The expected ad group index that IMA should load next. */ private int expectedAdGroupIndex; - /** - * The index of the current ad group that IMA is loading. - */ + /** The index of the current ad group that IMA is loading. */ private int adGroupIndex; - /** - * Whether IMA has sent an ad event to pause content since the last resume content event. - */ + /** Whether IMA has sent an ad event to pause content since the last resume content event. */ private boolean imaPausedContent; /** The current ad playback state. */ private @ImaAdState int imaAdState; @@ -285,9 +281,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Fields tracking the player/loader state. - /** - * Whether the player is playing an ad. - */ + /** Whether the player is playing an ad. */ private boolean playingAd; /** * If the player is playing an ad, stores the ad index in its ad group. {@link C#INDEX_UNSET} @@ -310,13 +304,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * content progress should increase. {@link C#TIME_UNSET} otherwise. */ private long fakeContentProgressOffsetMs; - /** - * Stores the pending content position when a seek operation was intercepted to play an ad. - */ + /** Stores the pending content position when a seek operation was intercepted to play an ad. */ private long pendingContentPositionMs; - /** - * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. - */ + /** Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ private boolean sentPendingContentPositionMs; /** @@ -509,6 +499,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsManager.destroy(); adsManager = null; } + imaPausedContent = false; + imaAdState = IMA_AD_STATE_NONE; + pendingAdLoadError = null; + adPlaybackState = AdPlaybackState.NONE; + updateAdPlaybackState(); } @Override @@ -558,7 +553,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "onAdEvent: " + adEventType); } if (adsManager == null) { - Log.w(TAG, "Dropping ad event after release: " + adEvent); + Log.w(TAG, "Ignoring AdEvent after release: " + adEvent); return; } try { @@ -654,6 +649,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void loadAd(String adUriString) { try { + if (DEBUG) { + Log.d(TAG, "loadAd in ad group " + adGroupIndex); + } + if (adsManager == null) { + Log.w(TAG, "Ignoring loadAd after release"); + return; + } if (adGroupIndex == C.INDEX_UNSET) { Log.w( TAG, @@ -662,9 +664,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adGroupIndex = expectedAdGroupIndex; adsManager.start(); } - if (DEBUG) { - Log.d(TAG, "loadAd in ad group " + adGroupIndex); - } int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); if (adIndexInAdGroup == C.INDEX_UNSET) { Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); @@ -693,6 +692,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { Log.d(TAG, "playAd"); } + if (adsManager == null) { + Log.w(TAG, "Ignoring playAd after release"); + return; + } switch (imaAdState) { case IMA_AD_STATE_PLAYING: // IMA does not always call stopAd before resuming content. @@ -736,6 +739,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { Log.d(TAG, "stopAd"); } + if (adsManager == null) { + Log.w(TAG, "Ignoring stopAd after release"); + return; + } if (player == null) { // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. Log.w(TAG, "Unexpected stopAd while detached"); @@ -1083,6 +1090,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d( TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); } + if (adsManager == null) { + Log.w(TAG, "Ignoring ad prepare error after release"); + return; + } if (imaAdState == IMA_AD_STATE_NONE) { // Send IMA a content position at the ad group so that it will try to play it, at which point // we can notify that it failed to load. @@ -1165,7 +1176,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.e(TAG, message, cause); // We can't recover from an unexpected error in general, so skip all remaining ads. if (adPlaybackState == null) { - adPlaybackState = new AdPlaybackState(); + adPlaybackState = AdPlaybackState.NONE; } else { for (int i = 0; i < adPlaybackState.adGroupCount; i++) { adPlaybackState = adPlaybackState.withSkippedAdGroup(i); From be995f0b274c98efa286121b25cfbcf69fa15af7 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 26 Jun 2018 03:34:19 -0700 Subject: [PATCH 058/106] Rename HlsPlaylistTracker's release to stop ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202103550 --- .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../hls/playlist/DefaultHlsPlaylistTracker.java | 2 +- .../source/hls/playlist/HlsPlaylistTracker.java | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index e0c805e1af..aa57ca24f5 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -401,7 +401,7 @@ public final class HlsMediaSource extends BaseMediaSource @Override public void releaseSourceInternal() { if (playlistTracker != null) { - playlistTracker.release(); + playlistTracker.stop(); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index 014a302de7..7266184750 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -105,7 +105,7 @@ public final class DefaultHlsPlaylistTracker } @Override - public void release() { + public void stop() { primaryHlsUrl = null; primaryUrlSnapshot = null; masterPlaylist = null; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java index febd1c217d..01dce9fcd3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java @@ -100,8 +100,8 @@ public interface HlsPlaylistTracker { /** * Starts the playlist tracker. * - *

    Must be called from the playback thread. A tracker may be restarted after a {@link - * #release()} call. + *

    Must be called from the playback thread. A tracker may be restarted after a {@link #stop()} + * call. * * @param initialPlaylistUri Uri of the HLS stream. Can point to a media playlist or a master * playlist. @@ -111,8 +111,12 @@ public interface HlsPlaylistTracker { void start( Uri initialPlaylistUri, EventDispatcher eventDispatcher, PrimaryPlaylistListener listener); - /** Releases all acquired resources. Must be called once per {@link #start} call. */ - void release(); + /** + * Stops the playlist tracker and releases any acquired resources. + * + *

    Must be called once per {@link #start} call. + */ + void stop(); /** * Registers a listener to receive events from the playlist tracker. From a0810856e71018551a077569e13c2d630adc622e Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 27 Jun 2018 02:47:21 -0700 Subject: [PATCH 059/106] Add DRM workaround for Asus Zenfone 2. Issue: #4413 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202277924 --- RELEASENOTES.md | 4 +++- .../android/exoplayer2/drm/FrameworkMediaDrm.java | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 42b9449410..39d1a78403 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,7 +6,9 @@ * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). * IMA: Fix behavior when creating/releasing the player then releasing - `ImaAdsLoader` ((#3879)[https://github.com/google/ExoPlayer/issues/3879]). + `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). +* Fix issue playing DRM protected streams on Asus Zenfone 2 + ([#4403](https://github.com/google/ExoPlayer/issues/4413)). ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index a10c4c612e..c87e82f972 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -71,6 +71,9 @@ public final class FrameworkMediaDrm implements ExoMediaDrmSee GitHub issue #4413. + */ + private static boolean needsForceL3Workaround() { + return "ASUS_Z00AD".equals(Util.MODEL); + } } From 186711287b0ebd827fa175a0556e8d9b64f38426 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 28 Jun 2018 04:05:43 -0700 Subject: [PATCH 060/106] Add some leeway for finding additional tracks in PsExtractor. Currently we immediately stop searching after we found one video and one audio track. This change adds some leeway to detect additional tracks. Issue:#4406 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202455491 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/ts/PsExtractor.java | 21 +++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 39d1a78403..fb8ca532ec 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,8 @@ `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). * Fix issue playing DRM protected streams on Asus Zenfone 2 ([#4403](https://github.com/google/ExoPlayer/issues/4413)). +* Add support for multiple audio and video tracks in MPEG-PS streams + ([#4406](https://github.com/google/ExoPlayer/issues/4406)). ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index f3aad6ba6b..8acb36b41e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -52,7 +52,12 @@ public final class PsExtractor implements Extractor { private static final int PACKET_START_CODE_PREFIX = 0x000001; private static final int MPEG_PROGRAM_END_CODE = 0x000001B9; private static final int MAX_STREAM_ID_PLUS_ONE = 0x100; + + // Max search length for first audio and video track in input data. private static final long MAX_SEARCH_LENGTH = 1024 * 1024; + // Max search length for additional audio and video tracks in input data after at least one audio + // and video track has been found. + private static final long MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND = 8 * 1024; public static final int PRIVATE_STREAM_1 = 0xBD; public static final int AUDIO_STREAM = 0xC0; @@ -66,6 +71,7 @@ public final class PsExtractor implements Extractor { private boolean foundAllTracks; private boolean foundAudioTrack; private boolean foundVideoTrack; + private long lastTrackPosition; // Accessed only by the loading thread. private ExtractorOutput output; @@ -188,18 +194,21 @@ public final class PsExtractor implements Extractor { if (!foundAllTracks) { if (payloadReader == null) { ElementaryStreamReader elementaryStreamReader = null; - if (!foundAudioTrack && streamId == PRIVATE_STREAM_1) { + if (streamId == PRIVATE_STREAM_1) { // Private stream, used for AC3 audio. // NOTE: This may need further parsing to determine if its DTS, but that's likely only // valid for DVDs. elementaryStreamReader = new Ac3Reader(); foundAudioTrack = true; - } else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { + lastTrackPosition = input.getPosition(); + } else if ((streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) { elementaryStreamReader = new MpegAudioReader(); foundAudioTrack = true; - } else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { + lastTrackPosition = input.getPosition(); + } else if ((streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) { elementaryStreamReader = new H262Reader(); foundVideoTrack = true; + lastTrackPosition = input.getPosition(); } if (elementaryStreamReader != null) { TrackIdGenerator idGenerator = new TrackIdGenerator(streamId, MAX_STREAM_ID_PLUS_ONE); @@ -208,7 +217,11 @@ public final class PsExtractor implements Extractor { psPayloadReaders.put(streamId, payloadReader); } } - if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) { + long maxSearchPosition = + foundAudioTrack && foundVideoTrack + ? lastTrackPosition + MAX_SEARCH_LENGTH_AFTER_AUDIO_AND_VIDEO_FOUND + : MAX_SEARCH_LENGTH; + if (input.getPosition() > maxSearchPosition) { foundAllTracks = true; output.endTracks(); } From 6cdaf593e01483e87b32c04bd14495ed9064b0bf Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 28 Jun 2018 09:54:09 -0700 Subject: [PATCH 061/106] Improve DefaultTrackSelector documentation It's quite hard to find the defaults currently. Placing them on each variable makes them easier to find. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202495929 --- .../trackselection/DefaultTrackSelector.java | 167 ++++++++++-------- 1 file changed, 94 insertions(+), 73 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 3bbb2a7941..e905060bd2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -153,7 +153,8 @@ import java.util.concurrent.atomic.AtomicReference; public class DefaultTrackSelector extends MappingTrackSelector { /** - * A builder for {@link Parameters}. + * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations of + * the parameters that can be configured using this builder. */ public static final class ParametersBuilder { @@ -177,9 +178,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private boolean viewportOrientationMayChange; private int tunnelingAudioSessionId; - /** - * Creates a builder obtaining the initial values from {@link Parameters#DEFAULT}. - */ + /** Creates a builder with default initial values. */ public ParametersBuilder() { this(Parameters.DEFAULT); } @@ -343,15 +342,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Equivalent to invoking {@link #setViewportSize} with the viewport size obtained from - * {@link Util#getPhysicalDisplaySize(Context)}. + * Equivalent to calling {@link #setViewportSize(int, int, boolean)} with the viewport size + * obtained from {@link Util#getPhysicalDisplaySize(Context)}. * - * @param context The context to obtain the viewport size from. - * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. + * @param context Any context. + * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. * @return This builder. */ - public ParametersBuilder setViewportSizeToPhysicalDisplaySize(Context context, - boolean viewportOrientationMayChange) { + public ParametersBuilder setViewportSizeToPhysicalDisplaySize( + Context context, boolean viewportOrientationMayChange) { // Assume the viewport is fullscreen. Point viewportSize = Util.getPhysicalDisplaySize(context); return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); @@ -368,13 +367,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and - * {@link Parameters#viewportOrientationMayChange}. + * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and {@link + * Parameters#viewportOrientationMayChange}. * + * @param viewportWidth See {@link Parameters#viewportWidth}. + * @param viewportHeight See {@link Parameters#viewportHeight}. + * @param viewportOrientationMayChange See {@link Parameters#viewportOrientationMayChange}. * @return This builder. */ - public ParametersBuilder setViewportSize(int viewportWidth, int viewportHeight, - boolean viewportOrientationMayChange) { + public ParametersBuilder setViewportSize( + int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; this.viewportOrientationMayChange = viewportOrientationMayChange; @@ -485,8 +487,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in - * tunneling mode. Session ids can be generated using {@link + * See {@link Parameters#tunnelingAudioSessionId}. + * + *

    Enables or disables tunneling. To enable tunneling, pass an audio session id to use when + * in tunneling mode. Session ids can be generated using {@link * C#generateAudioSessionIdV21(Context)}. To disable tunneling pass {@link * C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and * supported by the audio and video renderers for the selected tracks. @@ -540,25 +544,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** Constraint parameters for {@link DefaultTrackSelector}. */ public static final class Parameters implements Parcelable { - /** - * An instance with default values: - * - *

      - *
    • No preferred audio language. - *
    • No preferred text language. - *
    • Text tracks with undetermined language are not selected if no track with {@link - * #preferredTextLanguage} is available. - *
    • All selection flags are considered for text track selections. - *
    • Lowest bitrate track selections are not forced. - *
    • Adaptation between different mime types is not allowed. - *
    • Non seamless adaptation is allowed. - *
    • No max limit for video width/height. - *
    • No max video bitrate. - *
    • Video constraints are exceeded if no supported selection can be made otherwise. - *
    • Renderer capabilities are exceeded if no supported selection can be made. - *
    • No viewport constraints. - *
    - */ + /** An instance with default values. */ public static final Parameters DEFAULT = new Parameters(); // Per renderer overrides. @@ -568,105 +554,140 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio /** - * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. - * {@code null} selects the default track, or the first track if there's no default. + * The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null} + * selects the default track, or the first track if there's no default. The default value is + * {@code null}. */ public final @Nullable String preferredAudioLanguage; // Text /** * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the - * default track if there is one, or no track otherwise. + * default track if there is one, or no track otherwise. The default value is {@code null}. */ public final @Nullable String preferredTextLanguage; /** - * Whether a text track with undetermined language should be selected if no track with - * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. + * Whether a text track with undetermined language should be selected if no track with {@link + * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The + * default value is {@code false}. */ public final boolean selectUndeterminedTextLanguage; /** * Bitmask of selection flags that are disabled for text track selections. See {@link - * C.SelectionFlags}. + * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags). */ public final int disabledTextTrackSelectionFlags; // Video /** - * Maximum allowed video width. + * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no + * constraint). + * + *

    Note: To restrict adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), it's preferable to use viewport + * constraints ({@link #viewportWidth}, {@link #viewportHeight} and + * {@link #viewportOrientationMayChange}) rather than video size constraints + * ({@link #maxVideoWidth and {@link #maxVideoHeight}). This is because selecting video tracks + * for a given viewport is normally more nuanced than imposing fixed limits on resolution (e.g. + * it's normally preferable to select one format that exceeds the size of the viewport, and to + * take into account the possibility that the orientation of the viewport may change). */ public final int maxVideoWidth; /** - * Maximum allowed video height. + * Maximum allowed video height. The default value is {@link Integer#MAX_VALUE} (i.e. no + * constraint). + * + *

    Note: To restrict adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), it's preferable to use viewport + * constraints ({@link #viewportWidth}, {@link #viewportHeight} and + * {@link #viewportOrientationMayChange}) rather than video size constraints + * ({@link #maxVideoWidth and {@link #maxVideoHeight}). This is because selecting video tracks + * for a given viewport is normally more nuanced than imposing fixed limits on resolution (e.g. + * it's normally preferable to select one format that exceeds the size of the viewport, and to + * take into account the possibility that the orientation of the viewport may change). */ public final int maxVideoHeight; /** - * Maximum video bitrate. + * Maximum video bitrate. The default value is {@link Integer#MAX_VALUE} (i.e. no constraint). */ public final int maxVideoBitrate; /** - * Whether to exceed video constraints when no selection can be made otherwise. + * Whether to exceed the {@link #maxVideoWidth}, {@link #maxVideoHeight} and {@link + * #maxVideoBitrate} constraints when no selection can be made otherwise. The default value is + * {@code true}. */ public final boolean exceedVideoConstraintsIfNecessary; /** - * Viewport width in pixels. Constrains video tracks selections for adaptive playbacks so that - * only tracks suitable for the viewport are selected. + * Viewport width in pixels. Constrains video track selections for adaptive playbacks so that + * only tracks suitable for the viewport are selected. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int viewportWidth; /** - * Viewport height in pixels. Constrains video tracks selections for adaptive playbacks so that - * only tracks suitable for the viewport are selected. + * Viewport height in pixels. Constrains video track selections for adaptive playbacks so that + * only tracks suitable for the viewport are selected. The default value is {@link + * Integer#MAX_VALUE} (i.e. no constraint). */ public final int viewportHeight; /** - * Whether the viewport orientation may change during playback. Constrains video tracks + * Whether the viewport orientation may change during playback. Constrains video track * selections for adaptive playbacks so that only tracks suitable for the viewport are selected. + * The default value is {@code true}. */ public final boolean viewportOrientationMayChange; // General /** * Whether to force selection of the single lowest bitrate audio and video tracks that comply - * with all other constraints. + * with all other constraints. The default value is {@code false}. */ public final boolean forceLowestBitrate; /** - * Whether to allow adaptive selections containing mixed mime types. + * Whether to allow adaptive selections containing mixed mime types. The default value is {@code + * false}. */ public final boolean allowMixedMimeAdaptiveness; /** - * Whether to allow adaptive selections where adaptation may not be completely seamless. + * Whether to allow adaptive selections where adaptation may not be completely seamless. The + * default value is {@code true}. */ public final boolean allowNonSeamlessAdaptiveness; /** - * Whether to exceed renderer capabilities when no selection can be made otherwise. + * Whether to exceed renderer capabilities when no selection can be made otherwise. This + * parameter applies when all of the tracks available for a renderer exceed the renderer's + * reported capabilities. If the parameter is {@code true} then the lowest quality track will + * still be selected. Playback may succeed if the renderer has under-reported its true + * capabilities. If {@code false} then no track will be selected. The default value is {@code + * true}. */ public final boolean exceedRendererCapabilitiesIfNecessary; /** * The audio session id to use when tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling - * is not to be enabled. + * is disabled. The default value is {@link C#AUDIO_SESSION_ID_UNSET} (i.e. tunneling is + * disabled). */ public final int tunnelingAudioSessionId; private Parameters() { this( - new SparseArray>(), - new SparseBooleanArray(), - null, - null, - false, - 0, - false, - false, - true, - Integer.MAX_VALUE, - Integer.MAX_VALUE, - Integer.MAX_VALUE, - true, - true, - Integer.MAX_VALUE, - Integer.MAX_VALUE, - true, - C.AUDIO_SESSION_ID_UNSET); + /* selectionOverrides= */ new SparseArray<>(), + /* rendererDisabledFlags= */ new SparseBooleanArray(), + /* preferredAudioLanguage= */ null, + /* preferredTextLanguage= */ null, + /* selectUndeterminedTextLanguage= */ false, + /* disabledTextTrackSelectionFlags= */ 0, + /* forceLowestBitrate= */ false, + /* allowMixedMimeAdaptiveness= */ false, + /* allowNonSeamlessAdaptiveness= */ true, + /* maxVideoWidth= */ Integer.MAX_VALUE, + /* maxVideoHeight= */ Integer.MAX_VALUE, + /* maxVideoBitrate= */ Integer.MAX_VALUE, + /* exceedVideoConstraintsIfNecessary= */ true, + /* exceedRendererCapabilitiesIfNecessary= */ true, + /* viewportWidth= */ Integer.MAX_VALUE, + /* viewportHeight= */ Integer.MAX_VALUE, + /* viewportOrientationMayChange= */ true, + /* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET); } /* package */ Parameters( From 3c4384ddd974316a23ba313a4cb060ceacf00f97 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 29 Jun 2018 03:20:18 -0700 Subject: [PATCH 062/106] Simplify DefaultTrackSelector documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202623116 --- .../trackselection/DefaultTrackSelector.java | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index e905060bd2..9000ace46d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -583,28 +583,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no * constraint). * - *

    Note: To restrict adaptive video track selections to be suitable for a given viewport (the - * region of the display within which video will be played), it's preferable to use viewport - * constraints ({@link #viewportWidth}, {@link #viewportHeight} and - * {@link #viewportOrientationMayChange}) rather than video size constraints - * ({@link #maxVideoWidth and {@link #maxVideoHeight}). This is because selecting video tracks - * for a given viewport is normally more nuanced than imposing fixed limits on resolution (e.g. - * it's normally preferable to select one format that exceeds the size of the viewport, and to - * take into account the possibility that the orientation of the viewport may change). + *

    To constrain adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), use ({@link #viewportWidth}, {@link + * #viewportHeight} and {@link #viewportOrientationMayChange}) instead. */ public final int maxVideoWidth; /** * Maximum allowed video height. The default value is {@link Integer#MAX_VALUE} (i.e. no * constraint). * - *

    Note: To restrict adaptive video track selections to be suitable for a given viewport (the - * region of the display within which video will be played), it's preferable to use viewport - * constraints ({@link #viewportWidth}, {@link #viewportHeight} and - * {@link #viewportOrientationMayChange}) rather than video size constraints - * ({@link #maxVideoWidth and {@link #maxVideoHeight}). This is because selecting video tracks - * for a given viewport is normally more nuanced than imposing fixed limits on resolution (e.g. - * it's normally preferable to select one format that exceeds the size of the viewport, and to - * take into account the possibility that the orientation of the viewport may change). + *

    To constrain adaptive video track selections to be suitable for a given viewport (the + * region of the display within which video will be played), use ({@link #viewportWidth}, {@link + * #viewportHeight} and {@link #viewportOrientationMayChange}) instead. */ public final int maxVideoHeight; /** @@ -653,12 +643,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean allowNonSeamlessAdaptiveness; /** - * Whether to exceed renderer capabilities when no selection can be made otherwise. This - * parameter applies when all of the tracks available for a renderer exceed the renderer's - * reported capabilities. If the parameter is {@code true} then the lowest quality track will - * still be selected. Playback may succeed if the renderer has under-reported its true - * capabilities. If {@code false} then no track will be selected. The default value is {@code - * true}. + * Whether to exceed renderer capabilities when no selection can be made otherwise. + * + *

    This parameter applies when all of the tracks available for a renderer exceed the + * renderer's reported capabilities. If the parameter is {@code true} then the lowest quality + * track will still be selected. Playback may succeed if the renderer has under-reported its + * true capabilities. If {@code false} then no track will be selected. The default value is + * {@code true}. */ public final boolean exceedRendererCapabilitiesIfNecessary; /** From 165fef878239f8483d4e2d171b47060ac87bb26d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 29 Jun 2018 09:43:49 -0700 Subject: [PATCH 063/106] Fix application of styles for CEA-608 Issue: #4321 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202660712 --- RELEASENOTES.md | 2 + .../exoplayer2/text/cea/Cea608Decoder.java | 222 ++++++++++-------- 2 files changed, 127 insertions(+), 97 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fb8ca532ec..f5171af80e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ * DRM: * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). +* CEA-608: Improve handling of embedded styles + ([#4321](https://github.com/google/ExoPlayer/issues/4321)). * IMA: Fix behavior when creating/releasing the player then releasing `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). * Fix issue playing DRM protected streams on Asus Zenfone 2 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 57614ae880..725321e53f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -21,10 +21,10 @@ import android.text.Layout.Alignment; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; -import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; @@ -55,15 +55,13 @@ public final class Cea608Decoder extends CeaDecoder { private static final int[] ROW_INDICES = new int[] {11, 1, 3, 12, 14, 5, 7, 9}; private static final int[] COLUMN_INDICES = new int[] {0, 4, 8, 12, 16, 20, 24, 28}; - private static final int[] COLORS = new int[] { - Color.WHITE, - Color.GREEN, - Color.BLUE, - Color.CYAN, - Color.RED, - Color.YELLOW, - Color.MAGENTA, - }; + + private static final int[] STYLE_COLORS = + new int[] { + Color.WHITE, Color.GREEN, Color.BLUE, Color.CYAN, Color.RED, Color.YELLOW, Color.MAGENTA + }; + private static final int STYLE_ITALICS = 0x07; + private static final int STYLE_UNCHANGED = 0x08; // The default number of rows to display in roll-up captions mode. private static final int DEFAULT_CAPTIONS_ROW_COUNT = 4; @@ -377,18 +375,10 @@ public final class Cea608Decoder extends CeaDecoder { // A midrow control code advances the cursor. currentCueBuilder.append(' '); - // cc2 - 0|0|1|0|ATRBT|U - // ATRBT is the 3-byte encoded attribute, and U is the underline toggle - boolean isUnderlined = (cc2 & 0x01) == 0x01; - currentCueBuilder.setUnderline(isUnderlined); - - int attribute = (cc2 >> 1) & 0x0F; - if (attribute == 0x07) { - currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2); - currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1); - } else { - currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1); - } + // cc2 - 0|0|1|0|STYLE|U + boolean underline = (cc2 & 0x01) == 0x01; + int style = (cc2 >> 1) & 0x07; + currentCueBuilder.setStyle(style, underline); } private void handlePreambleAddressCode(byte cc1, byte cc2) { @@ -414,22 +404,18 @@ public final class Cea608Decoder extends CeaDecoder { currentCueBuilder.setRow(row); } - if ((cc2 & 0x01) == 0x01) { - currentCueBuilder.setPreambleStyle(new UnderlineSpan()); - } - // cc2 - 0|1|N|0|STYLE|U // cc2 - 0|1|N|1|CURSR|U - int attribute = cc2 >> 1 & 0x0F; - if (attribute <= 0x07) { - if (attribute == 0x07) { - currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC)); - currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE)); - } else { - currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute])); - } - } else { - currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]); + boolean isCursor = (cc2 & 0x10) == 0x10; + boolean underline = (cc2 & 0x01) == 0x01; + int cursorOrStyle = (cc2 >> 1) & 0x07; + + // We need to call setStyle even for the isCursor case, to update the underline bit. + // STYLE_UNCHANGED is used for this case. + currentCueBuilder.setStyle(isCursor ? STYLE_UNCHANGED : cursorOrStyle, underline); + + if (isCursor) { + currentCueBuilder.setIndent(COLUMN_INDICES[cursorOrStyle]); } } @@ -585,44 +571,37 @@ public final class Cea608Decoder extends CeaDecoder { private static class CueBuilder { - private static final int POSITION_UNSET = -1; - // 608 captions define a 15 row by 32 column screen grid. These constants convert from 608 // positions to normalized screen position. private static final int SCREEN_CHARWIDTH = 32; private static final int BASE_ROW = 15; - private final List preambleStyles; - private final List midrowStyles; + private final List cueStyles; private final List rolledUpCaptions; - private final SpannableStringBuilder captionStringBuilder; + private final StringBuilder captionStringBuilder; private int row; private int indent; private int tabOffset; private int captionMode; private int captionRowCount; - private int underlineStartPosition; public CueBuilder(int captionMode, int captionRowCount) { - preambleStyles = new ArrayList<>(); - midrowStyles = new ArrayList<>(); + cueStyles = new ArrayList<>(); rolledUpCaptions = new ArrayList<>(); - captionStringBuilder = new SpannableStringBuilder(); + captionStringBuilder = new StringBuilder(); reset(captionMode); setCaptionRowCount(captionRowCount); } public void reset(int captionMode) { this.captionMode = captionMode; - preambleStyles.clear(); - midrowStyles.clear(); + cueStyles.clear(); rolledUpCaptions.clear(); - captionStringBuilder.clear(); + captionStringBuilder.setLength(0); row = BASE_ROW; indent = 0; tabOffset = 0; - underlineStartPosition = POSITION_UNSET; } public void setCaptionRowCount(int captionRowCount) { @@ -630,7 +609,8 @@ public final class Cea608Decoder extends CeaDecoder { } public boolean isEmpty() { - return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() + return cueStyles.isEmpty() + && rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0; } @@ -638,6 +618,16 @@ public final class Cea608Decoder extends CeaDecoder { int length = captionStringBuilder.length(); if (length > 0) { captionStringBuilder.delete(length - 1, length); + // Decrement style start positions if necessary. + for (int i = cueStyles.size() - 1; i >= 0; i--) { + CueStyle style = cueStyles.get(i); + if (style.start == length) { + style.start--; + } else { + // All earlier cues must have style.start < length. + break; + } + } } } @@ -651,11 +641,8 @@ public final class Cea608Decoder extends CeaDecoder { public void rollUp() { rolledUpCaptions.add(buildSpannableString()); - captionStringBuilder.clear(); - preambleStyles.clear(); - midrowStyles.clear(); - underlineStartPosition = POSITION_UNSET; - + captionStringBuilder.setLength(0); + cueStyles.clear(); int numRows = Math.min(captionRowCount, row); while (rolledUpCaptions.size() >= numRows) { rolledUpCaptions.remove(0); @@ -670,23 +657,8 @@ public final class Cea608Decoder extends CeaDecoder { tabOffset = tabs; } - public void setPreambleStyle(CharacterStyle style) { - preambleStyles.add(style); - } - - public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) { - midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement)); - } - - public void setUnderline(boolean enabled) { - if (enabled) { - underlineStartPosition = captionStringBuilder.length(); - } else if (underlineStartPosition != POSITION_UNSET) { - // underline spans won't overlap, so it's safe to modify the builder directly with them - captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, - captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - underlineStartPosition = POSITION_UNSET; - } + public void setStyle(int style, boolean underline) { + cueStyles.add(new CueStyle(style, underline, captionStringBuilder.length())); } public void append(char text) { @@ -694,31 +666,69 @@ public final class Cea608Decoder extends CeaDecoder { } public SpannableString buildSpannableString() { - int length = captionStringBuilder.length(); + SpannableStringBuilder builder = new SpannableStringBuilder(captionStringBuilder); + int length = builder.length(); - // preamble styles apply to the entire cue - for (int i = 0; i < preambleStyles.size(); i++) { - captionStringBuilder.setSpan(preambleStyles.get(i), 0, length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + int underlineStartPosition = C.INDEX_UNSET; + int italicStartPosition = C.INDEX_UNSET; + int colorStartPosition = 0; + int color = Color.WHITE; + + boolean nextItalic = false; + int nextColor = Color.WHITE; + + for (int i = 0; i < cueStyles.size(); i++) { + CueStyle cueStyle = cueStyles.get(i); + boolean underline = cueStyle.underline; + int style = cueStyle.style; + if (style != STYLE_UNCHANGED) { + // If the style is a color then italic is cleared. + nextItalic = style == STYLE_ITALICS; + // If the style is italic then the color is left unchanged. + nextColor = style == STYLE_ITALICS ? nextColor : STYLE_COLORS[style]; + } + + int position = cueStyle.start; + int nextPosition = (i + 1) < cueStyles.size() ? cueStyles.get(i + 1).start : length; + if (position == nextPosition) { + // There are more cueStyles to process at the current position. + continue; + } + + // Process changes to underline up to the current position. + if (underlineStartPosition != C.INDEX_UNSET && !underline) { + setUnderlineSpan(builder, underlineStartPosition, position); + underlineStartPosition = C.INDEX_UNSET; + } else if (underlineStartPosition == C.INDEX_UNSET && underline) { + underlineStartPosition = position; + } + // Process changes to italic up to the current position. + if (italicStartPosition != C.INDEX_UNSET && !nextItalic) { + setItalicSpan(builder, italicStartPosition, position); + italicStartPosition = C.INDEX_UNSET; + } else if (italicStartPosition == C.INDEX_UNSET && nextItalic) { + italicStartPosition = position; + } + // Process changes to color up to the current position. + if (nextColor != color) { + setColorSpan(builder, colorStartPosition, position, color); + color = nextColor; + colorStartPosition = position; + } } - // midrow styles only apply to part of the cue, and after preamble styles - for (int i = 0; i < midrowStyles.size(); i++) { - CueStyle cueStyle = midrowStyles.get(i); - int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement) - ? midrowStyles.get(i + cueStyle.nextStyleIncrement).start - : length; - captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + // Add any final spans. + if (underlineStartPosition != C.INDEX_UNSET && underlineStartPosition != length) { + setUnderlineSpan(builder, underlineStartPosition, length); + } + if (italicStartPosition != C.INDEX_UNSET && italicStartPosition != length) { + setItalicSpan(builder, italicStartPosition, length); + } + if (colorStartPosition != length) { + setColorSpan(builder, colorStartPosition, length, color); } - // special case for midrow underlines that went to the end of the cue - if (underlineStartPosition != POSITION_UNSET) { - captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - return new SpannableString(captionStringBuilder); + return new SpannableString(builder); } public Cue build() { @@ -788,16 +798,34 @@ public final class Cea608Decoder extends CeaDecoder { return captionStringBuilder.toString(); } + private static void setUnderlineSpan(SpannableStringBuilder builder, int start, int end) { + builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static void setItalicSpan(SpannableStringBuilder builder, int start, int end) { + builder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static void setColorSpan( + SpannableStringBuilder builder, int start, int end, int color) { + if (color == Color.WHITE) { + // White is treated as the default color (i.e. no span is attached). + return; + } + builder.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + private static class CueStyle { - public final CharacterStyle style; - public final int start; - public final int nextStyleIncrement; + public final int style; + public final boolean underline; - public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) { + public int start; + + public CueStyle(int style, boolean underline, int start) { this.style = style; + this.underline = underline; this.start = start; - this.nextStyleIncrement = nextStyleIncrement; } } From 1f188c7c0063c374e18d445b48415df533100442 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Jul 2018 01:38:26 -0700 Subject: [PATCH 064/106] Exclude text streams from duration calculations Issue: #4029 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202912333 --- RELEASENOTES.md | 2 ++ .../source/dash/DashMediaSource.java | 20 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f5171af80e..3df15654e0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### 2.8.3 ### +* DASH: Exclude text streams from duration calculations + ([#4029](https://github.com/google/ExoPlayer/issues/4029)). * DRM: * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 7b854e9d29..66b933f234 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispat import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; @@ -974,8 +975,25 @@ public final class DashMediaSource extends BaseMediaSource { long availableEndTimeUs = Long.MAX_VALUE; boolean isIndexExplicit = false; boolean seenEmptyIndex = false; + + boolean haveAudioVideoAdaptationSets = false; for (int i = 0; i < adaptationSetCount; i++) { - DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex(); + int type = period.adaptationSets.get(i).type; + if (type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO) { + haveAudioVideoAdaptationSets = true; + break; + } + } + + for (int i = 0; i < adaptationSetCount; i++) { + AdaptationSet adaptationSet = period.adaptationSets.get(i); + // Exclude text adaptation sets from duration calculations, if we have at least one audio + // or video adaptation set. See: https://github.com/google/ExoPlayer/issues/4029 + if (haveAudioVideoAdaptationSets && adaptationSet.type == C.TRACK_TYPE_TEXT) { + continue; + } + + DashSegmentIndex index = adaptationSet.representations.get(0).getIndex(); if (index == null) { return new PeriodSeekInfo(true, 0, durationUs); } From a50d31a70b975d16bd863ded771451b6157675ad Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 6 Jul 2018 08:32:38 -0700 Subject: [PATCH 065/106] Add workaround for unmatched track indices in trex and tkhd. Both boxes should contain the same list of track indices. However, if only one track index in each list does not match, we can just assume that these belong together. Issue:#4477 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203481258 --- RELEASENOTES.md | 3 +++ .../extractor/mp4/FragmentedMp4Extractor.java | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3df15654e0..93c5c344bc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,9 @@ ([#4403](https://github.com/google/ExoPlayer/issues/4413)). * Add support for multiple audio and video tracks in MPEG-PS streams ([#4406](https://github.com/google/ExoPlayer/issues/4406)). +* Add workaround for track index mismatches between trex and tkhd boxes in + fragmented MP4 files + ([#4477](https://github.com/google/ExoPlayer/issues/4477)). ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index 0bf42f1839..a61b41dea2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -499,7 +499,7 @@ public final class FragmentedMp4Extractor implements Extractor { for (int i = 0; i < trackCount; i++) { Track track = tracks.valueAt(i); TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type)); - trackBundle.init(track, defaultSampleValuesArray.get(track.id)); + trackBundle.init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); trackBundles.put(track.id, trackBundle); durationUs = Math.max(durationUs, track.durationUs); } @@ -509,11 +509,23 @@ public final class FragmentedMp4Extractor implements Extractor { Assertions.checkState(trackBundles.size() == trackCount); for (int i = 0; i < trackCount; i++) { Track track = tracks.valueAt(i); - trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id)); + trackBundles + .get(track.id) + .init(track, getDefaultSampleValues(defaultSampleValuesArray, track.id)); } } } + private DefaultSampleValues getDefaultSampleValues( + SparseArray defaultSampleValuesArray, int trackId) { + if (defaultSampleValuesArray.size() == 1) { + // Ignore track id if there is only one track to cope with non-matching track indices. + // See https://github.com/google/ExoPlayer/issues/4477. + return defaultSampleValuesArray.valueAt(/* index= */ 0); + } + return Assertions.checkNotNull(defaultSampleValuesArray.get(trackId)); + } + private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { parseMoof(moof, trackBundles, flags, extendedTypeScratch); // If drm init data is sideloaded, we ignore pssh boxes. From 6ad98405a331e31f3cc673204ef96623399e2be4 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 6 Jul 2018 08:36:14 -0700 Subject: [PATCH 066/106] Avoid providing invalid responses to MediaDrm MediaDrm.provideXResponse methods only accept the response corresponding to the most recent MediaDrm.getXRequest call. Previously, our code allowed the following incorrect call sequence: a = getKeyRequest b = getKeyRequest provideKeyResponse(responseFor(a)); This would occur in the edge case of a second key request being triggered whilst the first was still in flight. The provideKeyResponse call would then fail. This change fixes the problem by treating responseFor(a) as stale. Note that a slightly better fix would be to defer calling getKeyRequest the second time until after processing the response corresponding to the first one, however this is significantly harder to implement, and is probably not worth it for what should be an edge case. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203481685 --- .../exoplayer2/drm/DefaultDrmSession.java | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index fbbbcbc9ef..c4be283548 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -97,6 +97,9 @@ import java.util.UUID; private byte[] sessionId; private byte[] offlineLicenseKeySetId; + private Object currentKeyRequest; + private Object currentProvisionRequest; + /** * Instantiates a new DRM session. * @@ -171,6 +174,8 @@ import java.util.UUID; requestHandlerThread = null; mediaCrypto = null; lastException = null; + currentKeyRequest = null; + currentProvisionRequest = null; if (sessionId != null) { mediaDrm.closeSession(sessionId); sessionId = null; @@ -215,8 +220,8 @@ import java.util.UUID; // Provisioning implementation. public void provision() { - ProvisionRequest request = mediaDrm.getProvisionRequest(); - postRequestHandler.obtainMessage(MSG_PROVISION, request, true).sendToTarget(); + currentProvisionRequest = mediaDrm.getProvisionRequest(); + postRequestHandler.post(MSG_PROVISION, currentProvisionRequest, /* allowRetry= */ true); } public void onProvisionCompleted() { @@ -289,11 +294,12 @@ import java.util.UUID; return false; } - private void onProvisionResponse(Object response) { - if (state != STATE_OPENING && !isOpen()) { + private void onProvisionResponse(Object request, Object response) { + if (request != currentProvisionRequest || (state != STATE_OPENING && !isOpen())) { // This event is stale. return; } + currentProvisionRequest = null; if (response instanceof Exception) { provisioningManager.onProvisionError((Exception) response); @@ -383,20 +389,21 @@ import java.util.UUID; licenseServerUrl = schemeData.licenseServerUrl; } try { - KeyRequest request = + KeyRequest mediaDrmKeyRequest = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); - Pair arguments = Pair.create(request, licenseServerUrl); - postRequestHandler.obtainMessage(MSG_KEYS, arguments, allowRetry).sendToTarget(); + currentKeyRequest = Pair.create(mediaDrmKeyRequest, licenseServerUrl); + postRequestHandler.post(MSG_KEYS, currentKeyRequest, allowRetry); } catch (Exception e) { onKeysError(e); } } - private void onKeyResponse(Object response) { - if (!isOpen()) { + private void onKeyResponse(Object request, Object response) { + if (request != currentKeyRequest || !isOpen()) { // This event is stale. return; } + currentKeyRequest = null; if (response instanceof Exception) { onKeysError((Exception) response); @@ -461,12 +468,15 @@ import java.util.UUID; @Override public void handleMessage(Message msg) { + Pair requestAndResponse = (Pair) msg.obj; + Object request = requestAndResponse.first; + Object response = requestAndResponse.second; switch (msg.what) { case MSG_PROVISION: - onProvisionResponse(msg.obj); + onProvisionResponse(request, response); break; case MSG_KEYS: - onKeyResponse(msg.obj); + onKeyResponse(request, response); break; default: break; @@ -483,23 +493,27 @@ import java.util.UUID; super(backgroundLooper); } - Message obtainMessage(int what, Object object, boolean allowRetry) { - return obtainMessage(what, allowRetry ? 1 : 0 /* allow retry*/, 0 /* error count */, - object); + void post(int what, Object request, boolean allowRetry) { + int allowRetryInt = allowRetry ? 1 : 0; + int errorCount = 0; + obtainMessage(what, allowRetryInt, errorCount, request).sendToTarget(); } @Override @SuppressWarnings("unchecked") public void handleMessage(Message msg) { + Object request = msg.obj; Object response; try { switch (msg.what) { case MSG_PROVISION: - response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); + response = callback.executeProvisionRequest(uuid, (ProvisionRequest) request); break; case MSG_KEYS: - Pair arguments = (Pair) msg.obj; - response = callback.executeKeyRequest(uuid, arguments.first, arguments.second); + Pair keyRequest = (Pair) request; + KeyRequest mediaDrmKeyRequest = keyRequest.first; + String licenseServerUrl = keyRequest.second; + response = callback.executeKeyRequest(uuid, mediaDrmKeyRequest, licenseServerUrl); break; default: throw new RuntimeException(); @@ -510,7 +524,7 @@ import java.util.UUID; } response = e; } - postResponseHandler.obtainMessage(msg.what, response).sendToTarget(); + postResponseHandler.obtainMessage(msg.what, Pair.create(request, response)).sendToTarget(); } private boolean maybeRetryRequest(Message originalMsg) { From f2f149adca2bd9133e9d973d33de2efee0409c2d Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 6 Jul 2018 09:16:31 -0700 Subject: [PATCH 067/106] Add workaround for unmatched track indices in tkhd and tfhd. If there is only one track, we can assume that both boxes refer to the same track even if the track indices don't match. Issue:#4083 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203485872 --- RELEASENOTES.md | 3 +++ .../extractor/mp4/FragmentedMp4Extractor.java | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 93c5c344bc..70fd33db6d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -18,6 +18,9 @@ * Add workaround for track index mismatches between trex and tkhd boxes in fragmented MP4 files ([#4477](https://github.com/google/ExoPlayer/issues/4477)). +* Add workaround for track index mismatches between tfhd and tkhd boxes in + fragmented MP4 files + ([#4083](https://github.com/google/ExoPlayer/issues/4083)). ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java index a61b41dea2..74181f8898 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java @@ -654,7 +654,7 @@ public final class FragmentedMp4Extractor implements Extractor { private static void parseTraf(ContainerAtom traf, SparseArray trackBundleArray, @Flags int flags, byte[] extendedTypeScratch) throws ParserException { LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd); - TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray, flags); + TrackBundle trackBundle = parseTfhd(tfhd.data, trackBundleArray); if (trackBundle == null) { return; } @@ -805,13 +805,13 @@ public final class FragmentedMp4Extractor implements Extractor { * @return The {@link TrackBundle} to which the {@link TrackFragment} belongs, or null if the tfhd * does not refer to any {@link TrackBundle}. */ - private static TrackBundle parseTfhd(ParsableByteArray tfhd, - SparseArray trackBundles, int flags) { + private static TrackBundle parseTfhd( + ParsableByteArray tfhd, SparseArray trackBundles) { tfhd.setPosition(Atom.HEADER_SIZE); int fullAtom = tfhd.readInt(); int atomFlags = Atom.parseFullAtomFlags(fullAtom); int trackId = tfhd.readInt(); - TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0); + TrackBundle trackBundle = getTrackBundle(trackBundles, trackId); if (trackBundle == null) { return null; } @@ -836,6 +836,17 @@ public final class FragmentedMp4Extractor implements Extractor { return trackBundle; } + private static @Nullable TrackBundle getTrackBundle( + SparseArray trackBundles, int trackId) { + if (trackBundles.size() == 1) { + // Ignore track id if there is only one track. This is either because we have a side-loaded + // track (flag FLAG_SIDELOADED) or to cope with non-matching track indices (see + // https://github.com/google/ExoPlayer/issues/4083). + return trackBundles.valueAt(/* index= */ 0); + } + return trackBundles.get(trackId); + } + /** * Parses a tfdt atom (defined in 14496-12). * From 7207665ce0bc24bf5d3f6e56fc13b8db153dbc94 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sat, 7 Jul 2018 12:28:58 -0400 Subject: [PATCH 068/106] flip flag values to their proper names so that trackselector parameters can be useful --- .../android/exoplayer2/extractor/mkv/MatroskaExtractor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1049554f7a..82e4a6ff46 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 @@ -616,10 +616,10 @@ public final class MatroskaExtractor implements Extractor { currentTrack.number = (int) value; break; case ID_FLAG_DEFAULT: - currentTrack.flagForced = value == 1; + currentTrack.flagDefault = value == 1; break; case ID_FLAG_FORCED: - currentTrack.flagDefault = value == 1; + currentTrack.flagForced = value == 1; break; case ID_TRACK_TYPE: currentTrack.type = (int) value; From d49c5a476db87ae38533fd18d9cd5d49b777453f Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Jul 2018 01:52:35 -0700 Subject: [PATCH 069/106] Tweak DefaultTrackSelector documentation Viewport constraints apply to adaptive content even if the actual playback isn't adaptive (i.e. because the selector ends up making a fixed track selection). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203726831 --- .../trackselection/DefaultTrackSelector.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 9000ace46d..31b79cdd3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -608,20 +608,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean exceedVideoConstraintsIfNecessary; /** - * Viewport width in pixels. Constrains video track selections for adaptive playbacks so that - * only tracks suitable for the viewport are selected. The default value is {@link - * Integer#MAX_VALUE} (i.e. no constraint). + * Viewport width in pixels. Constrains video track selections for adaptive content so that only + * tracks suitable for the viewport are selected. The default value is {@link Integer#MAX_VALUE} + * (i.e. no constraint). */ public final int viewportWidth; /** - * Viewport height in pixels. Constrains video track selections for adaptive playbacks so that + * Viewport height in pixels. Constrains video track selections for adaptive content so that * only tracks suitable for the viewport are selected. The default value is {@link * Integer#MAX_VALUE} (i.e. no constraint). */ public final int viewportHeight; /** * Whether the viewport orientation may change during playback. Constrains video track - * selections for adaptive playbacks so that only tracks suitable for the viewport are selected. + * selections for adaptive content so that only tracks suitable for the viewport are selected. * The default value is {@code true}. */ public final boolean viewportOrientationMayChange; From 522adc3772e8a1397941fffc054885b92d7f2ef3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 9 Jul 2018 05:44:39 -0700 Subject: [PATCH 070/106] Restructure track selection in DashMediaPeriod. Until now, the streams were released and re-enabled for each type of stream (primary, event, embedded) in that order. That leads to problems when replacing streams from one type to another (for example embedded to primary). This change restructures the track selection to: 1. Release and reset all streams that need to be released or replaced. 1(a). Including embedded orphan streams. 2. Select new streams. Issue:#4477 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203751233 --- RELEASENOTES.md | 2 + .../source/dash/DashMediaPeriod.java | 255 ++++++++++-------- 2 files changed, 139 insertions(+), 118 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 70fd33db6d..86ddada1f0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,8 @@ * Add workaround for track index mismatches between tfhd and tkhd boxes in fragmented MP4 files ([#4083](https://github.com/google/ExoPlayer/issues/4083)). +* Fix issue when switching track selection from an embedded track to a primary + track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)). ### 2.8.2 ### diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index f80ff89fc1..dd41db29e9 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source.dash; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Pair; -import android.util.SparseArray; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -186,126 +185,34 @@ import java.util.List; @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - SparseArray> primarySampleStreams = new SparseArray<>(); - List eventSampleStreamList = new ArrayList<>(); + int[] streamIndexToTrackGroupIndex = getStreamIndexToTrackGroupIndex(selections); + releaseDisabledStreams(selections, mayRetainStreamFlags, streams); + releaseOrphanEmbeddedStreams(selections, streams, streamIndexToTrackGroupIndex); + selectNewStreams( + selections, streams, streamResetFlags, positionUs, streamIndexToTrackGroupIndex); - selectPrimarySampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs, primarySampleStreams); - selectEventSampleStreams(selections, mayRetainStreamFlags, streams, - streamResetFlags, eventSampleStreamList); - selectEmbeddedSampleStreams(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs, primarySampleStreams); - - sampleStreams = newSampleStreamArray(primarySampleStreams.size()); - for (int i = 0; i < sampleStreams.length; i++) { - sampleStreams[i] = primarySampleStreams.valueAt(i); + ArrayList> sampleStreamList = new ArrayList<>(); + ArrayList eventSampleStreamList = new ArrayList<>(); + for (SampleStream sampleStream : sampleStreams) { + if (sampleStream instanceof ChunkSampleStream) { + @SuppressWarnings("unchecked") + ChunkSampleStream stream = + (ChunkSampleStream) sampleStream; + sampleStreamList.add(stream); + } else if (sampleStream instanceof EventSampleStream) { + eventSampleStreamList.add((EventSampleStream) sampleStream); + } } + sampleStreams = newSampleStreamArray(sampleStreamList.size()); + sampleStreamList.toArray(sampleStreams); eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()]; eventSampleStreamList.toArray(eventSampleStreams); + compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); return positionUs; } - private void selectPrimarySampleStreams( - TrackSelection[] selections, - boolean[] mayRetainStreamFlags, - SampleStream[] streams, - boolean[] streamResetFlags, - long positionUs, - SparseArray> primarySampleStreams) { - for (int i = 0; i < selections.length; i++) { - if (streams[i] instanceof ChunkSampleStream) { - @SuppressWarnings("unchecked") - ChunkSampleStream stream = (ChunkSampleStream) streams[i]; - if (selections[i] == null || !mayRetainStreamFlags[i]) { - stream.release(this); - streams[i] = null; - } else { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - primarySampleStreams.put(trackGroupIndex, stream); - } - } - - if (streams[i] == null && selections[i] != null) { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { - ChunkSampleStream stream = buildSampleStream(trackGroupInfo, - selections[i], positionUs); - primarySampleStreams.put(trackGroupIndex, stream); - streams[i] = stream; - streamResetFlags[i] = true; - } - } - } - } - - private void selectEventSampleStreams(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, - List eventSampleStreamsList) { - for (int i = 0; i < selections.length; i++) { - if (streams[i] instanceof EventSampleStream) { - EventSampleStream stream = (EventSampleStream) streams[i]; - if (selections[i] == null || !mayRetainStreamFlags[i]) { - streams[i] = null; - } else { - eventSampleStreamsList.add(stream); - } - } - - if (streams[i] == null && selections[i] != null) { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { - EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); - Format format = selections[i].getTrackGroup().getFormat(0); - EventSampleStream stream = new EventSampleStream(eventStream, format, manifest.dynamic); - streams[i] = stream; - streamResetFlags[i] = true; - eventSampleStreamsList.add(stream); - } - } - } - } - - private void selectEmbeddedSampleStreams( - TrackSelection[] selections, - boolean[] mayRetainStreamFlags, - SampleStream[] streams, - boolean[] streamResetFlags, - long positionUs, - SparseArray> primarySampleStreams) { - for (int i = 0; i < selections.length; i++) { - if ((streams[i] instanceof EmbeddedSampleStream || streams[i] instanceof EmptySampleStream) - && (selections[i] == null || !mayRetainStreamFlags[i])) { - // The stream is for an embedded track and is either no longer selected or needs replacing. - releaseIfEmbeddedSampleStream(streams[i]); - streams[i] = null; - } - // We need to consider replacing the stream even if it's non-null because the primary stream - // may have been replaced, selected or deselected. - if (selections[i] != null) { - int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); - TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; - if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) { - ChunkSampleStream primaryStream = primarySampleStreams.get( - trackGroupInfo.primaryTrackGroupIndex); - SampleStream stream = streams[i]; - boolean mayRetainStream = primaryStream == null ? stream instanceof EmptySampleStream - : (stream instanceof EmbeddedSampleStream - && ((EmbeddedSampleStream) stream).parent == primaryStream); - if (!mayRetainStream) { - releaseIfEmbeddedSampleStream(stream); - streams[i] = primaryStream == null ? new EmptySampleStream() - : primaryStream.selectEmbeddedTrack(positionUs, trackGroupInfo.trackType); - streamResetFlags[i] = true; - } - } - } - } - } - @Override public void discardBuffer(long positionUs, boolean toKeyframe) { for (ChunkSampleStream sampleStream : sampleStreams) { @@ -372,6 +279,124 @@ import java.util.List; // Internal methods. + private int[] getStreamIndexToTrackGroupIndex(TrackSelection[] selections) { + int[] streamIndexToTrackGroupIndex = new int[selections.length]; + for (int i = 0; i < selections.length; i++) { + if (selections[i] != null) { + streamIndexToTrackGroupIndex[i] = trackGroups.indexOf(selections[i].getTrackGroup()); + } else { + streamIndexToTrackGroupIndex[i] = C.INDEX_UNSET; + } + } + return streamIndexToTrackGroupIndex; + } + + private void releaseDisabledStreams( + TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams) { + for (int i = 0; i < selections.length; i++) { + if (selections[i] == null || !mayRetainStreamFlags[i]) { + if (streams[i] instanceof ChunkSampleStream) { + @SuppressWarnings("unchecked") + ChunkSampleStream stream = + (ChunkSampleStream) streams[i]; + stream.release(this); + } else if (streams[i] instanceof EmbeddedSampleStream) { + ((EmbeddedSampleStream) streams[i]).release(); + } + streams[i] = null; + } + } + } + + private void releaseOrphanEmbeddedStreams( + TrackSelection[] selections, SampleStream[] streams, int[] streamIndexToTrackGroupIndex) { + for (int i = 0; i < selections.length; i++) { + if (streams[i] instanceof EmptySampleStream || streams[i] instanceof EmbeddedSampleStream) { + // We need to release an embedded stream if the corresponding primary stream is released. + int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex); + boolean mayRetainStream; + if (primaryStreamIndex == C.INDEX_UNSET) { + // If the corresponding primary stream is not selected, we may retain an existing + // EmptySampleStream. + mayRetainStream = streams[i] instanceof EmptySampleStream; + } else { + // If the corresponding primary stream is selected, we may retain the embedded stream if + // the stream's parent still matches. + mayRetainStream = + (streams[i] instanceof EmbeddedSampleStream) + && ((EmbeddedSampleStream) streams[i]).parent == streams[primaryStreamIndex]; + } + if (!mayRetainStream) { + if (streams[i] instanceof EmbeddedSampleStream) { + ((EmbeddedSampleStream) streams[i]).release(); + } + streams[i] = null; + } + } + } + } + + private void selectNewStreams( + TrackSelection[] selections, + SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs, + int[] streamIndexToTrackGroupIndex) { + // Create newly selected primary and event streams. + for (int i = 0; i < selections.length; i++) { + if (streams[i] == null && selections[i] != null) { + streamResetFlags[i] = true; + int trackGroupIndex = streamIndexToTrackGroupIndex[i]; + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_PRIMARY) { + streams[i] = buildSampleStream(trackGroupInfo, selections[i], positionUs); + } else if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_MANIFEST_EVENTS) { + EventStream eventStream = eventStreams.get(trackGroupInfo.eventStreamGroupIndex); + Format format = selections[i].getTrackGroup().getFormat(0); + streams[i] = new EventSampleStream(eventStream, format, manifest.dynamic); + } + } + } + // Create newly selected embedded streams from the corresponding primary stream. Note that this + // second pass is needed because the primary stream may not have been created yet in a first + // pass if the index of the primary stream is greater than the index of the embedded stream. + for (int i = 0; i < selections.length; i++) { + if (streams[i] == null && selections[i] != null) { + int trackGroupIndex = streamIndexToTrackGroupIndex[i]; + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (trackGroupInfo.trackGroupCategory == TrackGroupInfo.CATEGORY_EMBEDDED) { + int primaryStreamIndex = getPrimaryStreamIndex(i, streamIndexToTrackGroupIndex); + if (primaryStreamIndex == C.INDEX_UNSET) { + // If an embedded track is selected without the corresponding primary track, create an + // empty sample stream instead. + streams[i] = new EmptySampleStream(); + } else { + streams[i] = + ((ChunkSampleStream) streams[primaryStreamIndex]) + .selectEmbeddedTrack(positionUs, trackGroupInfo.trackType); + } + } + } + } + } + + private int getPrimaryStreamIndex(int embeddedStreamIndex, int[] streamIndexToTrackGroupIndex) { + int embeddedTrackGroupIndex = streamIndexToTrackGroupIndex[embeddedStreamIndex]; + if (embeddedTrackGroupIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + int primaryTrackGroupIndex = trackGroupInfos[embeddedTrackGroupIndex].primaryTrackGroupIndex; + for (int i = 0; i < streamIndexToTrackGroupIndex.length; i++) { + int trackGroupIndex = streamIndexToTrackGroupIndex[i]; + if (trackGroupIndex == primaryTrackGroupIndex + && trackGroupInfos[trackGroupIndex].trackGroupCategory + == TrackGroupInfo.CATEGORY_PRIMARY) { + return i; + } + } + return C.INDEX_UNSET; + } + private static Pair buildTrackGroups( List adaptationSets, List eventStreams) { int[][] groupedAdaptationSetIndices = getGroupedAdaptationSetIndices(adaptationSets); @@ -624,12 +649,6 @@ import java.util.List; return new ChunkSampleStream[length]; } - private static void releaseIfEmbeddedSampleStream(SampleStream sampleStream) { - if (sampleStream instanceof EmbeddedSampleStream) { - ((EmbeddedSampleStream) sampleStream).release(); - } - } - private static final class TrackGroupInfo { @Retention(RetentionPolicy.SOURCE) From 7e9e29d8a87557acdbf5fe52c5445761c48af900 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 9 Jul 2018 08:07:39 -0700 Subject: [PATCH 071/106] Fix wrong loop variable in DashMediaPeriod. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203766579 --- .../google/android/exoplayer2/source/dash/DashMediaPeriod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index dd41db29e9..ddb655fa92 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -193,7 +193,7 @@ import java.util.List; ArrayList> sampleStreamList = new ArrayList<>(); ArrayList eventSampleStreamList = new ArrayList<>(); - for (SampleStream sampleStream : sampleStreams) { + for (SampleStream sampleStream : streams) { if (sampleStream instanceof ChunkSampleStream) { @SuppressWarnings("unchecked") ChunkSampleStream stream = From 44c45fd18db8345d389ed3335aaffd69b658dcf0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 9 Jul 2018 08:17:36 -0700 Subject: [PATCH 072/106] Move subsampleOffset in Format It's no longer text specific (it's used for metadata as well, and in theory could apply to any stream in which samples contain multiple sub-samples) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203767825 --- .../com/google/android/exoplayer2/Format.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 61d416da09..1614e53164 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -80,6 +80,13 @@ public final class Format implements Parcelable { /** DRM initialization data if the stream is protected, or null otherwise. */ public final @Nullable DrmInitData drmInitData; + /** + * For samples that contain subsamples, this is an offset that should be added to subsample + * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are + * relative to the timestamps of their parent samples. + */ + public final long subsampleOffsetUs; + // Video specific. /** @@ -141,15 +148,6 @@ public final class Format implements Parcelable { */ public final int encoderPadding; - // Text specific. - - /** - * For samples that contain subsamples, this is an offset that should be added to subsample - * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are - * relative to the timestamps of their parent samples. - */ - public final long subsampleOffsetUs; - // Audio and text specific. /** From 2f6273c9fcc2fcafe49a6404ab658042e13dfe17 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 10 Jul 2018 05:00:34 -0700 Subject: [PATCH 073/106] Fix DownloadService doesn't stop when the app is killed Also fixed showing "remove notification" when download is completed. Issue:#4469 Issue:#4488 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=203927268 --- .../exoplayer2/offline/DownloadService.java | 21 ++++++++++++++++-- .../ui/DownloadNotificationUtil.java | 22 ++++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 6dae3f70b3..995e71a94a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -86,6 +86,7 @@ public abstract class DownloadService extends Service { private DownloadManagerListener downloadManagerListener; private int lastStartId; private boolean startedInForeground; + private boolean taskRemoved; /** * Creates a DownloadService with {@link #DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL}. @@ -219,12 +220,17 @@ public abstract class DownloadService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { lastStartId = startId; + taskRemoved = false; String intentAction = null; if (intent != null) { intentAction = intent.getAction(); startedInForeground |= intent.getBooleanExtra(KEY_FOREGROUND, false) || ACTION_RESTART.equals(intentAction); } + // intentAction is null if the service is restarted or no action is specified. + if (intentAction == null) { + intentAction = ACTION_INIT; + } logd("onStartCommand action: " + intentAction + " startId: " + startId); switch (intentAction) { case ACTION_INIT: @@ -260,6 +266,12 @@ public abstract class DownloadService extends Service { return START_STICKY; } + @Override + public void onTaskRemoved(Intent rootIntent) { + logd("onTaskRemoved rootIntent: " + rootIntent); + taskRemoved = true; + } + @Override public void onDestroy() { logd("onDestroy"); @@ -353,8 +365,13 @@ public abstract class DownloadService extends Service { if (startedInForeground && Util.SDK_INT >= 26) { foregroundNotificationUpdater.showNotificationIfNotAlready(); } - boolean stopSelfResult = stopSelfResult(lastStartId); - logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult); + if (Util.SDK_INT < 28 && taskRemoved) { // See [Internal: b/74248644]. + stopSelf(); + logd("stopSelf()"); + } else { + boolean stopSelfResult = stopSelfResult(lastStartId); + logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult); + } } private void logd(String message) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java index 0a841fa38f..97832abfc7 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java @@ -55,10 +55,18 @@ public final class DownloadNotificationUtil { int downloadTaskCount = 0; boolean allDownloadPercentagesUnknown = true; boolean haveDownloadedBytes = false; + boolean haveDownloadTasks = false; + boolean haveRemoveTasks = false; for (TaskState taskState : taskStates) { - if (taskState.action.isRemoveAction || taskState.state != TaskState.STATE_STARTED) { + if (taskState.state != TaskState.STATE_STARTED + && taskState.state != TaskState.STATE_COMPLETED) { continue; } + if (taskState.action.isRemoveAction) { + haveRemoveTasks = true; + continue; + } + haveDownloadTasks = true; if (taskState.downloadPercentage != C.PERCENTAGE_UNSET) { allDownloadPercentagesUnknown = false; totalPercentage += taskState.downloadPercentage; @@ -67,18 +75,20 @@ public final class DownloadNotificationUtil { downloadTaskCount++; } - boolean haveDownloadTasks = downloadTaskCount > 0; int titleStringId = haveDownloadTasks ? R.string.exo_download_downloading - : (taskStates.length > 0 ? R.string.exo_download_removing : NULL_STRING_ID); + : (haveRemoveTasks ? R.string.exo_download_removing : NULL_STRING_ID); NotificationCompat.Builder notificationBuilder = newNotificationBuilder( context, smallIcon, channelId, contentIntent, message, titleStringId); - int progress = haveDownloadTasks ? (int) (totalPercentage / downloadTaskCount) : 0; - boolean indeterminate = - !haveDownloadTasks || (allDownloadPercentagesUnknown && haveDownloadedBytes); + int progress = 0; + boolean indeterminate = true; + if (haveDownloadTasks) { + progress = (int) (totalPercentage / downloadTaskCount); + indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes; + } notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate); notificationBuilder.setOngoing(true); notificationBuilder.setShowWhen(false); From 0a46e74105eb51f3499ec544260e25a2ac2f6848 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 12 Jul 2018 01:47:22 -0700 Subject: [PATCH 074/106] Ignore all edit lists if one track's edits can't be applied Issue: #4348 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=204261718 --- RELEASENOTES.md | 2 + .../exoplayer2/extractor/mp4/AtomParsers.java | 47 ++++++++------ .../extractor/mp4/Mp4Extractor.java | 65 ++++++++++++++----- .../extractor/mp4/TrackSampleTable.java | 28 +++----- 4 files changed, 86 insertions(+), 56 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 86ddada1f0..09c920ddda 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -21,6 +21,8 @@ * Add workaround for track index mismatches between tfhd and tkhd boxes in fragmented MP4 files ([#4083](https://github.com/google/ExoPlayer/issues/4083)). +* Ignore all MP4 edit lists if one edit list couldn't be handled + ([#4348](https://github.com/google/ExoPlayer/issues/4348)). * Fix issue when switching track selection from an embedded track to a primary track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index a2b787d6b0..d11914919a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -43,6 +43,9 @@ import java.util.List; */ /* package */ final class AtomParsers { + /** Thrown if an edit list couldn't be applied. */ + public static final class UnhandledEditListException extends ParserException {} + private static final String TAG = "AtomParsers"; private static final int TYPE_vide = Util.getIntegerCodeForString("vide"); @@ -117,10 +120,12 @@ import java.util.List; * @param stblAtom stbl (sample table) atom to decode. * @param gaplessInfoHolder Holder to populate with gapless playback information. * @return Sample table described by the stbl atom. - * @throws ParserException If the resulting sample sequence does not contain a sync sample. + * @throws UnhandledEditListException Thrown if the edit list can't be applied. + * @throws ParserException Thrown if the stbl atom can't be parsed. */ - public static TrackSampleTable parseStbl(Track track, Atom.ContainerAtom stblAtom, - GaplessInfoHolder gaplessInfoHolder) throws ParserException { + public static TrackSampleTable parseStbl( + Track track, Atom.ContainerAtom stblAtom, GaplessInfoHolder gaplessInfoHolder) + throws ParserException { SampleSizeBox sampleSizeBox; Atom.LeafAtom stszAtom = stblAtom.getLeafAtomOfType(Atom.TYPE_stsz); if (stszAtom != null) { @@ -136,7 +141,13 @@ import java.util.List; int sampleCount = sampleSizeBox.getSampleCount(); if (sampleCount == 0) { return new TrackSampleTable( - new long[0], new int[0], 0, new long[0], new int[0], C.TIME_UNSET); + track, + /* offsets= */ new long[0], + /* sizes= */ new int[0], + /* maximumSize= */ 0, + /* timestampsUs= */ new long[0], + /* flags= */ new int[0], + /* durationUs= */ C.TIME_UNSET); } // Entries are byte offsets of chunks. @@ -315,7 +326,8 @@ import java.util.List; // There is no edit list, or we are ignoring it as we already have gapless metadata to apply. // This implementation does not support applying both gapless metadata and an edit list. Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a @@ -342,7 +354,8 @@ import java.util.List; gaplessInfoHolder.encoderDelay = (int) encoderDelay; gaplessInfoHolder.encoderPadding = (int) encoderPadding; Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } } } @@ -359,7 +372,8 @@ import java.util.List; } durationUs = Util.scaleLargeTimestamp(duration - editStartTime, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); + return new TrackSampleTable( + track, offsets, sizes, maximumSize, timestamps, flags, durationUs); } // Omit any sample at the end point of an edit for audio tracks. @@ -409,6 +423,11 @@ import java.util.List; System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count); System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count); } + if (startIndex < endIndex && (editedFlags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) == 0) { + // Applying the edit list would require prerolling from a sync sample. + Log.w(TAG, "Ignoring edit list: edit does not start with a sync sample."); + throw new UnhandledEditListException(); + } for (int j = startIndex; j < endIndex; j++) { long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); long timeInSegmentUs = @@ -424,20 +443,8 @@ import java.util.List; pts += editDuration; } long editedDurationUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.timescale); - - boolean hasSyncSample = false; - for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) { - hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0; - } - if (!hasSyncSample) { - // We don't support edit lists where the edited sample sequence doesn't contain a sync sample. - // Such edit lists are often (although not always) broken, so we ignore it and continue. - Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample."); - Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); - return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags, durationUs); - } - return new TrackSampleTable( + track, editedOffsets, editedSizes, editedMaximumSize, 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 e70a49a2d7..1b455ab9e2 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 @@ -391,25 +391,21 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } - for (int i = 0; i < moov.containerChildren.size(); i++) { - Atom.ContainerAtom atom = moov.containerChildren.get(i); - if (atom.type != Atom.TYPE_trak) { - continue; - } - - Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), - C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, isQuickTime); - if (track == null) { - continue; - } - - Atom.ContainerAtom stblAtom = atom.getContainerAtomOfType(Atom.TYPE_mdia) - .getContainerAtomOfType(Atom.TYPE_minf).getContainerAtomOfType(Atom.TYPE_stbl); - TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); - if (trackSampleTable.sampleCount == 0) { - continue; - } + boolean ignoreEditLists = (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0; + ArrayList trackSampleTables; + try { + trackSampleTables = getTrackSampleTables(moov, gaplessInfoHolder, ignoreEditLists); + } catch (AtomParsers.UnhandledEditListException e) { + // Discard gapless info as we aren't able to handle corresponding edits. + gaplessInfoHolder = new GaplessInfoHolder(); + trackSampleTables = + getTrackSampleTables(moov, gaplessInfoHolder, /* ignoreEditLists= */ true); + } + int trackCount = trackSampleTables.size(); + for (int i = 0; i < trackCount; i++) { + TrackSampleTable trackSampleTable = trackSampleTables.get(i); + Track track = trackSampleTable.track; Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i, track.type)); // Each sample has up to three bytes of overhead for the start code that replaces its length. @@ -445,6 +441,39 @@ public final class Mp4Extractor implements Extractor, SeekMap { extractorOutput.seekMap(this); } + private ArrayList getTrackSampleTables( + ContainerAtom moov, GaplessInfoHolder gaplessInfoHolder, boolean ignoreEditLists) + throws ParserException { + ArrayList trackSampleTables = new ArrayList<>(); + for (int i = 0; i < moov.containerChildren.size(); i++) { + Atom.ContainerAtom atom = moov.containerChildren.get(i); + if (atom.type != Atom.TYPE_trak) { + continue; + } + Track track = + AtomParsers.parseTrak( + atom, + moov.getLeafAtomOfType(Atom.TYPE_mvhd), + /* duration= */ C.TIME_UNSET, + /* drmInitData= */ null, + ignoreEditLists, + isQuickTime); + if (track == null) { + continue; + } + Atom.ContainerAtom stblAtom = + atom.getContainerAtomOfType(Atom.TYPE_mdia) + .getContainerAtomOfType(Atom.TYPE_minf) + .getContainerAtomOfType(Atom.TYPE_stbl); + TrackSampleTable trackSampleTable = AtomParsers.parseStbl(track, stblAtom, gaplessInfoHolder); + if (trackSampleTable.sampleCount == 0) { + continue; + } + trackSampleTables.add(trackSampleTable); + } + return trackSampleTables; + } + /** * Attempts to extract the next sample in the current mdat atom for the specified track. *

    diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java index 9f77c49664..56851fc1e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java @@ -24,29 +24,19 @@ import com.google.android.exoplayer2.util.Util; */ /* package */ final class TrackSampleTable { - /** - * Number of samples. - */ + /** The track corresponding to this sample table. */ + public final Track track; + /** Number of samples. */ public final int sampleCount; - /** - * Sample offsets in bytes. - */ + /** Sample offsets in bytes. */ public final long[] offsets; - /** - * Sample sizes in bytes. - */ + /** Sample sizes in bytes. */ public final int[] sizes; - /** - * Maximum sample size in {@link #sizes}. - */ + /** Maximum sample size in {@link #sizes}. */ public final int maximumSize; - /** - * Sample timestamps in microseconds. - */ + /** Sample timestamps in microseconds. */ public final long[] timestampsUs; - /** - * Sample flags. - */ + /** Sample flags. */ public final int[] flags; /** * The duration of the track sample table in microseconds, or {@link C#TIME_UNSET} if the sample @@ -55,6 +45,7 @@ import com.google.android.exoplayer2.util.Util; public final long durationUs; public TrackSampleTable( + Track track, long[] offsets, int[] sizes, int maximumSize, @@ -65,6 +56,7 @@ import com.google.android.exoplayer2.util.Util; Assertions.checkArgument(offsets.length == timestampsUs.length); Assertions.checkArgument(flags.length == timestampsUs.length); + this.track = track; this.offsets = offsets; this.sizes = sizes; this.maximumSize = maximumSize; From d24e7cdffe53c545c8125d0aa226602313321577 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 13 Jul 2018 07:08:13 -0700 Subject: [PATCH 075/106] Fix a bug with TTML using font size as % of cellResolution. After [] we support default font size for TTML, relative to the cellResolution of the document. However, this introduced a bug that makes TTML font-size in such case always follow the cellResolution font size, even when SubtitleView.setApplyEmbeddedStyles(false) and SubtitleView.setApplyEmbeddedFontSizes(false) were used. This CL updates the fix so that the default font-size using cellResolution works in the same way as other embedded styles, and can be turned off using setters from SubtitleView. GitHub: #4491 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=204467033 --- RELEASENOTES.md | 8 ++- .../exoplayer2/ui/SubtitlePainter.java | 61 +++++++++++++------ .../android/exoplayer2/ui/SubtitleView.java | 13 ++-- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 09c920ddda..1829d4a956 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,13 +2,17 @@ ### 2.8.3 ### +* Captions: + * TTML: Fix an issue with TTML using font size as % of cell resolution that + makes `SubtitleView.setApplyEmbeddedFontSizes()` not work correctly. + ([#4491](https://github.com/google/ExoPlayer/issues/4491)). + * CEA-608: Improve handling of embedded styles + ([#4321](https://github.com/google/ExoPlayer/issues/4321)). * DASH: Exclude text streams from duration calculations ([#4029](https://github.com/google/ExoPlayer/issues/4029)). * DRM: * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). -* CEA-608: Improve handling of embedded styles - ([#4321](https://github.com/google/ExoPlayer/issues/4321)). * IMA: Fix behavior when creating/releasing the player then releasing `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). * Fix issue playing DRM protected streams on Asus Zenfone 2 diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index c5d264b310..3ad3e9d496 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -89,7 +89,8 @@ import com.google.android.exoplayer2.util.Util; private int edgeColor; @CaptionStyleCompat.EdgeType private int edgeType; - private float textSizePx; + private float defaultTextSizePx; + private float cueTextSizePx; private float bottomPaddingFraction; private int parentLeft; private int parentTop; @@ -130,8 +131,8 @@ import com.google.android.exoplayer2.util.Util; /** * Draws the provided {@link Cue} into a canvas with the specified styling. - *

    - * A call to this method is able to use cached results of calculations made during the previous + * + *

    A call to this method is able to use cached results of calculations made during the previous * call, and so an instance of this class is able to optimize repeated calls to this method in * which the same parameters are passed. * @@ -140,7 +141,8 @@ import com.google.android.exoplayer2.util.Util; * @param applyEmbeddedFontSizes If {@code applyEmbeddedStyles} is true, defines whether font * sizes embedded within the cue should be applied. Otherwise, it is ignored. * @param style The style to use when drawing the cue text. - * @param textSizePx The text size to use when drawing the cue text, in pixels. + * @param defaultTextSizePx The default text size to use when drawing the text, in pixels. + * @param cueTextSizePx The embedded text size of this cue, in pixels. * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is * {@link Cue#DIMEN_UNSET}, as a fraction of the viewport height * @param canvas The canvas into which to draw. @@ -149,9 +151,19 @@ import com.google.android.exoplayer2.util.Util; * @param cueBoxRight The right position of the enclosing cue box. * @param cueBoxBottom The bottom position of the enclosing cue box. */ - public void draw(Cue cue, boolean applyEmbeddedStyles, boolean applyEmbeddedFontSizes, - CaptionStyleCompat style, float textSizePx, float bottomPaddingFraction, Canvas canvas, - int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { + public void draw( + Cue cue, + boolean applyEmbeddedStyles, + boolean applyEmbeddedFontSizes, + CaptionStyleCompat style, + float defaultTextSizePx, + float cueTextSizePx, + float bottomPaddingFraction, + Canvas canvas, + int cueBoxLeft, + int cueBoxTop, + int cueBoxRight, + int cueBoxBottom) { boolean isTextCue = cue.bitmap == null; int windowColor = Color.BLACK; if (isTextCue) { @@ -180,7 +192,8 @@ import com.google.android.exoplayer2.util.Util; && this.edgeType == style.edgeType && this.edgeColor == style.edgeColor && Util.areEqual(this.textPaint.getTypeface(), style.typeface) - && this.textSizePx == textSizePx + && this.defaultTextSizePx == defaultTextSizePx + && this.cueTextSizePx == cueTextSizePx && this.bottomPaddingFraction == bottomPaddingFraction && this.parentLeft == cueBoxLeft && this.parentTop == cueBoxTop @@ -209,7 +222,8 @@ import com.google.android.exoplayer2.util.Util; this.edgeType = style.edgeType; this.edgeColor = style.edgeColor; this.textPaint.setTypeface(style.typeface); - this.textSizePx = textSizePx; + this.defaultTextSizePx = defaultTextSizePx; + this.cueTextSizePx = cueTextSizePx; this.bottomPaddingFraction = bottomPaddingFraction; this.parentLeft = cueBoxLeft; this.parentTop = cueBoxTop; @@ -228,8 +242,8 @@ import com.google.android.exoplayer2.util.Util; int parentWidth = parentRight - parentLeft; int parentHeight = parentBottom - parentTop; - textPaint.setTextSize(textSizePx); - int textPaddingX = (int) (textSizePx * INNER_PADDING_RATIO + 0.5f); + textPaint.setTextSize(defaultTextSizePx); + int textPaddingX = (int) (defaultTextSizePx * INNER_PADDING_RATIO + 0.5f); int availableWidth = parentWidth - textPaddingX * 2; if (cueSize != Cue.DIMEN_UNSET) { @@ -240,14 +254,12 @@ import com.google.android.exoplayer2.util.Util; return; } + CharSequence cueText = this.cueText; // Remove embedded styling or font size if requested. - CharSequence cueText; - if (applyEmbeddedFontSizes && applyEmbeddedStyles) { - cueText = this.cueText; - } else if (!applyEmbeddedStyles) { - cueText = this.cueText.toString(); // Equivalent to erasing all spans. - } else { - SpannableStringBuilder newCueText = new SpannableStringBuilder(this.cueText); + if (!applyEmbeddedStyles) { + cueText = cueText.toString(); // Equivalent to erasing all spans. + } else if (!applyEmbeddedFontSizes) { + SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText); int cueLength = newCueText.length(); AbsoluteSizeSpan[] absSpans = newCueText.getSpans(0, cueLength, AbsoluteSizeSpan.class); RelativeSizeSpan[] relSpans = newCueText.getSpans(0, cueLength, RelativeSizeSpan.class); @@ -258,6 +270,19 @@ import com.google.android.exoplayer2.util.Util; newCueText.removeSpan(relSpan); } cueText = newCueText; + } else { + // Apply embedded styles & font size. + if (cueTextSizePx > 0) { + // Use a SpannableStringBuilder encompassing the whole cue text to apply the default + // cueTextSizePx. + SpannableStringBuilder newCueText = new SpannableStringBuilder(cueText); + newCueText.setSpan( + new AbsoluteSizeSpan((int) cueTextSizePx), + /* start= */ 0, + /* end= */ newCueText.length(), + Spanned.SPAN_PRIORITY); + cueText = newCueText; + } } Alignment textAlignment = cueTextAlignment == null ? Alignment.ALIGN_CENTER : cueTextAlignment; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index bb9c38d886..a3db02bee5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -269,15 +269,15 @@ public final class SubtitleView extends View implements TextOutput { for (int i = 0; i < cueCount; i++) { Cue cue = cues.get(i); - float textSizePx = - resolveTextSizeForCue(cue, rawViewHeight, viewHeightMinusPadding, defaultViewTextSizePx); + float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding); SubtitlePainter painter = painters.get(i); painter.draw( cue, applyEmbeddedStyles, applyEmbeddedFontSizes, style, - textSizePx, + defaultViewTextSizePx, + cueTextSizePx, bottomPaddingFraction, canvas, left, @@ -287,14 +287,13 @@ public final class SubtitleView extends View implements TextOutput { } } - private float resolveTextSizeForCue( - Cue cue, int rawViewHeight, int viewHeightMinusPadding, float defaultViewTextSizePx) { + private float resolveCueTextSize(Cue cue, int rawViewHeight, int viewHeightMinusPadding) { if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) { - return defaultViewTextSizePx; + return 0; } float defaultCueTextSizePx = resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding); - return defaultCueTextSizePx > 0 ? defaultCueTextSizePx : defaultViewTextSizePx; + return Math.max(defaultCueTextSizePx, 0); } private float resolveTextSize( From 0b8724cb668b7c09008dffcef0652f7ff0456b3d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 13 Jul 2018 07:13:46 -0700 Subject: [PATCH 076/106] Fix bug where sourceId wasn't set for the first chunk The sample queues haven't been created when the first init call occurs. In this case we need to set sourceId when we subsequently create the queues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=204467538 --- .../android/exoplayer2/source/hls/HlsSampleStreamWrapper.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 705320bdad..e0b236fb4a 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -136,6 +136,7 @@ import java.util.Arrays; // Accessed only by the loading thread. private boolean tracksEnded; private long sampleOffsetUs; + private int chunkUid; /** * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. @@ -650,6 +651,7 @@ import java.util.Arrays; audioSampleQueueMappingDone = false; videoSampleQueueMappingDone = false; } + this.chunkUid = chunkUid; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.sourceId(chunkUid); } @@ -704,6 +706,7 @@ import java.util.Arrays; } } SampleQueue trackOutput = new SampleQueue(allocator); + trackOutput.sourceId(chunkUid); trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.setUpstreamFormatChangeListener(this); sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); From 9bb64b7e44dee50baf8eed7ee444575aa6ddaf87 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 20 Jul 2018 03:01:16 -0700 Subject: [PATCH 077/106] Add support for setting companion ad slots ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205373398 --- RELEASENOTES.md | 6 ++++-- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1829d4a956..1249be7acb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,8 +13,10 @@ * DRM: * Allow DrmInitData to carry a license server URL ([#3393](https://github.com/google/ExoPlayer/issues/3393)). -* IMA: Fix behavior when creating/releasing the player then releasing - `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). +* IMA: + * Fix behavior when creating/releasing the player then releasing + `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). + * Add support for setting slots for companion ads. * Fix issue playing DRM protected streams on Asus Zenfone 2 ([#4403](https://github.com/google/ExoPlayer/issues/4413)). * Add support for multiple audio and video tracks in MPEG-PS streams diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 9045ee777a..cce45f2d2c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -38,6 +38,7 @@ import com.google.ads.interactivemedia.v3.api.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.CompanionAdSlot; import com.google.ads.interactivemedia.v3.api.ImaSdkFactory; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; @@ -62,6 +63,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -395,6 +397,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adsLoader; } + /** + * Sets the slots for displaying companion ads. Individual slots can be created using {@link + * ImaSdkFactory#createCompanionAdSlot()}. + * + * @param companionSlots Slots for displaying companion ads. + * @see AdDisplayContainer#setCompanionSlots(Collection) + */ + public void setCompanionSlots(Collection companionSlots) { + adDisplayContainer.setCompanionSlots(companionSlots); + } + /** * Requests ads, if they have not already been requested. Must be called on the main thread. * From e03623f7012cd25075fd90d01dad4504fac684cf Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 20 Jul 2018 03:25:11 -0700 Subject: [PATCH 078/106] Fix issue with keeping window sequence number after repeated seeks. The number is shelved in calls to queue.clear() to keep it for the next media period. However, the queue may also become empty by repeated calls to advancePlayingPeriod which may happen when seeking to an unprepared period. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205376036 --- .../android/exoplayer2/MediaPeriodQueue.java | 4 +- .../android/exoplayer2/ExoPlayerTest.java | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) 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 717f873622..17a8ddd8d4 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 @@ -228,11 +228,13 @@ import com.google.android.exoplayer2.util.Assertions; reading = playing.next; } playing.release(); - playing = playing.next; length--; if (length == 0) { loading = null; + oldFrontPeriodUid = playing.uid; + oldFrontPeriodWindowSequenceNumber = playing.info.id.windowSequenceNumber; } + playing = playing.next; } else { playing = loading; reading = loading; 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 c05f8914f5..559a915b2b 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 @@ -1980,6 +1980,44 @@ public final class ExoPlayerTest { .inOrder(); } + @Test + public void testRepeatedSeeksToUnpreparedPeriodInSameWindowKeepsWindowSequenceNumber() + throws Exception { + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 2, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ 10 * C.MICROS_PER_SECOND)); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, /* manifest= */ null); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekToUnpreparedPeriod") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .seek(/* windowIndex= */ 0, /* positionMs= */ 9999) + .seek(/* windowIndex= */ 0, /* positionMs= */ 1) + .seek(/* windowIndex= */ 0, /* positionMs= */ 9999) + .play() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + + testRunner.assertPlayedPeriodIndices(0, 1, 0, 1); + assertThat(mediaSource.getCreatedMediaPeriods()) + .containsAllOf( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + assertThat(mediaSource.getCreatedMediaPeriods()) + .doesNotContain(new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); + } + @Test public void testRecursivePlayerChangesReportConsistentValuesForAllListeners() throws Exception { // We add two listeners to the player. The first stops the player as soon as it's ready and both From 7d4ac516e72233ebee63bc57fa4529c8b76c54d1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 23 Jul 2018 03:12:22 -0700 Subject: [PATCH 079/106] Update period index in DashMediaPeriod event dispatcher after manifest update. Issue:#4492 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205636634 --- .../android/exoplayer2/source/dash/DashMediaPeriod.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index ddb655fa92..e217c244b5 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -61,7 +61,6 @@ import java.util.List; /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; private final int minLoadableRetryCount; - private final EventDispatcher eventDispatcher; private final long elapsedRealtimeOffset; private final LoaderErrorThrower manifestLoaderErrorThrower; private final Allocator allocator; @@ -72,6 +71,7 @@ import java.util.List; private final IdentityHashMap, PlayerTrackEmsgHandler> trackEmsgHandlerBySampleStream; + private EventDispatcher eventDispatcher; private @Nullable Callback callback; private ChunkSampleStream[] sampleStreams; private EventSampleStream[] eventSampleStreams; @@ -126,6 +126,13 @@ import java.util.List; */ public void updateManifest(DashManifest manifest, int periodIndex) { this.manifest = manifest; + if (this.periodIndex != periodIndex) { + eventDispatcher = + eventDispatcher.withParameters( + /* windowIndex= */ 0, + eventDispatcher.mediaPeriodId.copyWithPeriodIndex(periodIndex), + manifest.getPeriod(periodIndex).startMs); + } this.periodIndex = periodIndex; playerEmsgHandler.updateManifest(manifest); if (sampleStreams != null) { From 50c8197004884196701b83d987a5c5af451c6296 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 23 Jul 2018 14:03:59 +0100 Subject: [PATCH 080/106] Widen setOutputSurface workaround --- .../video/MediaCodecVideoRenderer.java | 204 ++++++++++++++---- 1 file changed, 167 insertions(+), 37 deletions(-) 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 5e8a98ea68..916c311217 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 @@ -84,6 +84,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // pending output streams that have fewer frames than the codec latency. private static final int MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT = 10; + private static boolean evaluatedDeviceNeedsSetOutputSurfaceWorkaround; + private static boolean deviceNeedsSetOutputSurfaceWorkaround; + private final Context context; private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper; private final EventDispatcher eventDispatcher; @@ -825,11 +828,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private boolean shouldUseDummySurface(MediaCodecInfo codecInfo) { return Util.SDK_INT >= 23 && !tunneling - && !codecNeedsDummySurfaceWorkaround(codecInfo.name) && !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name) && (!codecInfo.secure || DummySurface.isSecureSupported(context)); } - + private void setJoiningDeadlineMs() { joiningDeadlineMs = allowedJoiningTimeMs > 0 ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; @@ -1172,20 +1174,36 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); } - private static boolean codecNeedsDummySurfaceWorkaround(String name) { - // Work around https://github.com/google/ExoPlayer/issues/4419. - return (("needle".equals(Util.DEVICE)) // FireTV 4K - && "OMX.amlogic.avc.decoder.awesome".equals(name)); - } - - /** - * Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} - * incorrectly. - *

    - * If true is returned then we fall back to releasing and re-instantiating the codec instead. + /* + * TODO: + * + * 1. Validate that Android device certification now ensures correct behavior, and add a + * corresponding SDK_INT upper bound for applying the workaround (probably SDK_INT < 26). + * 2. Determine a complete list of affected devices. + * 3. Some of the devices in this list only fail to support setOutputSurface when switching from + * a SurfaceView provided Surface to a Surface of another type (e.g. TextureView/DummySurface), + * and vice versa. One hypothesis is that setOutputSurface fails when the surfaces have + * different pixel formats. If we can find a way to query the Surface instances to determine + * whether this case applies, then we'll be able to provide a more targeted workaround. */ - private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { - // Work around https://github.com/google/ExoPlayer/issues/3236, + /** + * Returns whether the codec is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + * + *

    If true is returned then we fall back to releasing and re-instantiating the codec instead. + * + * @param name The name of the codec. + * @return True if the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + */ + protected boolean codecNeedsSetOutputSurfaceWorkaround(String name) { + if (Util.SDK_INT >= 27 || name.startsWith("OMX.google")) { + // Devices running API level 27 or later should also be unaffected. Google OMX decoders are + // not known to have this issue on any API level. + return false; + } + // Work around: + // https://github.com/google/ExoPlayer/issues/3236, // https://github.com/google/ExoPlayer/issues/3355, // https://github.com/google/ExoPlayer/issues/3439, // https://github.com/google/ExoPlayer/issues/3724, @@ -1194,28 +1212,140 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // https://github.com/google/ExoPlayer/issues/4084, // https://github.com/google/ExoPlayer/issues/4104, // https://github.com/google/ExoPlayer/issues/4134, - // https://github.com/google/ExoPlayer/issues/4315. - return (("deb".equals(Util.DEVICE) // Nexus 7 (2013) - || "flo".equals(Util.DEVICE) // Nexus 7 (2013) - || "mido".equals(Util.DEVICE) // Redmi Note 4 - || "santoni".equals(Util.DEVICE)) // Redmi 4X - && "OMX.qcom.video.decoder.avc".equals(name)) - || (("tcl_eu".equals(Util.DEVICE) // TCL Percee TV - || "SVP-DTV15".equals(Util.DEVICE) // Sony Bravia 4K 2015 - || "BRAVIA_ATV2".equals(Util.DEVICE) // Sony Bravia 4K GB - || Util.DEVICE.startsWith("panell_") // Motorola Moto C Plus - || "F3311".equals(Util.DEVICE) // Sony Xperia E5 - || "M5c".equals(Util.DEVICE) // Meizu M5C - || "QM16XE_U".equals(Util.DEVICE) // Philips QM163E - || "A7010a48".equals(Util.DEVICE) // Lenovo K4 Note - || "woods_f".equals(Util.MODEL) // Moto E (4) - || "watson".equals(Util.DEVICE)) // Moto C - && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) - || (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite - || "CAM-L21".equals(Util.MODEL)) // Huawei Y6II - && "OMX.k3.video.decoder.avc".equals(name)) - || (("HUAWEI VNS-L21".equals(Util.MODEL)) // Huawei P9 Lite - && "OMX.IMG.MSVDX.Decoder.AVC".equals(name)); + // https://github.com/google/ExoPlayer/issues/4315, + // https://github.com/google/ExoPlayer/issues/4419, + // https://github.com/google/ExoPlayer/issues/4460, + // https://github.com/google/ExoPlayer/issues/4468. + synchronized (MediaCodecVideoRenderer.class) { + if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) { + switch (Util.DEVICE) { + case "1601": + case "1713": + case "1714": + case "A10-70F": + case "A1601": + case "A2016a40": + case "A7000-a": + case "A7000plus": + case "A7010a48": + case "A7020a48": + case "AquaPowerM": + case "Aura_Note_2": + case "BLACK-1X": + case "BRAVIA_ATV2": + case "C1": + case "ComioS1": + case "CP8676_I02": + case "CPH1609": + case "CPY83_I00": + case "cv1": + case "cv3": + case "deb": + case "E5643": + case "ELUGA_A3_Pro": + case "ELUGA_Note": + case "ELUGA_Prim": + case "ELUGA_Ray_X": + case "EverStar_S": + case "F3111": + case "F3113": + case "F3116": + case "F3211": + case "F3213": + case "F3215": + case "F3311": + case "flo": + case "GiONEE_CBL7513": + case "GiONEE_GBL7319": + case "GIONEE_GBL7360": + case "GIONEE_SWW1609": + case "GIONEE_SWW1627": + case "GIONEE_SWW1631": + case "GIONEE_WBL5708": + case "GIONEE_WBL7365": + case "GIONEE_WBL7519": + case "griffin": + case "htc_e56ml_dtul": + case "hwALE-H": + case "HWBLN-H": + case "HWCAM-H": + case "HWVNS-H": + case "iball8735_9806": + case "Infinix-X572": + case "iris60": + case "itel_S41": + case "j2xlteins": + case "JGZ": + case "K50a40": + case "le_x6": + case "LS-5017": + case "M5c": + case "manning": + case "marino_f": + case "MEIZU_M5": + case "mh": + case "mido": + case "MX6": + case "namath": + case "nicklaus_f": + case "NX541J": + case "NX573J": + case "OnePlus5T": + case "p212": + case "P681": + case "P85": + case "panell_d": + case "panell_dl": + case "panell_ds": + case "panell_dt": + case "PB2-670M": + case "PGN528": + case "PGN610": + case "PGN611": + case "Phantom6": + case "Pixi4-7_3G": + case "Pixi5-10_4G": + case "PLE": + case "Q350": + case "Q4260": + case "Q427": + case "Q4310": + case "Q5": + case "QM16XE_U": + case "QX1": + case "santoni": + case "Slate_Pro": + case "SVP-DTV15": + case "s905x018": + case "taido_row": + case "TB3-730F": + case "TB3-730X": + case "TB3-850F": + case "TB3-850M": + case "tcl_eu": + case "V1": + case "V23GB": + case "V5": + case "vernee_M5": + case "watson": + case "whyred": + case "woods_f": + case "woods_fn": + case "X3_HK": + case "XE2X": + case "XT1663": + case "Z12_PRO": + case "Z80": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Workaround not required. + break; + } + evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true; + } + } + return deviceNeedsSetOutputSurfaceWorkaround; } protected static final class CodecMaxValues { From dcd549bd3e689e54d953c8b49cbd1b25bb7127b4 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 23 Jul 2018 14:06:29 +0100 Subject: [PATCH 081/106] Fix release branch --- .../android/exoplayer2/trackselection/DefaultTrackSelector.java | 2 +- .../java/com/google/android/exoplayer2/ui/SubtitlePainter.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 31b79cdd3f..0067ffa552 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -661,7 +661,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private Parameters() { this( - /* selectionOverrides= */ new SparseArray<>(), + /* selectionOverrides= */ new SparseArray>(), /* rendererDisabledFlags= */ new SparseBooleanArray(), /* preferredAudioLanguage= */ null, /* preferredTextLanguage= */ null, diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java index 3ad3e9d496..687cd78004 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitlePainter.java @@ -28,6 +28,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.text.Layout.Alignment; import android.text.SpannableStringBuilder; +import android.text.Spanned; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; From 224d0c2b2c68a3d9001fa7f8ce37c4d79c4d66b6 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Jun 2018 06:44:23 -0700 Subject: [PATCH 082/106] Add isControllerVisible Issue: #4385 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=200986828 --- RELEASENOTES.md | 2 ++ .../java/com/google/android/exoplayer2/ui/PlayerView.java | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1249be7acb..dd26b721c6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,6 +31,8 @@ ([#4348](https://github.com/google/ExoPlayer/issues/4348)). * Fix issue when switching track selection from an embedded track to a primary track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)). +* Add `PlayerView.isControllerVisible` + ([#4385](https://github.com/google/ExoPlayer/issues/4385)). ### 2.8.2 ### 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 a7aa48c0db..a7688bc540 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 @@ -696,6 +696,11 @@ public class PlayerView extends FrameLayout { return useController && controller.dispatchMediaKeyEvent(event); } + /** Returns whether the controller is currently visible. */ + public boolean isControllerVisible() { + return controller != null && controller.isVisible(); + } + /** * Shows the playback controls. Does nothing if playback controls are disabled. * From f39d28135dd872b4de88d5acb6378714eed5791f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 20 Jun 2018 01:38:42 -0700 Subject: [PATCH 083/106] Pass through all ID3 internal data from udta Also switch from using a CommentFrame to a new InternalFrame type for ID3 data stored with ID '----', to distinguish internal data from actual ID3 comments. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=201315254 --- RELEASENOTES.md | 2 + .../extractor/GaplessInfoHolder.java | 20 ++-- .../extractor/mp4/MetadataUtil.java | 6 +- .../metadata/id3/InternalFrame.java | 96 +++++++++++++++++++ 4 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dd26b721c6..f4f032b7fc 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -33,6 +33,8 @@ track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)). * Add `PlayerView.isControllerVisible` ([#4385](https://github.com/google/ExoPlayer/issues/4385)). +* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using + CommentFrame to InternalFrame for frames with gapless metadata in MP4. ### 2.8.2 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java index 75d8b4cf2d..54d48350fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; +import com.google.android.exoplayer2.metadata.id3.InternalFrame; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,7 +40,8 @@ public final class GaplessInfoHolder { } }; - private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; + private static final String GAPLESS_DOMAIN = "com.apple.iTunes"; + private static final String GAPLESS_DESCRIPTION = "iTunSMPB"; private static final Pattern GAPLESS_COMMENT_PATTERN = Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); @@ -91,7 +93,15 @@ public final class GaplessInfoHolder { Metadata.Entry entry = metadata.get(i); if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; - if (setFromComment(commentFrame.description, commentFrame.text)) { + if (GAPLESS_DESCRIPTION.equals(commentFrame.description) + && setFromComment(commentFrame.text)) { + return true; + } + } else if (entry instanceof InternalFrame) { + InternalFrame internalFrame = (InternalFrame) entry; + if (GAPLESS_DOMAIN.equals(internalFrame.domain) + && GAPLESS_DESCRIPTION.equals(internalFrame.description) + && setFromComment(internalFrame.text)) { return true; } } @@ -103,14 +113,10 @@ public final class GaplessInfoHolder { * Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header * or MPEG 4 user data), if valid and non-zero. * - * @param name The comment's identifier. * @param data The comment's payload data. * @return Whether the holder was populated. */ - private boolean setFromComment(String name, String data) { - if (!GAPLESS_COMMENT_ID.equals(name)) { - return false; - } + private boolean setFromComment(String data) { Matcher matcher = GAPLESS_COMMENT_PATTERN.matcher(data); if (matcher.find()) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java index fed1694925..991f765d0d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.Id3Frame; +import com.google.android.exoplayer2.metadata.id3.InternalFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -293,14 +294,13 @@ import com.google.android.exoplayer2.util.Util; data.skipBytes(atomSize - 12); } } - if (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) { - // We're only interested in iTunSMPB. + if (domain == null || name == null || dataAtomPosition == -1) { return null; } data.setPosition(dataAtomPosition); data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4) String value = data.readNullTerminatedString(dataAtomSize - 16); - return new CommentFrame(LANGUAGE_UNDEFINED, name, value); + return new InternalFrame(domain, name, value); } private static int parseUint8AttributeValue(ParsableByteArray data) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java new file mode 100644 index 0000000000..a828d80069 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/InternalFrame.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.metadata.id3; + +import android.os.Parcel; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** Internal ID3 frame that is intended for use by the player. */ +public final class InternalFrame extends Id3Frame { + + public static final String ID = "----"; + + public final String domain; + public final String description; + public final String text; + + public InternalFrame(String domain, String description, String text) { + super(ID); + this.domain = domain; + this.description = description; + this.text = text; + } + + /* package */ InternalFrame(Parcel in) { + super(ID); + domain = Assertions.checkNotNull(in.readString()); + description = Assertions.checkNotNull(in.readString()); + text = Assertions.checkNotNull(in.readString()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + InternalFrame other = (InternalFrame) obj; + return Util.areEqual(description, other.description) + && Util.areEqual(domain, other.domain) + && Util.areEqual(text, other.text); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (domain != null ? domain.hashCode() : 0); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + (text != null ? text.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return id + ": domain=" + domain + ", description=" + description; + } + + // Parcelable implementation. + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(domain); + dest.writeString(text); + } + + public static final Creator CREATOR = + new Creator() { + + @Override + public InternalFrame createFromParcel(Parcel in) { + return new InternalFrame(in); + } + + @Override + public InternalFrame[] newArray(int size) { + return new InternalFrame[size]; + } + }; +} From b0f44029b7554efc8f3c9f37f787c09b11dbc208 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 23 Jul 2018 14:44:42 +0100 Subject: [PATCH 084/106] Clean up release notes --- RELEASENOTES.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f4f032b7fc..a917bc5d38 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,10 @@ ### 2.8.3 ### +* IMA: + * Fix behavior when creating/releasing the player then releasing + `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). + * Add support for setting slots for companion ads. * Captions: * TTML: Fix an issue with TTML using font size as % of cell resolution that makes `SubtitleView.setApplyEmbeddedFontSizes()` not work correctly. @@ -10,13 +14,12 @@ ([#4321](https://github.com/google/ExoPlayer/issues/4321)). * DASH: Exclude text streams from duration calculations ([#4029](https://github.com/google/ExoPlayer/issues/4029)). -* DRM: - * Allow DrmInitData to carry a license server URL - ([#3393](https://github.com/google/ExoPlayer/issues/3393)). -* IMA: - * Fix behavior when creating/releasing the player then releasing - `ImaAdsLoader` ([#3879](https://github.com/google/ExoPlayer/issues/3879)). - * Add support for setting slots for companion ads. +* DRM: Allow DrmInitData to carry a license server URL + ([#3393](https://github.com/google/ExoPlayer/issues/3393)). +* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using + CommentFrame to InternalFrame for frames with gapless metadata in MP4. +* Add `PlayerView.isControllerVisible` + ([#4385](https://github.com/google/ExoPlayer/issues/4385)). * Fix issue playing DRM protected streams on Asus Zenfone 2 ([#4403](https://github.com/google/ExoPlayer/issues/4413)). * Add support for multiple audio and video tracks in MPEG-PS streams @@ -31,10 +34,7 @@ ([#4348](https://github.com/google/ExoPlayer/issues/4348)). * Fix issue when switching track selection from an embedded track to a primary track in DASH ([#4477](https://github.com/google/ExoPlayer/issues/4477)). -* Add `PlayerView.isControllerVisible` - ([#4385](https://github.com/google/ExoPlayer/issues/4385)). -* Expose all internal ID3 data stored in MP4 udta boxes, and switch from using - CommentFrame to InternalFrame for frames with gapless metadata in MP4. +* Improved compatibility with FireOS devices. ### 2.8.2 ### From a3de7bf39920603fed9526a4c01ea7e83dcd3bcf Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 23 Jul 2018 07:51:13 -0700 Subject: [PATCH 085/106] Update release notes + bump version ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205660355 --- constants.gradle | 4 ++-- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/constants.gradle b/constants.gradle index 5544173a3c..b8cfc7d2a4 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.8.2' - releaseVersionCode = 2802 + releaseVersion = '2.8.3' + releaseVersionCode = 2803 // Important: ExoPlayer specifies a minSdkVersion of 14 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 172eb19da3..8de3385d1a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.8.2"; + public static final String VERSION = "2.8.3"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.2"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.8.3"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2008002; + public static final int VERSION_INT = 2008003; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From a3e3f64b7951117d259945ea90e0207526b35a9c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 24 Jul 2018 06:47:38 -0700 Subject: [PATCH 086/106] Add PRO7S to surface switch workaround Issue: #4468 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205821059 --- .../google/android/exoplayer2/video/MediaCodecVideoRenderer.java | 1 + 1 file changed, 1 insertion(+) 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 916c311217..126087c04b 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 @@ -1306,6 +1306,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { case "Pixi4-7_3G": case "Pixi5-10_4G": case "PLE": + case "PRO7S": case "Q350": case "Q4260": case "Q427": From ed18be4eea1edc7cc23a05d447768373385299f7 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 1 Aug 2018 04:32:11 -0700 Subject: [PATCH 087/106] Add missing Nullable annotation Player.EventListener.onTimelineChanged Issue: #4593 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=206911927 --- .../android/exoplayer2/castdemo/PlayerManager.java | 3 ++- .../google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 4 ++-- .../exoplayer2/ext/leanback/LeanbackPlayerAdapter.java | 4 ++-- .../ext/mediasession/MediaSessionConnector.java | 4 ++-- .../main/java/com/google/android/exoplayer2/Player.java | 9 +++++---- .../android/exoplayer2/analytics/AnalyticsCollector.java | 2 +- .../com/google/android/exoplayer2/ExoPlayerTest.java | 2 +- .../google/android/exoplayer2/ui/PlayerControlView.java | 2 +- .../android/exoplayer2/ui/PlayerNotificationManager.java | 2 +- .../com/google/android/exoplayer2/testutil/Action.java | 4 +++- .../android/exoplayer2/testutil/ExoPlayerTestRunner.java | 4 ++-- 11 files changed, 22 insertions(+), 18 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 63b18b0aa7..6c8d7a294f 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.castdemo; import android.content.Context; import android.net.Uri; +import android.support.annotation.Nullable; import android.view.KeyEvent; import android.view.View; import com.google.android.exoplayer2.C; @@ -282,7 +283,7 @@ import java.util.ArrayList; @Override public void onTimelineChanged( - Timeline timeline, Object manifest, @TimelineChangeReason int reason) { + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { updateCurrentItemIndex(); if (timeline.isEmpty()) { castMediaQueueCreationPending = true; diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index cce45f2d2c..dc2df9eb71 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -795,8 +795,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Player.EventListener implementation. @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { // The player is being reset and this source will be released. return; diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 03f53c263f..a279874ec1 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -281,8 +281,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { Callback callback = getCallback(); callback.onDurationChanged(LeanbackPlayerAdapter.this); callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 4bafaa4326..039115174c 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -674,8 +674,8 @@ public final class MediaSessionConnector { private int currentWindowCount; @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { int windowCount = player.getCurrentTimeline().getWindowCount(); int windowIndex = player.getCurrentWindowIndex(); if (queueNavigator != null) { 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 328816d709..8501f5b827 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 @@ -191,7 +191,8 @@ public interface Player { * @param manifest The latest manifest. May be null. * @param reason The {@link TimelineChangeReason} responsible for this timeline change. */ - void onTimelineChanged(Timeline timeline, Object manifest, @TimelineChangeReason int reason); + void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason); /** * Called when the available or selected tracks change. @@ -281,8 +282,8 @@ public interface Player { abstract class DefaultEventListener implements EventListener { @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason) { // Call deprecated version. Otherwise, do nothing. onTimelineChanged(timeline, manifest); } @@ -337,7 +338,7 @@ public interface Player { * instead. */ @Deprecated - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, @Nullable Object manifest) { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 8f4267efce..16c1c5d170 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -420,7 +420,7 @@ public class AnalyticsCollector @Override public final void onTimelineChanged( - Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { mediaPeriodQueueTracker.onTimelineChanged(timeline); EventTime eventTime = generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { 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 559a915b2b..44f6f2aa82 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 @@ -2078,7 +2078,7 @@ public final class ExoPlayerTest { final EventListener eventListener = new DefaultEventListener() { @Override - public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { + public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { if (timeline.isEmpty()) { playerReference.get().setPlayWhenReady(/* playWhenReady= */ false); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 63c791d166..d673f3675b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -1088,7 +1088,7 @@ public class PlayerControlView extends FrameLayout { @Override public void onTimelineChanged( - Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { updateNavigation(); updateTimeBarMode(); updateProgress(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 19051ba932..e64d03e0bf 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -949,7 +949,7 @@ public class PlayerNotificationManager { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest, int reason) { + public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { return; } 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 a6c3438a52..1d9ccf4b50 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 @@ -575,7 +575,9 @@ public abstract class Action { new Player.DefaultEventListener() { @Override public void onTimelineChanged( - Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + Timeline timeline, + @Nullable Object manifest, + @Player.TimelineChangeReason int reason) { if (expectedTimeline == null || timeline.equals(expectedTimeline)) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); 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 cf7470b80a..101f2c4817 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 @@ -601,8 +601,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener // Player.EventListener @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { timelines.add(timeline); manifests.add(manifest); timelineChangeReasons.add(reason); From 5faa662e438d35066dd4afe77514fdeb6efbf1d1 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 1 Aug 2018 05:56:04 -0700 Subject: [PATCH 088/106] Apply setOutputSurfaceWorkaround to required FireOS devices Amazon like to use Device.MODEL, so key on that instead for these workarounds. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=206917935 --- .../exoplayer2/video/MediaCodecVideoRenderer.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 126087c04b..3c3f2ecac1 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 @@ -1340,7 +1340,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { deviceNeedsSetOutputSurfaceWorkaround = true; break; default: - // Workaround not required. + // Do nothing. + break; + } + switch (Util.MODEL) { + case "AFTA": + case "AFTN": + deviceNeedsSetOutputSurfaceWorkaround = true; + break; + default: + // Do nothing. break; } evaluatedDeviceNeedsSetOutputSurfaceWorkaround = true; From 00891f6469a8f23e58209451ae6ae4d0adfd8062 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 25 Jul 2018 07:07:53 -0700 Subject: [PATCH 089/106] Don't exclude .idea from MOE input ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=205988261 --- .idea/codeStyleSettings.xml | 496 ++++++++++++++++++++++++++++++++++++ 1 file changed, 496 insertions(+) create mode 100644 .idea/codeStyleSettings.xml diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000000..c89e6fd000 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,496 @@ + + + +

    + + + + xmlns:android + + ^$ + + + +
    +
    + + + + xmlns:.* + + ^$ + + + BY_NAME + +
    +
    + + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + style + + ^$ + + + +
    +
    + + + + .* + + ^$ + + + BY_NAME + +
    +
    + + + + .*:.*Style + + http://schemas.android.com/apk/res/android + + + BY_NAME + +
    +
    + + + + .*:layout_width + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:layout_height + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:layout_weight + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:layout_margin + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:layout_marginTop + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:layout_marginBottom + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:layout_marginStart + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:layout_marginEnd + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:layout_marginLeft + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:layout_marginRight + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:layout_.* + + http://schemas.android.com/apk/res/android + + + BY_NAME + +
    +
    + + + + .*:padding + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:paddingTop + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:paddingBottom + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:paddingStart + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:paddingEnd + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:paddingLeft + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .*:paddingRight + + http://schemas.android.com/apk/res/android + + + +
    +
    + + + + .* + http://schemas.android.com/apk/res/android + + + BY_NAME + +
    +
    + + + + .* + http://schemas.android.com/apk/res-auto + + + BY_NAME + +
    +
    + + + + .* + http://schemas.android.com/tools + + + BY_NAME + +
    +
    + + + + .* + .* + + + BY_NAME + +
    + + + + + +